fix: refresh complete reaction state

This commit is contained in:
2026-08-13 18:52:47 +09:00
parent ca8afd5d73
commit 6221c763ce
3 changed files with 71 additions and 8 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"tasks": { "tasks": {
"check": "deno fmt --check && deno lint && deno check src/index.ts && deno test", "check": "deno fmt --check && deno lint && deno check src/index.ts && deno test",
"start": "deno run --allow-env=TOKEN,CLIENT_ID,CONFIG_PATH,NODE_V8_COVERAGE,UNDICI_NO_FG,JEST_WORKER_ID,SHARDS,SHARD_COUNT,SHARDING_MANAGER,SHARDING_MANAGER_MODE,DISCORD_TOKEN,WS_NO_BUFFER_UTIL,WS_NO_UTF_8_VALIDATE --allow-net=discord.com,gateway.discord.gg --allow-read=./config.json,/data/config.json --allow-write=.,/data src/index.ts" "start": "deno run --allow-env=TOKEN,CLIENT_ID,CONFIG_PATH,NODE_V8_COVERAGE,UNDICI_NO_FG,JEST_WORKER_ID,SHARDS,SHARD_COUNT,SHARDING_MANAGER,SHARDING_MANAGER_MODE,DISCORD_TOKEN,WS_NO_BUFFER_UTIL,WS_NO_UTF_8_VALIDATE --allow-net=discord.com,gateway.discord.gg --allow-read=./config.json,./config.json.tmp,/data/config.json,/data/config.json.tmp --allow-write=.,/data src/index.ts"
}, },
"compilerOptions": { "compilerOptions": {
"lib": ["deno.window"] "lib": ["deno.window"]
+2 -3
View File
@@ -1,10 +1,9 @@
import { EmbedBuilder, MessageReaction, PartialMessageReaction } from "discord.js"; import { EmbedBuilder, Message } from "discord.js";
export default function awardEmbed( export default function awardEmbed(
reaction: MessageReaction | PartialMessageReaction, message: Message,
score: number, score: number,
): EmbedBuilder { ): EmbedBuilder {
const message = reaction.message;
const reactionSummary = message.reactions.cache const reactionSummary = message.reactions.cache
.map((item) => `${item.emoji} ×${item.count}`) .map((item) => `${item.emoji} ×${item.count}`)
.join(" · "); .join(" · ");
+68 -4
View File
@@ -51,6 +51,7 @@ client.once(Events.ClientReady, (readyClient) => {
`Logged in as ${readyClient.user.tag}; serving ${readyClient.guilds.cache.size} guild(s).`, `Logged in as ${readyClient.user.tag}; serving ${readyClient.guilds.cache.size} guild(s).`,
); );
readyClient.user.setActivity("👑 awarding funny messages"); readyClient.user.setActivity("👑 awarding funny messages");
void reconcileAwards().catch((error) => console.error("Failed to reconcile awards:", error));
}); });
client.on(Events.InteractionCreate, async (interaction) => { client.on(Events.InteractionCreate, async (interaction) => {
@@ -185,9 +186,13 @@ async function updateAward(
reaction: MessageReaction | PartialMessageReaction, reaction: MessageReaction | PartialMessageReaction,
): Promise<void> { ): Promise<void> {
if (reaction.partial) await reaction.fetch(); if (reaction.partial) await reaction.fetch();
if (reaction.message.partial) await reaction.message.fetch(); // Reaction gateway events only populate the cache entry that changed. Always fetch the
// message so reactions that existed before this process started are included as well.
const message = await reaction.message.fetch(true);
await updateAwardForMessage(message);
}
const message = reaction.message; async function updateAwardForMessage(message: Message): Promise<void> {
const entry = watchedEntry(message); const entry = watchedEntry(message);
if (!message.guild || !entry) return; if (!message.guild || !entry) return;
@@ -225,7 +230,7 @@ async function updateAward(
return; return;
} }
const payload = { embeds: [awardEmbed(reaction, score)] }; const payload = { embeds: [awardEmbed(message, score)] };
if (oldAward) { if (oldAward) {
await oldAward.edit(payload); await oldAward.edit(payload);
if (entry.awards[message.id] !== oldAward.id) { if (entry.awards[message.id] !== oldAward.id) {
@@ -239,6 +244,65 @@ async function updateAward(
} }
} }
async function reconcileAwards(): Promise<void> {
let reconciled = 0;
for (const entry of entries) {
if (!entry.output_channel) continue;
const guild = await client.guilds.fetch(entry.server).catch(() => undefined);
if (!guild) continue;
const outputChannel = await guild.channels.fetch(entry.output_channel).catch(() => undefined);
if (!(outputChannel instanceof BaseGuildTextChannel)) continue;
const sourceChannels = new Map<string, string>();
const recentAwards = await outputChannel.messages.fetch({ limit: 100 });
for (const award of recentAwards.values()) {
if (award.author.id !== client.user?.id) continue;
const source = getSourceMessageLocation(award);
if (source) sourceChannels.set(source.messageId, source.channelId);
}
for (const [sourceMessageId, awardMessageId] of Object.entries(entry.awards)) {
if (sourceChannels.has(sourceMessageId)) continue;
const award = await outputChannel.messages.fetch(awardMessageId).catch(() => undefined);
if (!award) continue;
const source = getSourceMessageLocation(award);
if (source?.messageId === sourceMessageId) {
sourceChannels.set(sourceMessageId, source.channelId);
}
}
for (const [sourceMessageId, sourceChannelId] of sourceChannels) {
const sourceChannel = await guild.channels.fetch(sourceChannelId).catch(() => undefined);
if (!(sourceChannel instanceof BaseGuildTextChannel)) continue;
const sourceMessage = await sourceChannel.messages.fetch({
message: sourceMessageId,
force: true,
}).catch(() => undefined);
if (!sourceMessage) continue;
await updateAwardForMessage(sourceMessage);
reconciled++;
}
}
if (reconciled > 0) console.log(`Reconciled ${reconciled} existing award(s).`);
}
function getSourceMessageLocation(
award: Message,
): { channelId: string; messageId: string } | undefined {
for (const embed of award.embeds) {
for (const field of embed.fields) {
const match = field.value.match(
/https:\/\/discord\.com\/channels\/\d+\/(\d+)\/(\d+)/,
);
if (match) return { channelId: match[1], messageId: match[2] };
}
}
return undefined;
}
function watchedEntry(message: Message | PartialMessage): Entry | undefined { function watchedEntry(message: Message | PartialMessage): Entry | undefined {
const entry = entries.find((item) => item.server === message.guildId); const entry = entries.find((item) => item.server === message.guildId);
if (!entry?.output_channel) return undefined; if (!entry?.output_channel) return undefined;
@@ -253,7 +317,7 @@ function listAllows(type: Type, values: string[], value: string): boolean {
async function getMessageScore( async function getMessageScore(
entry: Entry, entry: Entry,
message: Message | PartialMessage, message: Message,
): Promise<number> { ): Promise<number> {
const reactionCounts = new Map<string, number>(); const reactionCounts = new Map<string, number>();
for (const item of message.reactions.cache.values()) { for (const item of message.reactions.cache.values()) {