Task: reaction roles + Carl-bot migration (REQ-2026-04-15-reaction-roles)
- src/discord/reactionRoles.js: REACTION_ROLE_MAP for 3 #get-roles messages (Paths, Notifications, Servers), handleReactionAdd/Remove with partial fetch + silent-fail - src/index.js: add GuildMessageReactions + DirectMessages intents, Partials for Message/Channel/Reaction (needed for pre-cache bot messages) - src/discord/events.js: wire messageReactionAdd/Remove handlers + guildMemberAdd (Wanderer role + welcome DM, silent-fail on closed DMs) - src/routes/stripe.js: post-checkout link-reminder DM for Awakened+ via req.app.locals.client, non-blocking + silent-fail
This commit is contained in:
@@ -3,8 +3,21 @@ const { handleCreateServerCommand } = require('./createserver');
|
||||
const { handleDelServerCommand } = require('./delserver');
|
||||
const { handleTasksCommand, handleTaskButton } = require('./tasks');
|
||||
const { handleVerifyMvcCommand } = require('./verifymvc');
|
||||
const { handleReactionAdd, handleReactionRemove } = require('./reactionRoles');
|
||||
const discordRoleSync = require('../services/discordRoleSync');
|
||||
|
||||
// Carl-bot migration: Wanderer role assigned to every new join.
|
||||
const WANDERER_ROLE_ID = '1487267974367805545';
|
||||
|
||||
const WELCOME_DM = (username) =>
|
||||
`Hey ${username}! Welcome to Firefrost Gaming!\n` +
|
||||
`You just landed as a Wanderer — the door is open, come explore!\n\n` +
|
||||
`Quick links:\n` +
|
||||
`• Check out #rules first\n` +
|
||||
`• Say hi in #introductions\n` +
|
||||
`• Head to #get-roles to pick your path and grab your server channels\n\n` +
|
||||
`Questions? We're here. Welcome to the family!`;
|
||||
|
||||
function registerEvents(client) {
|
||||
client.on('interactionCreate', async interaction => {
|
||||
// Button interactions
|
||||
@@ -33,6 +46,37 @@ function registerEvents(client) {
|
||||
}
|
||||
});
|
||||
|
||||
// Reaction roles — #get-roles channel
|
||||
client.on('messageReactionAdd', (reaction, user) => {
|
||||
handleReactionAdd(reaction, user).catch(err =>
|
||||
console.error('[events] reaction add error:', err.message)
|
||||
);
|
||||
});
|
||||
client.on('messageReactionRemove', (reaction, user) => {
|
||||
handleReactionRemove(reaction, user).catch(err =>
|
||||
console.error('[events] reaction remove error:', err.message)
|
||||
);
|
||||
});
|
||||
|
||||
// Carl-bot migration — new member onboarding
|
||||
client.on('guildMemberAdd', async (member) => {
|
||||
try {
|
||||
await member.roles.add(WANDERER_ROLE_ID).catch(err => {
|
||||
console.warn(`[Welcome] failed to add Wanderer to ${member.user.username}:`, err.message);
|
||||
});
|
||||
console.log(`👋 [Welcome] ${member.user.username} joined → Wanderer`);
|
||||
|
||||
// DM welcome (silent-fail if user has DMs closed)
|
||||
try {
|
||||
await member.send(WELCOME_DM(member.user.username));
|
||||
} catch (err) {
|
||||
console.log(`[Welcome] DM to ${member.user.username} skipped: ${err.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Welcome] guildMemberAdd error:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Discord bot logged in as ${client.user.tag}`);
|
||||
// Initialize role sync service with the ready client
|
||||
|
||||
95
services/arbiter-3.0/src/discord/reactionRoles.js
Normal file
95
services/arbiter-3.0/src/discord/reactionRoles.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Reaction role handling — #get-roles channel.
|
||||
* REQ-2026-04-15-reaction-roles (Chronicler #92)
|
||||
*
|
||||
* Three bot-owned messages. Each emoji maps to a Discord role ID. On add →
|
||||
* assign role. On remove → remove role. Silent-fail on lookup errors so a bad
|
||||
* reaction can never crash the gateway listener.
|
||||
*/
|
||||
|
||||
// Keyed by message ID → { emoji (unicode or custom name) → role ID }
|
||||
const REACTION_ROLE_MAP = {
|
||||
// Message 1 — Choose Your Path
|
||||
'1493930565395681402': {
|
||||
'🔥': '1482490890453782612', // Fire Path
|
||||
'❄️': '1482491234378448946', // Frost Path
|
||||
},
|
||||
// Message 2 — Notification Preferences
|
||||
'1493930595435286548': {
|
||||
'📢': '1491778391060381776', // Announcements
|
||||
'🎉': '1491778662922457199', // Events
|
||||
'🗒️': '1491778706312532171', // Patch Notes
|
||||
},
|
||||
// Message 3 — Server Roles
|
||||
'1493930614066253908': {
|
||||
'🪨': '1491028769132253274', // Stoneblock 4
|
||||
'🏝️': '1491028496284258304', // All The Mods: To the Sky
|
||||
'🔴': '1491029000108380170', // All The Mons
|
||||
'🧙': '1491029070190870548', // Mythcraft 5
|
||||
'⚔️': '1491029454011629749', // Otherworld [D&D]
|
||||
'🧟': '1491029615739801800', // DeceasedCraft
|
||||
'🍽️': '1493352900997415134', // Farm Crossing 6
|
||||
'🏡': '1491030015746510939', // Homestead
|
||||
'🌌': '1491028885981102270', // Society: Sunlit Valley
|
||||
'🌊': '1491029215963906149', // Beyond Depth
|
||||
'☁️': '1491029284159094904', // Beyond Ascension
|
||||
'🏆': '1491029373640376330', // Wold's Vaults
|
||||
'🤿': '1491029708878647356', // Submerged 2
|
||||
'🌙': '1491029870002569298', // Cottage Witch
|
||||
'🌿': '1493924685170343978', // vanilla
|
||||
}
|
||||
};
|
||||
|
||||
async function resolveReaction(reaction) {
|
||||
if (reaction.partial) {
|
||||
try { await reaction.fetch(); } catch (e) {
|
||||
console.warn('[ReactionRoles] failed to fetch partial:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const map = REACTION_ROLE_MAP[reaction.message.id];
|
||||
if (!map) return null;
|
||||
// Unicode emojis use .name; custom emojis fall back to id.
|
||||
const key = reaction.emoji.name;
|
||||
const roleId = map[key];
|
||||
if (!roleId) return null;
|
||||
return { roleId };
|
||||
}
|
||||
|
||||
async function handleReactionAdd(reaction, user) {
|
||||
try {
|
||||
if (user.bot) return;
|
||||
const resolved = await resolveReaction(reaction);
|
||||
if (!resolved) return;
|
||||
const guild = reaction.message.guild;
|
||||
if (!guild) return;
|
||||
const member = await guild.members.fetch(user.id).catch(() => null);
|
||||
if (!member) return;
|
||||
await member.roles.add(resolved.roleId).catch(err => {
|
||||
console.warn(`[ReactionRoles] add failed for ${user.username} → ${resolved.roleId}:`, err.message);
|
||||
});
|
||||
console.log(`🎭 [ReactionRoles] +${resolved.roleId} → ${user.username}`);
|
||||
} catch (err) {
|
||||
console.error('[ReactionRoles] add handler error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReactionRemove(reaction, user) {
|
||||
try {
|
||||
if (user.bot) return;
|
||||
const resolved = await resolveReaction(reaction);
|
||||
if (!resolved) return;
|
||||
const guild = reaction.message.guild;
|
||||
if (!guild) return;
|
||||
const member = await guild.members.fetch(user.id).catch(() => null);
|
||||
if (!member) return;
|
||||
await member.roles.remove(resolved.roleId).catch(err => {
|
||||
console.warn(`[ReactionRoles] remove failed for ${user.username} → ${resolved.roleId}:`, err.message);
|
||||
});
|
||||
console.log(`🎭 [ReactionRoles] -${resolved.roleId} → ${user.username}`);
|
||||
} catch (err) {
|
||||
console.error('[ReactionRoles] remove handler error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { REACTION_ROLE_MAP, handleReactionAdd, handleReactionRemove };
|
||||
@@ -5,7 +5,7 @@ const session = require('express-session');
|
||||
const PgSession = require('connect-pg-simple')(session);
|
||||
const passport = require('passport');
|
||||
const DiscordStrategy = require('passport-discord').Strategy;
|
||||
const { Client, GatewayIntentBits, REST, Routes } = require('discord.js');
|
||||
const { Client, GatewayIntentBits, Partials, REST, Routes } = require('discord.js');
|
||||
const csrf = require('csurf');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
@@ -36,7 +36,17 @@ const pgPool = new Pool({
|
||||
});
|
||||
|
||||
// Initialize Discord Client
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.DirectMessages
|
||||
],
|
||||
// Partials needed so reaction events fire for messages not in cache (bot-owned
|
||||
// #get-roles messages are from before current session).
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction]
|
||||
});
|
||||
registerEvents(client);
|
||||
|
||||
// Passport Configuration
|
||||
|
||||
@@ -296,6 +296,26 @@ router.post('/webhook', express.raw({ type: 'application/json' }), async (req, r
|
||||
console.log(`⚠️ LP meta sync skipped for ${discordId}: ${result.reason}`);
|
||||
}
|
||||
}).catch(err => console.error('[LPSync] Background sync error:', err.message));
|
||||
|
||||
// Carl-bot migration: link-reminder DM for Awakened+ subscribers (non-blocking, silent-fail)
|
||||
// REQ-2026-04-15-reaction-roles (Chronicler #92)
|
||||
(async () => {
|
||||
try {
|
||||
const discordClient = req.app.locals.client;
|
||||
if (!discordClient) return;
|
||||
const user = await discordClient.users.fetch(discordId).catch(() => null);
|
||||
if (!user) return;
|
||||
await user.send(
|
||||
`Hey ${user.username}! You're now part of the Firefrost family! 🎉\n\n` +
|
||||
`One quick step — head to #link-your-account and use the /link command ` +
|
||||
`to connect your Minecraft account so we can whitelist you on our servers.\n\n` +
|
||||
`See you in-game!`
|
||||
);
|
||||
console.log(`💌 [LinkReminder] sent to ${user.username}`);
|
||||
} catch (err) {
|
||||
console.log(`[LinkReminder] DM to ${discordId} skipped: ${err.message}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user