bug fixes

This commit is contained in:
2025-10-10 18:51:23 -04:00
parent 8236c1e0e7
commit 61ab1e1d9e
15 changed files with 4463 additions and 7 deletions

View File

@@ -87,6 +87,12 @@ async function listInvites(guildId) {
return json || [];
}
async function listReactionRoles(guildId) {
const path = `/api/servers/${guildId}/reaction-roles`;
const json = await safeFetchJsonPath(path);
return json || [];
}
async function addInvite(guildId, invite) {
const path = `/api/servers/${guildId}/invites`;
try {
@@ -127,6 +133,33 @@ async function deleteInvite(guildId, code) {
}
}
async function updateReactionRole(guildId, id, updates) {
const path = `/api/servers/${guildId}/reaction-roles/${id}`;
try {
const res = await tryFetch(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!res) return null;
try { return await res.json(); } catch (e) { return null; }
} catch (e) {
console.error(`Failed to update reaction role ${id} for ${guildId}:`, e && e.message ? e.message : e);
return null;
}
}
async function deleteReactionRole(guildId, id) {
const path = `/api/servers/${guildId}/reaction-roles/${id}`;
try {
const res = await tryFetch(path, { method: 'DELETE' });
return res && res.ok;
} catch (e) {
console.error(`Failed to delete reaction role ${id} for ${guildId}:`, e && e.message ? e.message : e);
return false;
}
}
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite };
// Twitch users helpers
async function getTwitchUsers(guildId) {
@@ -262,4 +295,4 @@ async function reconcileInvites(guildId, currentDiscordInvites) {
}
}
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings, reconcileInvites };
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, listReactionRoles, updateReactionRole, deleteReactionRole, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings, reconcileInvites };

View File

@@ -0,0 +1,21 @@
module.exports = {
name: 'post-reaction-role',
description: 'Post a reaction role message for the given reaction role ID',
builder: (builder) => builder.setName('post-reaction-role').setDescription('Post a reaction role message').addIntegerOption(opt => opt.setName('id').setDescription('Reaction role ID').setRequired(true)),
async execute(interaction) {
const id = interaction.options.getInteger('id');
try {
const api = require('../api');
const rrList = await api.listReactionRoles(interaction.guildId) || [];
const rr = rrList.find(r => Number(r.id) === Number(id));
if (!rr) return interaction.reply({ content: 'Reaction role not found', ephemeral: true });
const bot = require('../index');
const result = await bot.postReactionRoleMessage(interaction.guildId, rr);
if (result && result.success) return interaction.reply({ content: 'Posted reaction role message', ephemeral: true });
return interaction.reply({ content: 'Failed to post message', ephemeral: true });
} catch (e) {
console.error('post-reaction-role command error:', e);
return interaction.reply({ content: 'Internal error', ephemeral: true });
}
}
};

View File

@@ -12,11 +12,27 @@ for (const file of commandFiles) {
const command = require(filePath);
if (command.enabled === false || command.dev === true) continue;
if (command.builder) {
if (command.builder) {
try {
// Some command modules export builder as a function (builder => builder...) or as an instance
if (typeof command.builder === 'function') {
// create a temporary SlashCommandBuilder by requiring it from discord.js
const { SlashCommandBuilder } = require('discord.js');
const built = command.builder(new SlashCommandBuilder());
if (built && typeof built.toJSON === 'function') commands.push(built.toJSON());
else commands.push({ name: command.name, description: command.description });
} else if (command.builder && typeof command.builder.toJSON === 'function') {
commands.push(command.builder.toJSON());
} else {
} else {
commands.push({ name: command.name, description: command.description });
}
} catch (e) {
console.warn(`Failed to build command ${command.name}:`, e && e.message ? e.message : e);
commands.push({ name: command.name, description: command.description });
}
} else {
commands.push({ name: command.name, description: command.description });
}
}
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN);

View File

