const TOKEN = Deno.env.get("TOKEN"); const CLIENT_ID = Deno.env.get("CLIENT_ID"); if (!TOKEN || !CLIENT_ID) { console.error(`No ${TOKEN ? "CLIENT_ID" : "TOKEN"} provided`); Deno.exit(1); } import commands from "./commands.ts"; import { REST, Routes, Client, Partials, GatewayIntentBits, Events, TextChannel, Message, PartialMessage, } from "discord.js"; const rest = new REST({ version: "10" }).setToken(TOKEN); try { console.log("refreshing slash commands..."); await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands }); console.log("OK"); } catch (error) { console.error(error); } import { writeConfig, readConfig } from "./config.ts"; const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.MessageContent, ], partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); client.on("ready", () => { if (!client.user) { console.error("Failed to login"); Deno.exit(1); } console.log(`Logged in as ${client.user.tag}!`); client.user.setActivity("👑let's award funny messages!"); }); import { Entry, Type } from "./entry.ts"; const entries: Entry[] = readConfig(); writeConfig(entries); client.on("interactionCreate", async (interaction) => { if (interaction.isChatInputCommand()) { if (interaction.commandName === "config") { if (interaction.user.id !== interaction.guild?.ownerId) { await interaction.reply({ content: "Only the server owner can configure the bot", ephemeral: true, }); return; } const subcommand = interaction.options.getSubcommand(true); if (subcommand === "output") { const channel = interaction.options.getChannel("channel", true); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } entry.output_channel = channel.id; writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to set output channel. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `Set output to <#${channel.id}>`, }); } else if (subcommand === "channels") { const channel = interaction.options.getChannel("channel", true); const mode = interaction.options.getString("add_remove", true); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } if (mode === "add") entry.channels.push(channel.id); else entry.channels = entry.channels.filter((c) => c !== channel.id); writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to configure channel list. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `Set to watch for reactions in <#${channel.id}>`, }); } else if (subcommand === "channels_type") { const channel_type = interaction.options.getString( "channels_type", true ); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } if (channel_type !== "include" && channel_type !== "exclude") { throw new Error(`Invalid channel type: ${channel_type}`); } entry.channel_type = channel_type as Type; writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to set channel list type. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `channel list type set to ${channel_type}`, ephemeral: true, }); } else if (subcommand === "emojis") { const emoji = interaction.options.getString("emoji", true); const mode = interaction.options.getString("add_remove", true); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } if (mode === "add") entry.emojis.push(emoji); else entry.emojis = entry.emojis.filter((e) => e !== emoji); await writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to configure emoji list. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `Set to watch for ${emoji}`, ephemeral: true, }); } else if (subcommand === "emojis_type") { const emoji_type = interaction.options.getString("emojis_type", true); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } if (emoji_type !== "include" && emoji_type !== "exclude") { throw new Error(`Invalid emoji type: ${emoji_type}`); } entry.emoji_type = emoji_type as Type; writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to set emoji list type. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `emoji list type set to ${emoji_type}`, ephemeral: true, }); } else if (subcommand === "score") { const score = interaction.options.getInteger("score", true); try { await interaction.deferReply({ ephemeral: true }); let entry = entries.find( (entry) => entry.server === interaction.guildId ); if (!entry) { entry = new Entry(interaction.guildId!); entries.push(entry); } entry.score = score; await writeConfig(entries); } catch (error) { console.error(error); await interaction.followUp({ content: "Failed to set score threshold. Please try again.", ephemeral: true, }); return; } await interaction.followUp({ content: `The score needed to trigger is now ${score}`, ephemeral: true, }); } } } }); import awardEmbed from "./award_message.ts"; async function handleReactionFetch(reaction: any) { if (reaction.partial) { try { await reaction.fetch(); } catch (error) { console.error("Something went wrong when fetching the message:", error); return false; } } return true; } client.on(Events.MessageReactionAdd, async (reaction, user) => { if (!(await handleReactionFetch(reaction))) return; if (!reaction.message.guild) return; const entry = entries.find( (entry) => entry.server === reaction.message.guildId ); if (entry === undefined) return; if ( (entry.channel_type === Type.EXCLUDE) === entry.channels.includes(reaction.message.channelId) ) return; if ( (entry.emoji_type === Type.EXCLUDE) === entry.emojis.includes(`${reaction.emoji.id || reaction.emoji.name}`) ) return; console.log(`reaction added: ${reaction.emoji.id || reaction.emoji.name}`); const totalScore = await getMessageScore(entry, reaction.message); if (totalScore >= entry.score) { const channel = reaction.message.guild.channels.cache.get( entry.output_channel ) as TextChannel; if (!channel) { console.error(`Output channel ${entry.output_channel} not found`); return; } if (!channel.isTextBased()) { console.error(`Channel ${entry.output_channel} is not a text channel`); return; } channel.messages.fetch({ limit: 10 }).then((messages) => { messages.find((message) => { if (message.embeds[0]?.fields[0].value.includes(reaction.message.id)) { message.delete().catch((error) => { console.error("Failed to delete old message:", error); }); console.log("old message deleted."); return true; } return false; }); channel.send({ embeds: [awardEmbed(reaction, totalScore)], }).catch((error) => { console.error("Failed to send award message:", error); }); console.log("message sent."); }).catch((error) => { console.error("Failed to fetch messages:", error); }); } }); client.on(Events.MessageReactionRemove, async (reaction, user) => { if (!(await handleReactionFetch(reaction))) return; if (!reaction.message.guild) return; const entry = entries.find( (entry) => entry.server === reaction.message.guildId ); if (entry === undefined) return; if ( (entry.channel_type === Type.EXCLUDE) === entry.channels.includes(reaction.message.channelId) ) return; if ( (entry.emoji_type === Type.EXCLUDE) === entry.emojis.includes(`${reaction.emoji.id || reaction.emoji.name}`) ) return; console.log(`reaction removed: ${reaction.emoji.id || reaction.emoji.name}`); const totalScore = await getMessageScore(entry, reaction.message); const channel = reaction.message.guild.channels.cache.get( entry.output_channel ) as TextChannel; if (!channel) { console.error(`Output channel ${entry.output_channel} not found`); return; } if (!channel.isTextBased()) { console.error(`Channel ${entry.output_channel} is not a text channel`); return; } channel.messages.fetch({ limit: 10 }).then((messages) => { messages.find((message) => { if (message.embeds[0]?.fields[0].value.includes(reaction.message.id)) { message.delete().catch((error) => { console.error("Failed to delete old message:", error); }); console.log("old message deleted."); if (totalScore >= entry.score) { channel.send({ embeds: [awardEmbed(reaction, totalScore)], }).catch((error) => { console.error("Failed to send award message:", error); }); console.log("message sent."); } return true; } return false; }); }).catch((error) => { console.error("Failed to fetch messages:", error); }); }); client.login(TOKEN).catch((error) => { console.error("Failed to login to Discord:", error); Deno.exit(1); }); async function getMessageScore( entry: Entry, message: Message | PartialMessage ): Promise { /* Message Score Calculation * * Per user: 1 / (-1 + 2x) */ const allReactions: Map = new Map(); const reactions = message.reactions.cache; for (const r of reactions.values()) { if ( (entry.emoji_type === Type.EXCLUDE) === entry.emojis.includes(`${r.emoji.id || r.emoji.name}`) ) continue; const users = await r.users.fetch(); for (const user of users.values()) { if (user.bot) continue; const score = allReactions.get(user.id) || 0; allReactions.set(user.id, score + 1 / (-1 + 2 * (score + 1))); } } let totalScore = 0; allReactions.forEach((value) => { totalScore += value; }); // "message" by "user" // id : score // ---------------- // Total: 1.5 console.log(`"${message.id}" by "${message.author?.username}"`); allReactions.forEach((value, key) => { console.log(`${key} : ${value}`); }); console.log(`----------------`); console.log(`Total: ${totalScore}`); return totalScore; }