11import 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' ;
311import { readFileSync } from 'fs' ;
412import { writeFile } from 'fs/promises' ;
513import { PDFDocument } from 'pdf-lib' ;
@@ -9,11 +17,14 @@ import { vars } from '../config';
917import { sendKickEmbed } from '../utils/embeds' ;
1018import { convertPdfToPic } from '../utils/pdfToPic' ;
1119import { openDB } from '../components/db' ;
20+ import { spawnSync } from 'child_process' ;
1221
1322const ANNOUNCEMENTS_CHANNEL_ID : string = vars . ANNOUNCEMENTS_CHANNEL_ID ;
1423const RESUME_CHANNEL_ID : string = vars . RESUME_CHANNEL_ID ;
1524const IRC_USER_ID : string = vars . IRC_USER_ID ;
1625const 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 */
7991const 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
122203export 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