feat: quote #42

Merged
Anri merged 16 commits from feat/citation into main 2022-07-27 13:00:24 +02:00
7 changed files with 280 additions and 40 deletions

View 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,
},
});
}
});
};

View file

@ -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"
} }

View file

@ -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
View 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>>
}
}

View file

@ -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,
], ],
}); });

View file

@ -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
View 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')
} `);
};