diff --git a/src/events/message/messageCreate.ts b/src/events/message/messageCreate.ts new file mode 100644 index 0000000..23d9954 --- /dev/null +++ b/src/events/message/messageCreate.ts @@ -0,0 +1,175 @@ +import { Client, GuildMember, Message, MessageEmbed, TextBasedChannel } from 'discord.js'; +import { getLocale } from '../../utils/locales'; +import { isImage, userWithNickname } from '../../utils/misc'; +import { showDate } from '../../utils/time'; + +/** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-messageCreate */ +export default async (message: Message, client: Client) => { + // Ignore message if + if ( + // Author is a bot + message.author.bot || + // Author is Discord + message.author.system || + // Message isn't a message + message.system || + // Message is in PM (future-proof if we add Intents.FLAGS.DIRECT_MESSAGES) + !message.guild || + // Guild is offline + !message.guild.available + ) { + return; + } + + /* Citation */ + const regex = 'https://(?:canary\\.|ptb\\.)?discord(?:app)?\\.com/channels/(\\d{17,19})/(\\d{17,19})/(\\d{17,19})'; + const urls = message.content.match(new RegExp(regex, 'g')); + + // Ignore message if there is no URLs + if (!urls) { + return; + } + + const messages = ( + await Promise.all( + urls.reduce( + (data: { + message_id: string; + channel: TextBasedChannel; + }[] = [], match) => { + const [, + guild_id, + channel_id, + message_id, + ] = new RegExp(regex).exec(match) as RegExpExecArray; + + // If message posted in another guild + if (guild_id !== message.guild?.id) { + return data; + } + + const channel = + message.guild.channels.cache.get(channel_id) as TextBasedChannel; + + // If channel doesn't exist in the guild and isn't text + if (!channel) { + return data; + } + + data.push({ message_id, channel }); + + return data; + }, [] + ).map(async ({ message_id, channel }) => { + const quoted_message = await channel.messages + .fetch(message_id) + .catch(() => undefined); + + // If message doesn't exist or empty + if (!quoted_message || ( + !quoted_message.content && + quoted_message.attachments.size == 0) + ) { + return; + } + + return quoted_message; + }) + ) + // Remove undefined elements + ).filter(Boolean); + + const loc = getLocale(client, client.config.default_lang); + + // Remove duplicates then map the quoted posts + [...new Set(messages)].map(quoted_post => { + const embed = new MessageEmbed() + .setColor('#2f3136') + .setAuthor({ + name: 'Citation', + iconURL: quoted_post?.author.displayAvatarURL(), + }); + + // Handle attachments + if (quoted_post?.attachments.size !== 0) { + if (quoted_post?.attachments.size === 1 && isImage( + quoted_post.attachments.first()?.name as string + )) { + // Only contains one image + embed.setImage(quoted_post.attachments.first()?.url as string); + } else { + // Contains more than one image and/or other files + let files = ''; + quoted_post?.attachments.forEach(file => files += + `[${file.name}](${file.url}), ` + ); + embed.addFields({ + // TODO: Don't pluralize when there is only one file. + name: 'Fichiers joints', + // TODO: Check if don't exceed char limit, if yes, split + // files into multiples field. + value: `${files.slice(0, -2)}.`, + }); + } + } + + // Description as post content + embed.setDescription(quoted_post?.content ?? ''); + + // Footer + let footer = `Posté le ${showDate( + client.config.default_lang, + loc, + quoted_post?.createdAt as Date + )}`; + if (quoted_post?.editedAt) { + footer += ` et modifié le ${showDate( + client.config.default_lang, + loc, + quoted_post.editedAt + )}`; + } + + let author = 'Auteur'; + if (message.author == quoted_post?.author) { + author += ' & Citateur'; + } else { + footer += `\nCité par ${userWithNickname( + message.member as GuildMember + ) ?? '?'} le ${showDate( + client.config.default_lang, + loc, + message.createdAt + )}`; + } + + embed.setFooter({ + text: footer, + iconURL: message.author.avatarURL() ?? undefined, + }); + + // Location/author of the quoted post + embed.addField(author, `${quoted_post?.author}`, true); + embed.addField( + 'Message', `${quoted_post?.channel} - [Lien Message](${quoted_post?.url})`, + true + ); + + // Delete source message if no content when removing links + if ( + !message.content.replace(new RegExp(regex, 'g'), '').trim() && + messages.length === urls.length && + !message.mentions.repliedUser + ) { + message.delete(); + return message.channel.send({ embeds: [embed] }); + } else { + return message.reply({ + embeds: [embed], + allowedMentions: { + repliedUser: false, + }, + }); + } + }); +}; diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 6f0439a..2ed735a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -12,5 +12,7 @@ "c_help_opt1_desc": "Command wanted in depth.", "c_help1": "List of categories and associated commands", "c_help2": "`/help ` to get more information about a command.", - "c_help3": "Can't find :" + "c_help3": "Can't find :", + + "u_time_at": "at" } diff --git a/src/locales/fr.json b/src/locales/fr.json index cad1fba..90fddc6 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -12,5 +12,7 @@ "c_help_opt1_desc": "Commande voulu en détail.", "c_help1": "Liste des catégories et des commandes associées", "c_help2": "`/help ` pour obtenir plus d'informations sur une commande.", - "c_help3": "Impossible de trouver :" + "c_help3": "Impossible de trouver :", + + "u_time_at": "à" } diff --git a/src/modules/client.ts b/src/modules/client.ts new file mode 100644 index 0000000..8d0aa0e --- /dev/null +++ b/src/modules/client.ts @@ -0,0 +1,41 @@ +import { Collection } from 'discord.js'; +import { SlashCommandBuilder } from '@discordjs/builders'; + +export {}; + +declare module 'discord.js' { + // eslint-disable-next-line no-shadow + export interface Client { + /** Store the configuration */ + config: { + /** Bot version */ + version: string, + /** Bot token from env variable */ + token_discord: string | undefined, + /** Default lang used */ + default_lang: string + }, + /** Store all the slash commands */ + commands: { + categories: Collection< + /** Category name */ + string, + /** Name of the commands in the category */ + string[] + >, + list: Collection< + /** Command name */ + string, + /** Command itself */ + { + /** Data about the command */ + data: SlashCommandBuilder, + /** How the command interact */ + interaction: (interaction: CommandInteraction, client: Client) => unknown + } + >, + } + /** Store all the localizations */ + locales: Map> + } +} diff --git a/src/utils/client.ts b/src/utils/client.ts index 2176192..182ebf1 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -1,50 +1,14 @@ import { Client, Collection, Intents } from 'discord.js'; import { readFileSync } from 'fs'; -import { SlashCommandBuilder } from '@discordjs/builders'; import { loadLocales } from './locales'; - -declare module 'discord.js' { - // eslint-disable-next-line no-shadow - export interface Client { - /** Store the configuration */ - config: { - /** Bot version */ - version: string, - /** Bot token from env variable */ - token_discord: string | undefined, - /** Default lang used */ - default_lang: string - }, - /** Store all the slash commands */ - commands: { - categories: Collection< - /** Category name */ - string, - /** Name of the commands in the category */ - string[] - >, - list: Collection< - /** Command name */ - string, - /** Command itself */ - { - /** Data about the command */ - data: SlashCommandBuilder, - /** How the command interact */ - interaction: (interaction: CommandInteraction, client: Client) => unknown - } - >, - } - /** Store all the localizations */ - locales: Map> - } -} +import '../modules/client'; /** Creation of the client and definition of its properties. */ export default async () => { const client: Client = new Client({ intents: [ Intents.FLAGS.GUILDS, + Intents.FLAGS.GUILD_MESSAGES, ], }); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 0f76dd4..0393cc0 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,3 +1,5 @@ +import { GuildMember } from 'discord.js'; + /** * Log module status. * @param {string} name Module name @@ -36,3 +38,41 @@ export const removeExtension = (filename: string) => { return array.join('.'); }; + +/** + * Get extension from a filename. + * @param filename string of the filename + * @returns string of the extension if it exists + */ +export const getExtension = (filename: string) => { + const array = filename.split('.'); + + return array.pop(); +}; + +/** + * Define if a media is a media based on file extension. + * @param filename string of the filename + * @returns true is file is a media + */ +export const isImage = (filename: string) => { + return Boolean(getExtension(filename)?.match( + /jpg|jpeg|png|webp|gif/ + )); +}; + +/** + * String with pseudo and nickname if available. + * @param member Member + * @returns string + */ +export const userWithNickname = (member: GuildMember) => { + if (!member) { + return undefined; + } + if (member.nickname) { + return `${member.nickname} (${member.user.tag})`; + } else { + return member.user.tag; + } +}; diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..b44614e --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,16 @@ +/** + * Parsed string adapted with TZ (locales) and format for the specified lang. + * @param tz Lang + * @param locale Locales + * @param date Date + * @returns String + */ +export const showDate = ( + tz: string, + locale: Map, + date: Date +) => { + return date.toLocaleString(tz).replace(' ', ` ${ + locale.get('u_time_at') + } `); +};