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 } ${ mentionChannel }  
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