feat: quote (#42)
Reviewed-on: https://git.kennel.ml/ConfrerieDuKassoulait/Botanique/pulls/42
This commit is contained in:
parent
06f19c2f84
commit
f6ff50449c
7 changed files with 280 additions and 40 deletions
175
src/events/message/messageCreate.ts
Normal file
175
src/events/message/messageCreate.ts
Normal file
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -12,5 +12,7 @@
|
||||||
"c_help_opt1_desc": "Command wanted in depth.",
|
"c_help_opt1_desc": "Command wanted in depth.",
|
||||||
"c_help1": "List of categories and associated commands",
|
"c_help1": "List of categories and associated commands",
|
||||||
"c_help2": "`/help <command>` to get more information about a command.",
|
"c_help2": "`/help <command>` to get more information about a command.",
|
||||||
"c_help3": "Can't find :"
|
"c_help3": "Can't find :",
|
||||||
|
|
||||||
|
"u_time_at": "at"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,7 @@
|
||||||
"c_help_opt1_desc": "Commande voulu en détail.",
|
"c_help_opt1_desc": "Commande voulu en détail.",
|
||||||
"c_help1": "Liste des catégories et des commandes associées",
|
"c_help1": "Liste des catégories et des commandes associées",
|
||||||
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
|
"c_help2": "`/help <commande>` pour obtenir plus d'informations sur une commande.",
|
||||||
"c_help3": "Impossible de trouver :"
|
"c_help3": "Impossible de trouver :",
|
||||||
|
|
||||||
|
"u_time_at": "à"
|
||||||
}
|
}
|
||||||
|
|
41
src/modules/client.ts
Normal file
41
src/modules/client.ts
Normal file
|
@ -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<string, Map<string, string>>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,50 +1,14 @@
|
||||||
import { Client, Collection, Intents } from 'discord.js';
|
import { Client, Collection, Intents } from 'discord.js';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
|
||||||
import { loadLocales } from './locales';
|
import { loadLocales } from './locales';
|
||||||
|
import '../modules/client';
|
||||||
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<string, Map<string, string>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creation of the client and definition of its properties. */
|
/** Creation of the client and definition of its properties. */
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const client: Client = new Client({
|
const client: Client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
Intents.FLAGS.GUILDS,
|
Intents.FLAGS.GUILDS,
|
||||||
|
Intents.FLAGS.GUILD_MESSAGES,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { GuildMember } from 'discord.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log module status.
|
* Log module status.
|
||||||
* @param {string} name Module name
|
* @param {string} name Module name
|
||||||
|
@ -36,3 +38,41 @@ export const removeExtension = (filename: string) => {
|
||||||
|
|
||||||
return array.join('.');
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
16
src/utils/time.ts
Normal file
16
src/utils/time.ts
Normal file
|
@ -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<string, unknown>,
|
||||||
|
date: Date
|
||||||
|
) => {
|
||||||
|
return date.toLocaleString(tz).replace(' ', ` ${
|
||||||
|
locale.get('u_time_at')
|
||||||
|
} `);
|
||||||
|
};
|
Loading…
Reference in a new issue