Skip to content

Commit 056ee25

Browse files
Enabled CodeyBot to nuke message without PDFs in #resume-critique and 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]>
1 parent 32e8541 commit 056ee25

File tree

3 files changed

+334
-37
lines changed

3 files changed

+334
-37
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"dotenv": "^8.6.0",
3434
"emoji-regex": "^10.2.1",
3535
"engine-blackjack-ts": "^0.9.11",
36+
"heic2jpg": "^1.0.2",
3637
"js-yaml": "^4.1.0",
3738
"lodash": "^4.17.21",
3839
"moment": "^2.29.4",

src/events/messageCreate.ts

+118-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import axios from 'axios';
2-
import { ChannelType, Client, Message, PermissionsBitField } from 'discord.js';
2+
import {
3+
ChannelType,
4+
Client,
5+
Message,
6+
PermissionsBitField,
7+
channelMention,
8+
userMention,
9+
EmbedBuilder,
10+
} from 'discord.js';
311
import { readFileSync } from 'fs';
412
import { writeFile } from 'fs/promises';
513
import { PDFDocument } from 'pdf-lib';
@@ -9,11 +17,14 @@ import { vars } from '../config';
917
import { sendKickEmbed } from '../utils/embeds';
1018
import { convertPdfToPic } from '../utils/pdfToPic';
1119
import { openDB } from '../components/db';
20+
import { spawnSync } from 'child_process';
1221

1322
const ANNOUNCEMENTS_CHANNEL_ID: string = vars.ANNOUNCEMENTS_CHANNEL_ID;
1423
const RESUME_CHANNEL_ID: string = vars.RESUME_CHANNEL_ID;
1524
const IRC_USER_ID: string = vars.IRC_USER_ID;
1625
const PDF_FILE_PATH = 'tmp/resume.pdf';
26+
const HEIC_FILE_PATH = 'tmp/img.heic';
27+
const CONVERTED_IMG_PATH = 'tmp/img.jpg';
1728

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

