import { DiscordHTTPError, DiscordRESTError, type Client } from 'oceanic.js' import { reactionKey } from './emoji.ts' import { deletePanel, panels, setReactor, type RolePanel } from './store.ts' /** Grants or revokes a panel's role to match one user's reaction, recording * the outcome so a later sync knows it was applied. Resolves false on failure. */ export async function applyReaction( client: Client, messageID: string, panel: RolePanel, userID: string, reacted: boolean ): Promise { const { guildID, roleID } = panel try { if (reacted) await client.rest.guilds.addMemberRole(guildID, userID, roleID) else await client.rest.guilds.removeMemberRole(guildID, userID, roleID) } catch (error) { const verb = reacted ? 'grant' : 'revoke' console.error(`panel ${messageID}: could not ${verb} role ${roleID} for ${userID}:`, error) return false } setReactor(panel, userID, reacted) return true } /** Diffs every panel's current reactions against its tracked reactors and * applies the grants and revocations that happened while the bot was down. */ export async function syncPanels(client: Client): Promise { for (const [messageID, panel] of panels) { try { // read the message's own reaction list so the emoji we query with // matches discord's stored form exactly (variation selectors vary) const message = await client.rest.channels.getMessage(panel.channelID, messageID) const match = message.reactions.find((r) => reactionKey(r.emoji) === panel.emoji) if (match === undefined || match.emoji.name === null) continue const emoji = match.emoji.id === null ? match.emoji.name : `${match.emoji.name}:${match.emoji.id}` const current = new Set( ( await client.rest.channels.getReactions(panel.channelID, messageID, emoji, { limit: Infinity }) ) .filter((user) => !user.bot) .map((user) => user.id) ) let granted = 0 let revoked = 0 for (const userID of current) { if (panel.reactors.has(userID)) continue if (await applyReaction(client, messageID, panel, userID, true)) granted++ } // snapshot: applyReaction removes from panel.reactors as we go for (const userID of [...panel.reactors]) { if (current.has(userID)) continue if (await applyReaction(client, messageID, panel, userID, false)) revoked++ } console.log(`panel ${messageID}: sync complete, granted ${granted}, revoked ${revoked}`) } catch (error) { if ( !(error instanceof DiscordHTTPError || error instanceof DiscordRESTError) || error.status !== 404 ) { console.error(`panel ${messageID}: startup sync failed:`, error) continue } // discord returns 404 for deleted messages and for channels the bot can // no longer see; only unregister once we know the channel still exists const channel = await client.rest.channels.get(panel.channelID).catch(() => undefined) if (channel !== undefined) { deletePanel(messageID) console.log(`panel ${messageID}: message no longer exists, unregistered`) } else { console.warn(`panel ${messageID}: message or channel inaccessible, keeping panel`) } } } }