1
1
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' ;
3
11
import { readFileSync } from 'fs' ;
4
12
import { writeFile } from 'fs/promises' ;
5
13
import { PDFDocument } from 'pdf-lib' ;
@@ -9,11 +17,14 @@ import { vars } from '../config';
9
17
import { sendKickEmbed } from '../utils/embeds' ;
10
18
import { convertPdfToPic } from '../utils/pdfToPic' ;
11
19
import { openDB } from '../components/db' ;
20
+ import { spawnSync } from 'child_process' ;
12
21
13
22
const ANNOUNCEMENTS_CHANNEL_ID : string = vars . ANNOUNCEMENTS_CHANNEL_ID ;
14
23
const RESUME_CHANNEL_ID : string = vars . RESUME_CHANNEL_ID ;
15
24
const IRC_USER_ID : string = vars . IRC_USER_ID ;
16
25
const PDF_FILE_PATH = 'tmp/resume.pdf' ;
26
+ const HEIC_FILE_PATH = 'tmp/img.heic' ;
27
+ const CONVERTED_IMG_PATH = 'tmp/img.jpg' ;
17
28
18
29
/*
19
30
* If honeypot is to exist again, then add HONEYPOT_CHANNEL_ID to the config
@@ -74,49 +85,119 @@ const punishSpammersAndTrolls = async (
74
85
} ;
75
86
76
87
/**
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
78
90
*/
79
91
const convertResumePdfsIntoImages = async (
92
+ client : Client ,
80
93
message : Message ,
81
94
) : Promise < Message < boolean > | undefined > => {
82
95
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 ) ;
86
108
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 ;
98
123
}
99
124
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
+ }
120
201
} ;
121
202
122
203
export const initMessageCreate = async (
@@ -135,7 +216,7 @@ export const initMessageCreate = async (
135
216
136
217
// If channel is in resumes, convert the message attachment to an image
137
218
if ( message . channelId === RESUME_CHANNEL_ID ) {
138
- await convertResumePdfsIntoImages ( message ) ;
219
+ await convertResumePdfsIntoImages ( client , message ) ;
139
220
}
140
221
141
222
// Ignore DMs; include announcements, thread, and regular text channels
0 commit comments