@@ -42,6 +42,72 @@ module.exports = {
console.log('✅ Invite reconciliation complete: no stale invites found');
}
// Reconcile reaction roles: ensure stored message IDs still exist, remove stale configs
console.log('🔄 Reconciling reaction roles (initial check)...');
try {
for (const guildId of guildIds) {
try {
const rrList = await api.listReactionRoles(guildId) || [];
for (const rr of rrList) {
if (!rr.message_id) continue; // not posted yet
try {
const guild = client.guilds.cache.get(guildId);
if (!guild) continue;
const channel = await guild.channels.fetch(rr.channel_id || rr.channelId).catch(() => null);
if (!channel) {
// channel missing -> delete RR
await api.deleteReactionRole(guildId, rr.id);
continue;
}
const msg = await channel.messages.fetch(rr.message_id).catch(() => null);
if (!msg) {
// message missing -> delete RR
await api.deleteReactionRole(guildId, rr.id);
continue;
}
} catch (inner) {
// ignore per-item errors
}
}
} catch (e) {
// ignore guild-level errors
}
}
console.log('✅ Reaction role initial reconciliation complete');
} catch (e) {
console.error('Failed reaction role reconciliation:', e && e.message ? e.message : e);
}
// Periodic reconciliation every 10 minutes
setInterval(async () => {
try {
for (const guildId of client.guilds.cache.map(g => g.id)) {
const rrList = await api.listReactionRoles(guildId) || [];
for (const rr of rrList) {
if (!rr.message_id) continue;
try {
const guild = client.guilds.cache.get(guildId);
if (!guild) continue;
const channel = await guild.channels.fetch(rr.channel_id || rr.channelId).catch(() => null);
if (!channel) {
await api.deleteReactionRole(guildId, rr.id);
continue;
}
const msg = await channel.messages.fetch(rr.message_id).catch(() => null);
if (!msg) {
await api.deleteReactionRole(guildId, rr.id);
continue;
}
} catch (e) {
// ignore
}
}
}
} catch (e) {
// ignore
}
}, 10 * 60 * 1000);
const activities = [
{ name: 'Watch EhChad Live!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
{ name: 'Follow EhChad!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },

View File

@@ -69,6 +69,61 @@ client.on('interactionCreate', async interaction => {
return;
}
// Reaction role button handling
if (interaction.isButton && interaction.customId && interaction.customId.startsWith('rr_')) {
// customId format: rr_<reactionRoleId>_<roleId>
const parts = interaction.customId.split('_');
if (parts.length >= 3) {
const rrId = parts[1];
const roleId = parts[2];
try {
const rr = await api.safeFetchJsonPath(`/api/servers/${interaction.guildId}/reaction-roles`);
// rr is array; find by id
const found = (rr || []).find(r => String(r.id) === String(rrId));
if (!found) {
await interaction.reply({ content: 'Reaction role configuration not found.', ephemeral: true });
return;
}
const button = (found.buttons || []).find(b => String(b.roleId) === String(roleId));
if (!button) {
await interaction.reply({ content: 'Button config not found.', ephemeral: true });
return;
}
const roleId = button.roleId || button.role_id || button.role;
const member = interaction.member;
if (!member) return;
// Validate role hierarchy: bot must be higher than role, and member must be lower than role
const guild = interaction.guild;
const role = guild.roles.cache.get(roleId) || null;
if (!role) { await interaction.reply({ content: 'Configured role no longer exists.', ephemeral: true }); return; }
const botMember = await guild.members.fetchMe();
const botHighest = botMember.roles.highest;
const targetPosition = role.position || 0;
if (botHighest.position <= targetPosition) {
await interaction.reply({ content: 'Cannot assign role: bot lacks sufficient role hierarchy (move bot role higher).', ephemeral: true });
return;
}
const memberHighest = member.roles.highest;
if (memberHighest.position >= targetPosition) {
await interaction.reply({ content: 'Cannot assign role: your highest role is higher or equal to the role to be assigned.', ephemeral: true });
return;
}
const hasRole = member.roles.cache.has(roleId);
if (hasRole) {
await member.roles.remove(roleId, `Reaction role button toggled by user ${interaction.user.id}`);
await interaction.reply({ content: `Removed role ${role.name}.`, ephemeral: true });
} else {
await member.roles.add(roleId, `Reaction role button toggled by user ${interaction.user.id}`);
await interaction.reply({ content: `Assigned role ${role.name}.`, ephemeral: true });
}
} catch (e) {
console.error('Error handling reaction role button:', e);
try { await interaction.reply({ content: 'Failed to process reaction role.', ephemeral: true }); } catch(e){}
}
}
return;
}
if (!interaction.isCommand()) return;
const command = client.commands.get(interaction.commandName);
@@ -176,6 +231,50 @@ async function announceLive(guildId, stream) {
module.exports = { login, client, setGuildSettings, getGuildSettingsFromCache, announceLive };
async function postReactionRoleMessage(guildId, reactionRole) {
try {
const guild = client.guilds.cache.get(guildId);
if (!guild) return { success: false, message: 'Guild not found' };
const channel = await guild.channels.fetch(reactionRole.channel_id || reactionRole.channelId).catch(() => null);
if (!channel) return { success: false, message: 'Channel not found' };
// Build buttons
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js');
const row = new ActionRowBuilder();
const buttons = reactionRole.buttons || [];
for (let i = 0; i < buttons.length; i++) {
const b = buttons[i];
const customId = `rr_${reactionRole.id}_${b.roleId}`;
const btn = new ButtonBuilder().setCustomId(customId).setLabel(b.label || b.name || `Button ${i+1}`).setStyle(ButtonStyle.Primary);
row.addComponents(btn);
}
const embedData = reactionRole.embed || reactionRole.embed || {};
const embed = new EmbedBuilder();
if (embedData.title) embed.setTitle(embedData.title);
if (embedData.description) embed.setDescription(embedData.description);
if (embedData.color) embed.setColor(embedData.color);
if (embedData.thumbnail) embed.setThumbnail(embedData.thumbnail);
if (embedData.fields && Array.isArray(embedData.fields)) {
for (const f of embedData.fields) {
if (f.name && f.value) embed.addFields({ name: f.name, value: f.value, inline: false });
}
}
const sent = await channel.send({ embeds: [embed], components: [row] });
// update backend with message id
try {
const api = require('./api');
await api.updateReactionRole(guildId, reactionRole.id, { messageId: sent.id });
} catch (e) {
console.error('Failed to update reaction role message id in backend:', e);
}
return { success: true, messageId: sent.id };
} catch (e) {
console.error('postReactionRoleMessage failed:', e && e.message ? e.message : e);
return { success: false, message: e && e.message ? e.message : 'unknown error' };
}
}
module.exports.postReactionRoleMessage = postReactionRoleMessage;
// Start twitch watcher when client is ready (use 'clientReady' as the event name)
try {
const watcher = require('./twitch-watcher');