Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
MAX_LINES=20
BIN_URL=
REQUEST_TIMEOUT=5000
BIN_URL=https://bin.readthedocs.fr
DELETE_MIN_REACTIONS=5
DISCORD_TOKEN=
CATEGORIES=
REQUEST_TIMEOUT=5000
BINS_TOKEN=
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,23 @@
},
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^15.0.1",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"dotenv": "^8.2.0",
"eslint": "^7.25.0",
"@types/node": "^15.0.2",
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"dotenv": "^9.0.0",
"eslint": "^7.26.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-node": "^0.3.4",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-json": "^3.0.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"jest": "^26.6.3",
"nodemon": "^2.0.7",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.5",
"ts-jest": "^26.5.6",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},
Expand Down
17 changes: 9 additions & 8 deletions src/events/message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { createBin, processContent, request, sendBinEmbed } from "../../helpers"
import { extensions } from "../../misc";

const MAX_LINES = parseInt(process.env.MAX_LINES!, 10);
const ORIGIN_URL = new URL(process.env.BIN_URL!).origin;
const HEALTH_URL = new URL("/health", process.env.BIN_URL);
const REQUEST_TIMEOUT = parseInt(process.env.REQUEST_TIMEOUT!, 10) || 5000;
const CATEGORIES = process.env.CATEGORIES!.split(",");

const noop = (): undefined => undefined;

Expand All @@ -18,8 +19,6 @@ export default class MessageEvent extends Event {
}

