Skip to content

Commit

Permalink
Enabled CodeyBot to nuke message without PDFs in #resume-critique and…
Browse files Browse the repository at this point in the history
… DM user with explanation (#505)

* Enabled CodeyBot to nuke message without PDFs in #resume-critique and DM user with explanation

* Allowed for messages with images in #resume-critique

* Allowed bot to convert HEIF/HEIC images to JPG so that Discord can show image preview

* Fixed some linting issues

---------

Co-authored-by: Di Nguyen <[email protected]>
  • Loading branch information
KuroganeToyama and KuroganeToyama authored Jan 23, 2024
1 parent 32e8541 commit 056ee25
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 37 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dotenv": "^8.6.0",
"emoji-regex": "^10.2.1",
"engine-blackjack-ts": "^0.9.11",
"heic2jpg": "^1.0.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
Expand Down
155 changes: 118 additions & 37 deletions src/events/messageCreate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import axios from 'axios';
import { ChannelType, Client, Message, PermissionsBitField } from 'discord.js';
import {
ChannelType,
Client,
Message,
PermissionsBitField,
channelMention,
userMention,
EmbedBuilder,
} from 'discord.js';
import { readFileSync } from 'fs';
import { writeFile } from 'fs/promises';
import { PDFDocument } from 'pdf-lib';
Expand All @@ -9,11 +17,14 @@ import { vars } from '../config';
import { sendKickEmbed } from '../utils/embeds';
import { convertPdfToPic } from '../utils/pdfToPic';
import { openDB } from '../components/db';
import { spawnSync } from 'child_process';

const ANNOUNCEMENTS_CHANNEL_ID: string = vars.ANNOUNCEMENTS_CHANNEL_ID;
const RESUME_CHANNEL_ID: string = vars.RESUME_CHANNEL_ID;
const IRC_USER_ID: string = vars.IRC_USER_ID;
const PDF_FILE_PATH = 'tmp/resume.pdf';
const HEIC_FILE_PATH = 'tmp/img.heic';
const CONVERTED_IMG_PATH = 'tmp/img.jpg';

/*
* If honeypot is to exist again, then add HONEYPOT_CHANNEL_ID to the config
Expand Down Expand Up @@ -74,49 +85,119 @@ const punishSpammersAndTrolls = async (
};

/**
* Convert any pdfs sent in the #resumes channel to an image.
* Convert any pdfs sent in the #resumes channel to an image,
* nuke message and DM user if no attachment is found or attachment is not PDF
*/
const convertResumePdfsIntoImages = async (
client: Client,
message: Message,
): Promise<Message<boolean> | undefined> => {
const attachment = message.attachments.first();
// If no resume pdf is provided, do nothing
if (!attachment || attachment.contentType !== 'application/pdf') return;
const db = await openDB();
const hasAttachment = attachment;
const isPDF = attachment && attachment.contentType === 'application/pdf';
const isImage =
attachment && attachment.contentType && attachment.contentType.startsWith('image');

// If no resume pdf is provided, nuke message and DM user about why their message got nuked
if (!(hasAttachment && (isPDF || isImage))) {
const user = message.author.id;
const channel = message.channelId;

const mentionUser = userMention(user);
const mentionChannel = channelMention(channel);

// Get resume pdf from message and write locally to tmp
const pdfLink = attachment.url;
const pdfResponse = await axios.get(pdfLink, { responseType: 'stream' });
const pdfContent = pdfResponse.data;
await writeFile(PDF_FILE_PATH, pdfContent);

// Get the size of the pdf
const pdfDocument = await PDFDocument.load(readFileSync(PDF_FILE_PATH));
const { width, height } = pdfDocument.getPage(0).getSize();
if (pdfDocument.getPageCount() > 1) {
return await message.channel.send('Resume must be 1 page.');
const explainMessage = `Hey ${mentionUser}, we've removed your message from ${mentionChannel} since only messages with PDFs/images are allowed there.
If you want critiques on your resume, please attach PDF/image when sending messages in ${mentionChannel}.
If you want to make critiques on a specific resume, please go to the corresponding thread in ${mentionChannel}.`;
const explainEmbed = new EmbedBuilder()
.setColor('Red')
.setTitle('Invalid Message Detected')
.setDescription(explainMessage);

await message.delete();
await client.users.send(user, { embeds: [explainEmbed] });

return;
}

const fileMatch = pdfLink.match('[^/]*$') || ['Resume'];
// Remove url parameters by calling `.split(?)[0]`
const fileName = fileMatch[0].split('?')[0];
// Convert the resume pdf into image
const imgResponse = await convertPdfToPic(PDF_FILE_PATH, 'resume', width * 2, height * 2);
// Send the image back to the channel as a thread
const thread = await message.startThread({
name: fileName.length < 100 ? fileName : 'Resume',
autoArchiveDuration: 60,
});
const preview_message = await thread.send({
files: imgResponse.map((img) => img.path),
});
// Inserting the pdf and preview message IDs into the DB
await db.run(
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
message.id,
preview_message.id,
);
return preview_message;
const db = await openDB();

if (isPDF) {
// Get resume pdf from message and write locally to tmp
const pdfLink = attachment.url;
const pdfResponse = await axios.get(pdfLink, { responseType: 'stream' });
const pdfContent = pdfResponse.data;
await writeFile(PDF_FILE_PATH, pdfContent);

// Get the size of the pdf
const pdfDocument = await PDFDocument.load(readFileSync(PDF_FILE_PATH));
const { width, height } = pdfDocument.getPage(0).getSize();
if (pdfDocument.getPageCount() > 1) {
return await message.channel.send('Resume must be 1 page.');
}

const fileMatch = pdfLink.match('[^/]*$') || ['Resume'];
// Remove url parameters by calling `.split(?)[0]`
const fileName = fileMatch[0].split('?')[0];
// Convert the resume pdf into image
const imgResponse = await convertPdfToPic(PDF_FILE_PATH, 'resume', width * 2, height * 2);
// Send the image back to the channel as a thread
const thread = await message.startThread({
name: fileName.length < 100 ? fileName : 'Resume',
autoArchiveDuration: 60,
});
const preview_message = await thread.send({
files: imgResponse.map((img) => img.path),
});
// Inserting the pdf and preview message IDs into the DB
await db.run(
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
message.id,
preview_message.id,
);
return preview_message;
} else if (isImage) {
let imageLink = attachment.url;

// Convert HEIC/HEIF to JPG
const isHEIC: boolean =
attachment &&
(attachment.contentType === 'image/heic' || attachment.contentType === 'image/heif');
if (isHEIC) {
const heicResponse = await axios.get(imageLink, { responseType: 'stream' });
const heicContent = heicResponse.data;
await writeFile(HEIC_FILE_PATH, heicContent);

const convertCommand = `npx heic2jpg ${HEIC_FILE_PATH}`;

spawnSync('sh', ['-c', convertCommand], { stdio: 'inherit' });
spawnSync('sh', ['-c', 'mv img.jpg tmp'], { stdio: 'inherit' });

imageLink = CONVERTED_IMG_PATH;
}

// Create a thread with the resume image
const imageName = attachment.name;
const thread = await message.startThread({
name: imageName.length < 100 ? imageName : 'Resume',
autoArchiveDuration: 60,
});

const preview_message = await thread.send({
files: [imageLink],
});

// Inserting the image and preview message IDs into the DB
await db.run(
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
message.id,
preview_message.id,
);

return preview_message;
}
};

export const initMessageCreate = async (
Expand All @@ -135,7 +216,7 @@ export const initMessageCreate = async (

// If channel is in resumes, convert the message attachment to an image
if (message.channelId === RESUME_CHANNEL_ID) {
await convertResumePdfsIntoImages(message);
await convertResumePdfsIntoImages(client, message);
}

// Ignore DMs; include announcements, thread, and regular text channels
Expand Down
Loading

0 comments on commit 056ee25

Please sign in to comment.