feat: Awakened Concierge — personalized welcome bot (Task #130)

- New service: src/services/awakenedConcierge.js
  - Fetches Discord username via Discord API
  - Calls Dify Awakened Concierge app (Gemma 4) for personalized message
  - Posts to #introductions (1403981218252324884) with typing indicator
  - Marks welcomed_at in subscriptions table
  - Non-fatal: welcome failure never breaks checkout flow

- stripe.js: calls welcomeNewMember() after syncRole() on checkout complete
- .env: CONCIERGE_API_KEY added to Command Center

Fire + Frost + Foundation 💙🔥❄️
This commit is contained in:
Claude
2026-04-13 01:22:01 +00:00
parent fd50009f67
commit a7b940b95d
2 changed files with 150 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ const cors = require('cors');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const db = require('../database');
const { syncRole, removeAllRoles, downgradeToAwakened } = require('../services/discordRoleSync');
const { welcomeNewMember } = require('../services/awakenedConcierge');
// CORS configuration for checkout endpoint
const corsOptions = {
@@ -280,6 +281,11 @@ router.post('/webhook', express.raw({ type: 'application/json' }), async (req, r
if (discordId && tierLevel) {
const roleResult = await syncRole(discordId, tierLevel);
console.log(`🎭 Role sync for ${discordId}: ${roleResult.message}`);
// Welcome new member via Awakened Concierge (non-blocking)
welcomeNewMember(discordId, client).catch(err =>
console.error('[Concierge] Background welcome error:', err.message)
);
}
break;

View File

@@ -0,0 +1,144 @@
/**
* Awakened Concierge Service
* Generates personalized welcome messages for new subscribers via Dify/Gemma 4
* Posts to #introductions channel on Discord
*
* Task #130: Awakened Concierge — Personalized Welcome Bot
* Date: April 12, 2026
*/
const DIFY_BASE_URL = process.env.DIFY_API_URL || 'https://codex.firefrostgaming.com';
const CONCIERGE_API_KEY = process.env.CONCIERGE_API_KEY;
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
const INTRODUCTIONS_CHANNEL_ID = '1403981218252324884';
/**
* Get Discord username for a Discord ID
*/
async function getDiscordUsername(discordId) {
try {
const response = await fetch(`https://discord.com/api/v10/users/${discordId}`, {
headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}` }
});
if (!response.ok) return null;
const user = await response.json();
return user.global_name || user.username || null;
} catch (err) {
console.error(`[Concierge] Failed to fetch Discord user ${discordId}:`, err.message);
return null;
}
}
/**
* Generate welcome message via Dify Awakened Concierge app
*/
async function generateWelcomeMessage(username) {
try {
const response = await fetch(`${DIFY_BASE_URL}/v1/chat-messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${CONCIERGE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputs: {},
query: `New member username: ${username}`,
response_mode: 'blocking',
user: `concierge-${Date.now()}`
})
});
if (!response.ok) {
console.error(`[Concierge] Dify API error: ${response.status}`);
return null;
}
const data = await response.json();
return data.answer || null;
} catch (err) {
console.error('[Concierge] Failed to generate welcome message:', err.message);
return null;
}
}
/**
* Post welcome message to #introductions with typing indicator
*/
async function postWelcomeToDiscord(message) {
try {
// Trigger typing indicator for natural feel
await fetch(`https://discord.com/api/v10/channels/${INTRODUCTIONS_CHANNEL_ID}/typing`, {
method: 'POST',
headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}` }
});
// Small delay — feels like someone is actually typing
await new Promise(resolve => setTimeout(resolve, 2000));
// Post the message
const response = await fetch(`https://discord.com/api/v10/channels/${INTRODUCTIONS_CHANNEL_ID}/messages`, {
method: 'POST',
headers: {
Authorization: `Bot ${DISCORD_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content: message })
});
if (!response.ok) {
console.error(`[Concierge] Discord post error: ${response.status}`);
return false;
}
return true;
} catch (err) {
console.error('[Concierge] Failed to post to Discord:', err.message);
return false;
}
}
/**
* Main entry point — welcome a new subscriber
* Called from stripe.js after successful checkout
*/
async function welcomeNewMember(discordId, client) {
if (!CONCIERGE_API_KEY) {
console.log('[Concierge] CONCIERGE_API_KEY not set, skipping welcome');
return;
}
try {
// Get their Discord username
const username = await getDiscordUsername(discordId);
if (!username) {
console.log(`[Concierge] Could not fetch username for ${discordId}, skipping`);
return;
}
console.log(`[Concierge] Generating welcome for ${username} (${discordId})`);
// Generate personalized message via Dify
const message = await generateWelcomeMessage(username);
if (!message) {
console.log(`[Concierge] No message generated for ${username}, skipping`);
return;
}
// Post to #introductions
const posted = await postWelcomeToDiscord(message);
if (posted) {
console.log(`[Concierge] ✅ Welcome posted for ${username}`);
// Mark as welcomed in DB
await client.query(
'UPDATE subscriptions SET welcomed_at = CURRENT_TIMESTAMP WHERE discord_id = $1 AND welcomed_at IS NULL',
[discordId]
);
}
} catch (err) {
// Non-fatal — welcome failure should never break checkout
console.error('[Concierge] Unexpected error:', err.message);
}
}
module.exports = { welcomeNewMember };