public async listener(message: Message): Promise<void> {
const categories = process.env.CATEGORIES!.split(",");

if (!(message.channel instanceof GuildChannel) || message.author.bot) {
return;
}
Expand All @@ -37,7 +36,7 @@ export default class MessageEvent extends Event {
return;
}

const binHealth = await request.head(`${ORIGIN_URL}/health`).catch(noop);
const binHealth = await request.head(HEALTH_URL).catch(noop);

const embed = new MessageEmbed()
.setColor(binHealth ? 0x2ab533 : 0xf33030)
Expand All @@ -50,7 +49,7 @@ export default class MessageEvent extends Event {
return;
}

if (!categories.includes(message.channel.parentID!)) {
if (!CATEGORIES.includes(message.channel.parentID!)) {
return;
}

Expand Down Expand Up @@ -80,15 +79,16 @@ export default class MessageEvent extends Event {

sendBinEmbed(
message,
processed || message.content,
processed?.[0] ?? message.content,
processed?.[1],
content ? (embed): MessageEmbed => embed.addField("📁 Pièce jointe", content) : undefined,
message.attachments.size > 0 ? message.attachments : undefined,
);
} catch (error) {
const errorEmbed = new MessageEmbed({ title: error.toString() });
errorEmbed.setDescription(
// eslint-disable-next-line max-len
`Cependant, bien que votre message n'ait pas été effacé, il a été jugé trop "lourd" pour être lu (code trop long, fichier texte présent).\n\nNous vous conseillons l'usage d'un service de bin pour les gros morceaux de code, tel ${ORIGIN_URL} (s'il est hors-ligne, utilisez d'autres alternatives comme https://paste.artemix.org/).`,
`Cependant, bien que votre message n'ait pas été effacé, il a été jugé trop "lourd" pour être lu (code trop long, fichier texte présent).\n\nNous vous conseillons l'usage d'un service de bin pour les gros morceaux de code, tel ${HEALTH_URL.origin} (s'il est hors-ligne, utilisez d'autres alternatives comme https://paste.artemix.org/).`,
);
message.channel.send(errorEmbed).catch(noop);
}
Expand All @@ -107,7 +107,8 @@ export default class MessageEvent extends Event {
if (processed) {
sendBinEmbed(
message,
processed,
processed[0],
processed[1],
undefined,
message.attachments.size > 0 ? message.attachments : undefined,
).catch(noop);
Expand Down
15 changes: 9 additions & 6 deletions src/helpers/__tests__/contentProcessing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const consoleError = console.error;

describe(processContent, () => {
it("should replace the code with undefined when an error occurs since there are no changes", async () => {
process.env.BIN_URL = "https://binn.readthedocs.fr/new";
process.env.BIN_URL = "https://binn.readthedocs.fr/";
console.error = jest.fn();

expect(await processContent("see : `this\nis\nmultiline !`", MAX_LINES)).toBeUndefined();
Expand All @@ -15,7 +15,7 @@ describe(processContent, () => {

beforeEach(() => {
jest.resetModules();
process.env.BIN_URL = "http://localhost:8012/new";
process.env.BIN_URL = "http://localhost:8012/";
console.error = consoleError;
});

Expand All @@ -25,25 +25,28 @@ describe(processContent, () => {
`see : \`\`\`js\nthis\nis\nmulti\nline !\`\`\` \`\`\`${"and this is big\n".repeat(4097)}\`\`\``,
MAX_LINES,
),
).toEqual(expect.stringMatching(`see : ${binUrl("js")} \\[Err(eu|o)r( [0-9]+ )?: .+\\.?\\]`));
).toStrictEqual([
expect.stringMatching(`see : ${binUrl("js")} \\[Err(eu|o)r( [0-9]+ )?: .+\\.?\\]`),
expect.stringMatching(binUrl("js")),
]);
});

it("should return undefined if there are no changes", async () => {
expect(await processContent("no changes", MAX_LINES)).toBeUndefined();
});

it("should replace duplicated codes by the same bin url", async () => {
const results = (await processContent("`\na\nb\nc` et `\na\nb\nc`", MAX_LINES))?.split(" et ", 2);
const results = (await processContent("`\na\nb\nc` et `\na\nb\nc`", MAX_LINES))?.[0].split(" et ", 2);
expect(results).not.toBeUndefined();
expect(results?.[0]).toEqual(results?.[1]);

const results2 = (await processContent("```js\na\nb\nc``` ```js\na\nb\nc```", MAX_LINES))?.split(" ", 2);
const results2 = (await processContent("```js\na\nb\nc``` ```js\na\nb\nc```", MAX_LINES))?.[0].split(" ", 2);
expect(results2).not.toBeUndefined();
expect(results2?.[0]).toEqual(results2?.[1]);
});

it("should replace duplicated codes by the same bin url with different extension", async () => {
const results = (await processContent("```python\na\nb``` ```py\na\nb```", 1))?.split(" ", 2);
const results = (await processContent("```python\na\nb``` ```py\na\nb```", 1))?.[0].split(" ", 2);
expect(results).not.toBeUndefined();
expect(results?.[0]).toEqual(`${results?.[1].slice(0, -3)}python>`);
});
Expand Down
1 change: 1 addition & 0 deletions src/helpers/__tests__/sendBinEmbed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe(sendBinEmbed, () => {
await sendBinEmbed(
(message as unknown) as Message,
"hey",
undefined,
(embed) => embed.addField("this", "is", true),
attachments.clone().set("3", new MessageAttachment(`${cdnLink}4.jpg`, "4.jpg", { size: 1e6 })),
);
Expand Down
30 changes: 14 additions & 16 deletions src/helpers/contentProcessing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { extname } from "path";

import { BinError } from "../misc/BinError";
import { createBin } from "./createBin";
import { logError } from "./logError";
Expand Down Expand Up @@ -72,12 +70,11 @@ function insertAt(source: string, insertion: string, start: number, end = start)
return source.slice(0, start) + insertion + source.slice(end);
}

export async function processContent(source: string, maxLines: number): Promise<string | undefined> {
const codes = new Map<string, (ext?: string) => string>();
export async function processContent(source: string, maxLines: number): Promise<[string, string[]] | undefined> {
const codes = new Map<string, string | Error>();
let final = source;

let escaped = false;
let errors = 0;

for (let i = 0; i < final.length; i++) {
const char = final[i];
Expand All @@ -103,32 +100,33 @@ export async function processContent(source: string, maxLines: number): Promise<
continue;
}

let bin = codes.get(result.content.trim())?.(result.lang);
const content = result.content.trim();
let bin: string | Error | undefined = codes.get(content);

if (!bin) {
const link = await createBin({ code: result.content, filename: `..${result.lang || "txt"}` })
.then((url) => (ext = "txt"): string => `<${url.replace(extname(url), `.${ext}`)}>`)
bin = await createBin({ code: result.content, filename: `..${result.lang || "txt"}` })
.then((url) => url.slice(0, url.lastIndexOf(".")))
// eslint-disable-next-line @typescript-eslint/no-loop-func
.catch((e: Error) => {
errors++;
// log if the error is critical.
if (!(e instanceof BinError) || [400, 403, 404, 405].includes(e.code) || e.code >= 500) {
logError(e);
}
return (): string => `[${e}]`;
return e;
});

bin = link(result.lang);
codes.set(result.content.trim(), link);
codes.set(content, bin);
}

final = insertAt(final, bin, start + 1, i + result.end);
i += bin.length - 1;
const text = typeof bin === "string" ? `<${bin}.${result.lang || "txt"}>` : `[${bin}]`;
final = insertAt(final, text, start + 1, i + result.end);
i += text.length - 1;

continue;
}

escaped = false;
}
return codes.size - errors > 0 ? final : undefined;

const binUrls = [...codes.values()].filter((code): code is string => typeof code === "string");
return binUrls.length > 0 ? [final, binUrls] : undefined;
}
9 changes: 6 additions & 3 deletions src/helpers/createBin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { FormData } from "@typescord/famfor";
import { Headers, HTTPError, TimeoutError } from "got";
import { URL } from "url";

import { BinError } from "../misc/BinError";
import { request } from "./request";

const BINS_TOKEN = process.env.BINS_TOKEN!;
const BIN_URL = new URL("/new", process.env.BIN_URL);
const TOKEN_REGEXP = /[a-z\d]{24}\.[a-z\d]{6}\.[\w-]{27}|mfa\.[\w-]{84}/gi;

interface BinOptions {
Expand All @@ -15,16 +18,16 @@ interface BinOptions {

export async function createBin({ code, lifeTime, maxUses, filename }: BinOptions): Promise<string> {
const fd = new FormData();

fd.append("lifetime", (lifeTime || 0).toString());
fd.append("maxusage", (maxUses ?? 0).toString());
fd.append("code", code.replace(TOKEN_REGEXP, "[DISCORD TOKEN DETECTED]"), {
fd.append(filename, code.replace(TOKEN_REGEXP, "[DISCORD TOKEN DETECTED]"), {
filename,
type: "text/plain; charset=utf-8",
});
fd.append("token", BINS_TOKEN);

return request
.post(process.env.BIN_URL!, {
.post(BIN_URL, {
headers: (fd.headers as unknown) as Headers,
body: fd.stream,
})
Expand Down
12 changes: 9 additions & 3 deletions src/helpers/isCurrentEnvValid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export function isCurrentEnvValid(): boolean {
return ["DISCORD_TOKEN", "MAX_LINES", "CATEGORIES", "BIN_URL", "REQUEST_TIMEOUT"].every(
(name) => process.env[name],
);
return [
"DISCORD_TOKEN",
"MAX_LINES",
"CATEGORIES",
"BIN_URL",
"REQUEST_TIMEOUT",
"BINS_TOKEN",
"DELETE_MIN_REACTIONS",
].every((name) => process.env[name]);
}
2 changes: 1 addition & 1 deletion src/helpers/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const request = got.extend({
timeout: parseInt(process.env.REQUEST_TIMEOUT!, 10) || 5000,
retry: {
limit: 2,
methods: ["POST", "GET", "HEAD"],
methods: ["POST", "GET", "HEAD", "DELETE"],
statusCodes: [500, 502, 503, 504, 521, 522, 524],
errorCodes: ["ECONNRESET", "EADDRINUSE", "ECONNREFUSED", "EPIPE", "ENETUNREACH", "EAI_AGAIN"],
},
Expand Down
28 changes: 25 additions & 3 deletions src/helpers/sendBinEmbed.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Collection, Message, MessageAttachment, MessageEmbed, MessageReaction, Snowflake, User } from "discord.js";

import { request } from "./request";

const noop = (): undefined => undefined;

const DELETE_MIN_REACTIONS = parseInt(process.env.DELETE_MIN_REACTIONS!, 10);
const ADMIN_TOKEN = `Token ${process.env.BINS_TOKEN!}`;
const MAX_ATTACHMENTS_SIZE = 8_388_381;

export async function sendBinEmbed(
message: Message,
description: string,
binUrls?: string[],
extender?: (embed: MessageEmbed) => MessageEmbed,
attachments?: Collection<Snowflake, MessageAttachment>,
): Promise<void> {
Expand Down Expand Up @@ -36,7 +41,7 @@ export async function sendBinEmbed(
const botMessage = await message.channel.send({ embed, files }).catch(noop);

if (waitMessage?.deletable) {
await waitMessage.delete().catch(noop);
waitMessage.delete().catch(noop);
}

if (!botMessage) {
Expand All @@ -50,9 +55,20 @@ export async function sendBinEmbed(
await botMessage.react("🗑️");

const collector = await botMessage.awaitReactions(
({ emoji }: MessageReaction, user: User) => user.id === message.author.id && emoji.name === "🗑️",
{ max: 1, time: 20_000 },
(reaction: MessageReaction, user: User) => {
if (reaction.emoji.name !== "🗑️") {
return false;
}
return (
user.id === message.author.id ||
reaction.count! >= DELETE_MIN_REACTIONS ||
message.guild!.member(user)?.permissions.has("MANAGE_MESSAGES") ||
false
);
},
{ max: 1, time: 30_000 },
);

if (collector.size === 0) {
botMessage.reactions.removeAll().catch(noop);
return;
Expand All @@ -61,4 +77,10 @@ export async function sendBinEmbed(
if (botMessage.deletable) {
botMessage.delete().catch(noop);
}

if (binUrls) {
for (const binUrl of binUrls) {
request.delete(binUrl, { headers: { Authorization: ADMIN_TOKEN } }).catch(noop);
}
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Client } from "./classes";
import { logError } from "./helpers/logError";

const client = new Client({
messageEditHistoryMaxSize: 0,
partials: ["USER", "GUILD_MEMBER"],
messageCacheMaxSize: 0, // don't cache messages
ws: {
intents: ["GUILD_MESSAGES", "GUILDS", "GUILD_MESSAGE_REACTIONS"],
},
Expand Down
Loading