From 161083c58f51588b0b500b2b63e6f656a682e305 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 20 Oct 2024 03:42:47 +0900 Subject: [PATCH] change message with cool embed --- config.json | 18 +++ package.json | 7 +- src/award_message.ts | 24 ++++ src/commands.ts | 83 +++++++++++ src/config.ts | 2 +- src/entry.ts | 20 ++- src/index.ts | 311 ++++++++++++++++++++++++++++++----------- src/modal/configure.ts | 42 ------ 8 files changed, 368 insertions(+), 139 deletions(-) create mode 100644 config.json create mode 100644 src/award_message.ts create mode 100644 src/commands.ts delete mode 100644 src/modal/configure.ts diff --git a/config.json b/config.json new file mode 100644 index 0000000..facf712 --- /dev/null +++ b/config.json @@ -0,0 +1,18 @@ +[ + { + "server": "1119476423309131817", + "output_hook": "https://discord.com/api/webhooks/1297208829553020999/GpWxqlPVzGMDzBLWb5E6xc9jpd2revn5VbdOr9jgb-Cj8cwmEh6wD5943wZTCQ9jF2i8", + "output_channel": "1282388288002330654", + "channel_type": "exclude", + "channels": [ + "1120639520426172447", + "1120646044594733107" + ], + "emoji_type": "exclude", + "emojis": [ + "👀", + "👍" + ], + "count": 1 + } +] \ No newline at end of file diff --git a/package.json b/package.json index 75d5eb3..22af717 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "main": "dist/index.js", "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "run": "tsx src/index.ts", + "register": "tsx src/index.ts --register" }, "type": "module", "dependencies": { @@ -15,4 +14,4 @@ "tsx": "^4.19.0", "typescript": "^5.5.4" } -} +} \ No newline at end of file diff --git a/src/award_message.ts b/src/award_message.ts new file mode 100644 index 0000000..c094a4c --- /dev/null +++ b/src/award_message.ts @@ -0,0 +1,24 @@ +import { EmbedBuilder, MessageReaction, PartialMessageReaction } from 'discord.js'; + +export default function awardEmbed(reaction: MessageReaction | PartialMessageReaction) { + function formatTime(time: Date) { + const yyyy = time.getFullYear(); + const mm = time.getMonth().toString().padStart(2, '0'); + const dd = time.getDate().toString().padStart(2, '0'); + const hh = time.getHours().toString().padStart(2, '0'); + const MM = time.getMinutes().toString().padStart(2, '0'); + return `${yyyy}-${mm}-${dd} ${hh}:${MM}`; + } + return new EmbedBuilder() + .setColor(0x777777) + .setAuthor({ + name: reaction.message.author?.displayName || "unknown", + iconURL: reaction.message.author?.avatarURL() || undefined, + }) + .setDescription(reaction.message.content || null) + .setThumbnail(reaction.message.attachments.first()?.url || null) + .addFields({ + name: `${reaction.emoji.name} x${reaction.count} `, + value: `in <#${reaction.message.channelId}> • ${formatTime(reaction.message.createdAt)}\n-# ${reaction.message.id}`, + }) +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..30a9d36 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,83 @@ +import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from '@discordjs/builders'; + +const output = new SlashCommandSubcommandBuilder() + .setName('output') + .setDescription('Set the output channel') + .addChannelOption(option => + option.setName('channel') + .setDescription('The channel to send messages to') + .setRequired(true)); +const channels = new SlashCommandSubcommandBuilder() + .setName('channels') + .setDescription('Configure channel list') + .addStringOption(option => + option.setName('add_remove') + .setDescription('add_remove') + .setRequired(true) + .addChoices( + { name: 'Add', value: 'add' }, + { name: 'Remove', value: 'remove' } + )) + .addChannelOption(option => + option.setName('channel') + .setDescription('The channel to list') + .setRequired(true)); +const channels_type = new SlashCommandSubcommandBuilder() + .setName('channels_type') + .setDescription('Set the channel list type') + .addStringOption(option => + option.setName('channels_type') + .setDescription('include or exclude') + .setRequired(true) + .addChoices( + { name: 'Include', value: 'include' }, + { name: 'Exclude', value: 'exclude' } + )); +const emojis = new SlashCommandSubcommandBuilder() + .setName('emojis') + .setDescription('Configure emoji list') + .addStringOption(option => + option.setName('add_remove') + .setDescription('add_remove') + .setRequired(true) + .addChoices( + { name: 'Add', value: 'add' }, + { name: 'Remove', value: 'remove' } + )) + .addStringOption(option => + option.setName('emoji') + .setDescription('The emoji to list') + .setRequired(true)); +const emojis_type = new SlashCommandSubcommandBuilder() + .setName('emojis_type') + .setDescription('Set the emoji list type') + .addStringOption(option => + option.setName('emojis_type') + .setDescription('include or exclude') + .setRequired(true) + .addChoices( + { name: 'Include', value: 'include' }, + { name: 'Exclude', value: 'exclude' } + )); +const count = new SlashCommandSubcommandBuilder() + .setName('count') + .setDescription('Set the number of reactions to trigger') + .addIntegerOption(option => + option.setName('count') + .setDescription('The number of reactions to trigger') + .setRequired(true)); + + +const config = new SlashCommandBuilder() + .setName('config') + .setDescription('Configure the bot') + .addSubcommand(output) + .addSubcommand(channels) + .addSubcommand(channels_type) + .addSubcommand(emojis) + .addSubcommand(emojis_type) + .addSubcommand(count); + +const commands = [config]; +export default commands; + diff --git a/src/config.ts b/src/config.ts index 8b47135..18c2834 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import { Entry } from './entry.ts'; -export function writeConfig(entries: Entry[]) { +export async function writeConfig(entries: Entry[]) { const data = JSON.stringify(entries, null, 2); fs.writeFile('./config.json', data, (err) => { if (err) { diff --git a/src/entry.ts b/src/entry.ts index cbc8802..0430014 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,12 +1,18 @@ +export enum Type { + INCLUDE = "include", + EXCLUDE = "exclude" +} + export class Entry { server: string; - channel: string; - emoji: string; - count: number; - constructor(server: string, channel: string, emoji: string, count: number) { + output_channel: string = ""; + output_hook: string = ""; + channel_type: Type = Type.EXCLUDE; + channels: string[] = []; + emoji_type: Type = Type.EXCLUDE; + emojis: string[] = []; + count: number = 3; + constructor(server: string) { this.server = server; - this.channel = channel; - this.emoji = emoji; - this.count = count; } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7e6fdf5..2c00684 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,24 @@ +import 'dotenv/config' +const TOKEN = process.env.TOKEN; +const CLIENT_ID = process.env.CLIENT_ID; +if (!TOKEN || !CLIENT_ID) { + console.error(`No ${TOKEN ? "CLIENT_ID" : "TOKEN"} provided`); + process.exit(); +} +import commands from "./commands.ts"; + +if (process.argv.includes("--register")) { + 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); + } + process.exit(); +} + import { REST, Routes, @@ -5,99 +26,170 @@ import { Partials, GatewayIntentBits, Events, + TextChannel, + Webhook, + WebhookMessageCreateOptions, + WebhookClient, } from "discord.js"; - import { writeConfig, readConfig } from "./config.ts"; - -import 'dotenv/config' -const TOKEN = process.env.TOKEN; -const CLIENT_ID = process.env.CLIENT_ID; -if (!TOKEN) { - console.error("No token provided"); - process.exit(); -} -if (!CLIENT_ID) { - console.error("No client ID provided"); - process.exit(); -} - -const rest = new REST({ version: "10" }).setToken(TOKEN); -try { - console.log("refreshing slash commands..."); - await rest.put(Routes.applicationCommands(CLIENT_ID), { - body: [ - { - name: "config", - description: "Replies with the channel ID", - }, - ], - }); - console.log("OK"); -} catch (error) { - console.error(error); -} - 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) return; + if (!client.user) { + console.error("Failed to login"); + process.exit(); + } + console.log(`Logged in as ${client.user.tag}!`); + client.user.setActivity("for reactions"); }); -import { Entry } from "./entry.ts"; +import { Entry, Type } from "./entry.ts"; const entries: Entry[] = readConfig(); +writeConfig(entries); -import modal from "./modal/configure.ts"; client.on("interactionCreate", async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (interaction.isChatInputCommand()) { + if (interaction.commandName === "config") { + 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_hook = (await registWebhook(channel as TextChannel)); + entry.output_channel = channel.id; + writeConfig(entries); + } catch (error) { + console.error(error); + } finally { + 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); + } finally { + 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 }); - if (interaction.commandName === "config") { - await interaction.showModal(modal); - } -}); -client.on(Events.InteractionCreate, async (interaction) => { - if (!interaction.isModalSubmit()) return; - if (interaction.customId === "config") { - const emoji = interaction.fields.getTextInputValue("emoji"); - const count = parseInt(interaction.fields.getTextInputValue("count")); - const channelId = interaction.fields.getTextInputValue("channelId"); - const serverId = interaction.guildId; - if (!emoji || !channelId) { - await interaction.reply({ - content: "Please fill out all fields", - ephemeral: true, - }); - return; + let entry = entries.find((entry) => entry.server === interaction.guildId); + if (!entry) { + entry = new Entry(interaction.guildId!); + entries.push(entry); + } + entry.channel_type = channel_type as Type; + writeConfig(entries); + } catch (error) { + console.error(error); + } finally { + 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); + } finally { + 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); + } + entry.emoji_type = emoji_type as Type; + writeConfig(entries); + } catch (error) { + console.error(error); + } finally { + await interaction.followUp({ + content: `emoji list type set to ${emoji_type}`, + ephemeral: true, + }); + } + } else if (subcommand === "count") { + const count = interaction.options.getInteger("count", 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.count = count; + await writeConfig(entries); + } catch (error) { + console.error(error); + } finally { + await interaction.followUp({ + content: `The number needed to trigger is now ${count}`, + ephemeral: true, + }); + } + } } - if (serverId === null) { - return; - } - - console.log(`Set entry: ${serverId} -> ${channelId} (${emoji})`); - const entry = entries.find((entry) => entry.server === serverId); - if (entry) { - entry.channel = channelId; - entry.emoji = emoji; - entry.count = count; - } else { - entries.push(new Entry(serverId, channelId, emoji, count)); - } - writeConfig(entries); - - await interaction.reply({ - content: `Set to send messages to <#${channelId}>, watching for ${emoji}`, - ephemeral: true, - }); } }); +import awardEmbed from "./award_message.ts"; client.on(Events.MessageReactionAdd, async (reaction, user) => { if (reaction.partial) { try { @@ -109,25 +201,74 @@ client.on(Events.MessageReactionAdd, async (reaction, user) => { } if (!reaction.message.guild) return; - const entry = entries.find( - (entry) => entry.server === reaction.message.guildId - ); - if ( - !entry || - reaction.emoji.name !== entry.emoji || - reaction.count === null || - reaction.count < entry.count - ) - 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.name}`)) return; - const channel = await client.channels.fetch(entry.channel); - if (channel === null || !channel.isTextBased() || channel.isDMBased()) return; + console.log(`Reaction added: ${reaction.emoji.name} in ${reaction.message.guild.channels.cache.get(reaction.message.channelId)?.name}`); + + if (reaction.count === null || reaction.count < entry.count) return; if (reaction.count === entry.count) { - await channel.send( - `🎉 ${reaction.count} people have reacted with ${entry.emoji}! 🎉` - ); + const attachments = reaction.message.attachments.filter((attachment) => !(!attachment.height && !attachment.width)) + + const channel = reaction.message.guild.channels.cache.get(entry.output_channel) as TextChannel; + if (!channel) return; + channel.send({ + embeds: [awardEmbed(reaction)] + }); + // sendWebhookMessage(entry.output_hook, { + // username: `${reaction.message.author?.displayName}`, + // avatarURL: reaction.message.author?.avatarURL() || "", + // content: reaction.message.content?.replace(/<@!?(\d+)>/g, (match) => { + // const id = match.match(/\d+/)?.[0]; + // if (!id) return match; + // const member = reaction.message.guild?.members.cache.get(id); + // return `**@ ${member?.displayName}**` || match; + // }) || "", + // files: attachments.map((attachment) => attachment.url), + // embeds: [awardEmbed(reaction)], + // }); + } else if (reaction.count > entry.count) { + } }); client.login(TOKEN); + +// async function registWebhook(channel: TextChannel): Promise { +// const existing = entries.find((entry) => entry.server === channel.guildId)?.output_hook; +// if (existing) { +// try { +// const webhook = new WebhookClient({ url: existing }); +// webhook.edit({ +// name: 'Reaction Award Bot', +// channel: channel.id, +// }); +// return webhook.url; +// } catch (error) { +// console.error("failed to edit existing webhook"); +// } +// } else { +// try { +// const webhook = await channel.createWebhook({ +// name: 'Reaction Award Bot', +// }) +// return webhook.url +// } catch (error) { +// console.error("failed to create webhook"); +// } +// } +// return ""; +// } + +// async function sendWebhookMessage(url: string, options: WebhookMessageCreateOptions) { +// try { +// const webhook = new WebhookClient({ url }); +// if (!webhook) return; +// await webhook.send(options); +// } catch (error) { +// console.error(error); +// } +// } \ No newline at end of file diff --git a/src/modal/configure.ts b/src/modal/configure.ts deleted file mode 100644 index 2506861..0000000 --- a/src/modal/configure.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - ModalBuilder, - TextInputBuilder, - ActionRowBuilder, - TextInputStyle, - ModalActionRowComponentBuilder, -} from 'discord.js'; - -const modal = new ModalBuilder() - .setCustomId('config') - .setTitle('Configuration'); - -const emojiInput = new TextInputBuilder() - .setCustomId('emoji') - .setLabel('Emoji') - .setStyle(TextInputStyle.Short) - .setPlaceholder('Enter the emoji') - .setValue('⭐') - .setRequired(true); - -const countInput = new TextInputBuilder() - .setCustomId('count') - .setLabel('Count') - .setStyle(TextInputStyle.Short) - .setPlaceholder('Enter the count') - .setValue('5') - .setRequired(true); - -const channelIdInput = new TextInputBuilder() - .setCustomId('channelId') - .setLabel('Channel ID') - .setStyle(TextInputStyle.Short) - .setPlaceholder('Enter the channel ID') - .setRequired(true); - -const first = new ActionRowBuilder().addComponents(emojiInput) -const second = new ActionRowBuilder().addComponents(countInput) -const third = new ActionRowBuilder().addComponents(channelIdInput) - -modal.addComponents(first, second, third); - -export default modal; \ No newline at end of file