7687
/**
77-
* Convert any pdfs sent in the #resumes channel to an image.
88+
* Convert any pdfs sent in the #resumes channel to an image,
89+
* nuke message and DM user if no attachment is found or attachment is not PDF
7890
*/
7991
const convertResumePdfsIntoImages = async (
92+
client: Client,
8093
message: Message,
8194
): Promise<Message<boolean> | undefined> => {
8295
const attachment = message.attachments.first();
83-
// If no resume pdf is provided, do nothing
84-
if (!attachment || attachment.contentType !== 'application/pdf') return;
85-
const db = await openDB();
96+
const hasAttachment = attachment;
97+
const isPDF = attachment && attachment.contentType === 'application/pdf';
98+
const isImage =
99+
attachment && attachment.contentType && attachment.contentType.startsWith('image');
100+
101+
// If no resume pdf is provided, nuke message and DM user about why their message got nuked
102+
if (!(hasAttachment && (isPDF || isImage))) {
103+
const user = message.author.id;
104+
const channel = message.channelId;
105+
106+
const mentionUser = userMention(user);
107+
const mentionChannel = channelMention(channel);
86108

87-
// Get resume pdf from message and write locally to tmp
88-
const pdfLink = attachment.url;
89-
const pdfResponse = await axios.get(pdfLink, { responseType: 'stream' });
90-
const pdfContent = pdfResponse.data;
91-
await writeFile(PDF_FILE_PATH, pdfContent);
92-
93-
// Get the size of the pdf
94-
const pdfDocument = await PDFDocument.load(readFileSync(PDF_FILE_PATH));
95-
const { width, height } = pdfDocument.getPage(0).getSize();
96-
if (pdfDocument.getPageCount() > 1) {
97-
return await message.channel.send('Resume must be 1 page.');
109+
const explainMessage = `Hey ${mentionUser}, we've removed your message from ${mentionChannel} since only messages with PDFs/images are allowed there.
110+
111+
If you want critiques on your resume, please attach PDF/image when sending messages in ${mentionChannel}.
112+
113+
If you want to make critiques on a specific resume, please go to the corresponding thread in ${mentionChannel}.`;
114+
const explainEmbed = new EmbedBuilder()
115+
.setColor('Red')
116+
.setTitle('Invalid Message Detected')
117+
.setDescription(explainMessage);
118+
119+
await message.delete();
120+
await client.users.send(user, { embeds: [explainEmbed] });
121+
122+
return;
98123
}
99124

100-
const fileMatch = pdfLink.match('[^/]*$') || ['Resume'];
101-
// Remove url parameters by calling `.split(?)[0]`
102-
const fileName = fileMatch[0].split('?')[0];
103-
// Convert the resume pdf into image
104-
const imgResponse = await convertPdfToPic(PDF_FILE_PATH, 'resume', width * 2, height * 2);
105-
// Send the image back to the channel as a thread
106-
const thread = await message.startThread({
107-
name: fileName.length < 100 ? fileName : 'Resume',
108-
autoArchiveDuration: 60,
109-
});
110-
const preview_message = await thread.send({
111-
files: imgResponse.map((img) => img.path),
112-
});
113-
// Inserting the pdf and preview message IDs into the DB
114-
await db.run(
115-
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
116-
message.id,
117-
preview_message.id,
118-
);
119-
return preview_message;
125+
const db = await openDB();
126+
127+
if (isPDF) {
128+
// Get resume pdf from message and write locally to tmp
129+
const pdfLink = attachment.url;
130+
const pdfResponse = await axios.get(pdfLink, { responseType: 'stream' });
131+
const pdfContent = pdfResponse.data;
132+
await writeFile(PDF_FILE_PATH, pdfContent);
133+
134+
// Get the size of the pdf
135+
const pdfDocument = await PDFDocument.load(readFileSync(PDF_FILE_PATH));
136+
const { width, height } = pdfDocument.getPage(0).getSize();
137+
if (pdfDocument.getPageCount() > 1) {
138+
return await message.channel.send('Resume must be 1 page.');
139+
}
140+
141+
const fileMatch = pdfLink.match('[^/]*$') || ['Resume'];
142+
// Remove url parameters by calling `.split(?)[0]`
143+
const fileName = fileMatch[0].split('?')[0];
144+
// Convert the resume pdf into image
145+
const imgResponse = await convertPdfToPic(PDF_FILE_PATH, 'resume', width * 2, height * 2);
146+
// Send the image back to the channel as a thread
147+
const thread = await message.startThread({
148+
name: fileName.length < 100 ? fileName : 'Resume',
149+
autoArchiveDuration: 60,
150+
});
151+
const preview_message = await thread.send({
152+
files: imgResponse.map((img) => img.path),
153+
});
154+
// Inserting the pdf and preview message IDs into the DB
155+
await db.run(
156+
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
157+
message.id,
158+
preview_message.id,
159+
);
160+
return preview_message;
161+
} else if (isImage) {
162+
let imageLink = attachment.url;
163+
164+
// Convert HEIC/HEIF to JPG
165+
const isHEIC: boolean =
166+
attachment &&
167+
(attachment.contentType === 'image/heic' || attachment.contentType === 'image/heif');
168+
if (isHEIC) {
169+
const heicResponse = await axios.get(imageLink, { responseType: 'stream' });
170+
const heicContent = heicResponse.data;
171+
await writeFile(HEIC_FILE_PATH, heicContent);
172+
173+
const convertCommand = `npx heic2jpg ${HEIC_FILE_PATH}`;
174+
175+
spawnSync('sh', ['-c', convertCommand], { stdio: 'inherit' });
176+
spawnSync('sh', ['-c', 'mv img.jpg tmp'], { stdio: 'inherit' });
177+
178+
imageLink = CONVERTED_IMG_PATH;
179+
}
180+
181+
// Create a thread with the resume image
182+
const imageName = attachment.name;
183+
const thread = await message.startThread({
184+
name: imageName.length < 100 ? imageName : 'Resume',
185+
autoArchiveDuration: 60,
186+
});
187+
188+
const preview_message = await thread.send({
189+
files: [imageLink],
190+
});
191+
192+
// Inserting the image and preview message IDs into the DB
193+
await db.run(
194+
'INSERT INTO resume_preview_info (initial_pdf_id, preview_id) VALUES(?, ?)',
195+
message.id,
196+
preview_message.id,
197+
);
198+
199+
return preview_message;
200+
}
120201
};
121202

122203
export const initMessageCreate = async (
@@ -135,7 +216,7 @@ export const initMessageCreate = async (
135216

136217
// If channel is in resumes, convert the message attachment to an image
137218
if (message.channelId === RESUME_CHANNEL_ID) {
138-
await convertResumePdfsIntoImages(message);
219+
await convertResumePdfsIntoImages(client, message);
139220
}
140221

141222
// Ignore DMs; include announcements, thread, and regular text channels

0 commit comments

Comments
 (0)