bug fixes
This commit is contained in:
@@ -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 };
|
||||
|
||||
21
discord-bot/commands/post-reaction-role.js
Normal file
21
discord-bot/commands/post-reaction-role.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user