@@ -23,12 +23,15 @@ export default class Checkin extends Command {
23
23
. addBooleanOption ( option =>
24
24
option
25
25
. setName ( 'public' )
26
- . setDescription ( 'If true, send public embed of checking code for live events!' )
26
+ . setDescription ( 'If true, send public embed of check-in code for live events!' )
27
27
. setRequired ( false )
28
28
)
29
29
. addBooleanOption ( option =>
30
30
option . setName ( 'widescreen' ) . setDescription ( 'Include a slide for the QR code.' ) . setRequired ( false )
31
31
)
32
+ . addBooleanOption ( option =>
33
+ option . setName ( 'asform' ) . setDescription ( 'Generate a second QR code for AS Funding' ) . setRequired ( false )
34
+ )
32
35
. addStringOption ( option =>
33
36
option . setName ( 'date' ) . setDescription ( 'The date to check for events. Use MM/DD format.' ) . setRequired ( false )
34
37
)
@@ -71,6 +74,7 @@ export default class Checkin extends Command {
71
74
// Get arguments. Get rid of the null types by checking them.
72
75
const publicArgument = interaction . options . getBoolean ( 'public' ) ;
73
76
const widescreenArgument = interaction . options . getBoolean ( 'widescreen' ) ;
77
+ const asFormArgument = interaction . options . getBoolean ( 'asform' ) ;
74
78
const dateArgument = interaction . options . getString ( 'date' ) ;
75
79
76
80
// Regex to match dates in the format of MM/DD(/YYYY) or MM-DD(-YYYY).
@@ -101,6 +105,8 @@ export default class Checkin extends Command {
101
105
const isPublic = publicArgument !== null ? publicArgument : false ;
102
106
// By default, we want to include the slide.
103
107
const needsSlide = widescreenArgument !== null ? widescreenArgument : true ;
108
+ // By default, we want to generate the dual AS Form
109
+ const needsASForm = asFormArgument !== null ? asFormArgument : true ;
104
110
105
111
// Defer the reply ephemerally only if it's a private command call.
106
112
await super . defer ( interaction , ! isPublic ) ;
@@ -136,18 +142,31 @@ export default class Checkin extends Command {
136
142
137
143
// Now we finally check the command argument.
138
144
// If we just had `checkin` in our call, no arguments...
145
+ const { asAttendanceForm } = this . client . settings ;
139
146
if ( ! isPublic ) {
140
147
const author = await this . client . users . fetch ( interaction . member ! . user . id ) ;
141
148
// What we need now is to construct the Payload to send for `checkin`.
142
- const privateMessage = await Checkin . getCheckinMessage ( todayEvents , isPublic , needsSlide ) ;
149
+ const privateMessage = await Checkin . getCheckinMessage (
150
+ todayEvents ,
151
+ isPublic ,
152
+ needsSlide ,
153
+ needsASForm ,
154
+ asAttendanceForm
155
+ ) ;
143
156
await author . send ( privateMessage ) ;
144
157
await super . edit ( interaction , {
145
158
content : 'Check your DM.' ,
146
159
ephemeral : true ,
147
160
} ) ;
148
161
await interaction . followUp ( `**/checkin** was used privately by ${ interaction . user } !` ) ;
149
162
} else {
150
- const publicMessage = await Checkin . getCheckinMessage ( todayEvents , isPublic , needsSlide ) ;
163
+ const publicMessage = await Checkin . getCheckinMessage (
164
+ todayEvents ,
165
+ isPublic ,
166
+ needsSlide ,
167
+ needsASForm ,
168
+ asAttendanceForm
169
+ ) ;
151
170
await super . edit ( interaction , publicMessage ) ;
152
171
}
153
172
} catch ( e ) {
@@ -192,14 +211,28 @@ export default class Checkin extends Command {
192
211
* Generate the QR Code for the given event and and return the Data URL for the code.
193
212
* @param event Portal Event to create the QR code for.
194
213
* @param expressCheckinURL URL that the QR code links to.
214
+ * @param needsASForm if an AS attendance form is needed (if we used AS funding)
215
+ * @param asFormFilledURL URL for the AS attendance form with prefilled fields.
216
+ * @param needsSlide whether or not we're generating a widesgreen slide graphic
195
217
* @returns URL of the generated QR code.
196
218
*/
197
- private static async generateQRCodeURL ( event : PortalEvent , expressCheckinURL : URL , needsSlide : boolean ) {
219
+ private static async generateQRCodeURL (
220
+ event : PortalEvent ,
221
+ expressCheckinURL : URL ,
222
+ needsASForm : boolean ,
223
+ asFormFilledURL : URL ,
224
+ needsSlide : boolean
225
+ ) {
198
226
// Doesn't need landscape QR slide. Return the QR code by itself
199
227
let qrCodeDataUrl ;
200
228
if ( needsSlide ) {
201
- const eventQrCode = QR . generateQR ( expressCheckinURL . toString ( ) , '' , '' ) ;
202
- qrCodeDataUrl = await this . createQRSlide ( event , eventQrCode ) ;
229
+ const eventQrCode = QR . generateQR ( expressCheckinURL . toString ( ) , '' , '' , 'acm' ) ;
230
+ if ( needsASForm ) {
231
+ const asFormQrCode = QR . generateQR ( asFormFilledURL . toString ( ) , '' , '' , 'as' ) ;
232
+ qrCodeDataUrl = await this . createQRSlide ( event , eventQrCode , asFormQrCode ) ;
233
+ } else {
234
+ qrCodeDataUrl = await this . createQRSlide ( event , eventQrCode ) ;
235
+ }
203
236
} else {
204
237
const eventQrCode = QR . generateQR (
205
238
expressCheckinURL . toString ( ) ,
@@ -216,9 +249,10 @@ export default class Checkin extends Command {
216
249
* Creates a slide with the given QR Code and returns its URL.
217
250
* @param event Portal Event to create the slide for.
218
251
* @param eventQrCode QR Code for the event.
252
+ * @param asFormQrCode Prefilled QR Code for AS Funding Form.
219
253
* @returns URL of the generated slide.
220
254
*/
221
- private static async createQRSlide ( event : PortalEvent , eventQrCode : string ) {
255
+ private static async createQRSlide ( event : PortalEvent , eventQrCode : string , asFormQrCode ?: string ) {
222
256
/**
223
257
* Rescales the font; makes the font size smaller if the text is longer
224
258
* and bigger if the text is shorter.
@@ -241,9 +275,68 @@ export default class Checkin extends Command {
241
275
242
276
// Creating slide with Canvas
243
277
// Helpful resource: https://blog.logrocket.com/creating-saving-images-node-canvas/
244
- const slide = createCanvas ( 1920 , 1080 ) ;
278
+ const slide = createCanvas ( 1920 , 1280 ) ;
245
279
const context = slide . getContext ( '2d' ) ;
246
- context . fillRect ( 0 , 0 , 1920 , 1080 ) ;
280
+ context . fillRect ( 0 , 0 , 1920 , 1280 ) ;
281
+
282
+ // AS attendance form and ACM portal checkin both needed — use dual layout
283
+ if ( typeof asFormQrCode !== 'undefined' && asFormQrCode ) {
284
+ // Draw background
285
+ const background = await loadImage ( './src/assets/dual-qr-slide-background.png' ) ;
286
+ context . drawImage ( background , 0 , 0 , 1920 , 1280 ) ;
287
+
288
+ // Draw QR code
289
+ // Tilting the slide 45 degrees before adding QR code
290
+ const angleInRadians = Math . PI / 4 ;
291
+ context . rotate ( angleInRadians ) ;
292
+ const qrImg = await loadImage ( await eventQrCode ) ;
293
+ const asQrImg = await loadImage ( await asFormQrCode ) ;
294
+ context . drawImage ( qrImg , 1195 , - 790 , 400 , 400 ) ;
295
+ context . drawImage ( asQrImg , 535 , - 130 , 400 , 400 ) ;
296
+ context . rotate ( - 1 * angleInRadians ) ;
297
+
298
+ // Everything starting here has a shadow
299
+ context . shadowColor = '#00000040' ;
300
+ context . shadowBlur = 4 ;
301
+ context . shadowOffsetY = 4 ;
302
+
303
+ // Event title
304
+ const title =
305
+ event . title . substring ( 0 , 36 ) === event . title ? event . title : event . title . substring ( 0 , 36 ) . concat ( '...' ) ;
306
+ const titleSize = rescaleFont ( title . length , 8 , 70 ) ;
307
+ context . textAlign = 'center' ;
308
+ context . font = `${ titleSize } pt 'DM Sans'` ;
309
+ context . fillText ( title , 480 , 1150 ) ;
310
+
311
+ // Everything starting here has a shadow
312
+ context . shadowColor = '#00000040' ;
313
+ context . shadowBlur = 6.5 ;
314
+ context . shadowOffsetY = 6.5 ;
315
+
316
+ // Code
317
+ const checkinCode = event . attendanceCode ;
318
+ const checkinSize = rescaleFont ( checkinCode . length , 30 , 70 ) ;
319
+ context . fillStyle = '#ffffff' ;
320
+ context . font = `${ checkinSize } pt 'DM Sans'` ;
321
+ const textMetrics = context . measureText ( checkinCode ) ;
322
+ let codeWidth = textMetrics . actualBoundingBoxLeft + textMetrics . actualBoundingBoxRight ;
323
+ // Add 120 for padding on left and right side
324
+ codeWidth += 120 ;
325
+ context . fillStyle = '#70BAFF' ;
326
+ context . beginPath ( ) ;
327
+ // roundRect parameters: x, y, width, height, radius
328
+ context . roundRect ( 1410 - codeWidth / 2 , 930 , codeWidth , 115 , 20 ) ;
329
+ context . fill ( ) ;
330
+ context . shadowOffsetY = 6.62 ;
331
+ context . font = `${ checkinSize } pt 'DM Sans'` ;
332
+ context . fillStyle = '#fff' ;
333
+ context . fillText ( checkinCode , 1410 , 1010 ) ;
334
+
335
+ // Get the Data URL of the image (base-64 encoded string of image).
336
+ // Easier to attach than saving files.
337
+ return slide . toDataURL ( ) ;
338
+ }
339
+ // Only ACM portal checkin needed
247
340
248
341
// Draw background
249
342
const background = await loadImage ( './src/assets/qr-slide-background.png' ) ;
@@ -296,8 +389,7 @@ export default class Checkin extends Command {
296
389
297
390
// Get the Data URL of the image (base-64 encoded string of image).
298
391
// Easier to attach than saving files.
299
- const qrCodeDataUrl = await slide . toDataURL ( ) ;
300
- return qrCodeDataUrl ;
392
+ return slide . toDataURL ( ) ;
301
393
}
302
394
303
395
/**
@@ -319,7 +411,9 @@ export default class Checkin extends Command {
319
411
private static async getCheckinMessage (
320
412
events : PortalEvent [ ] ,
321
413
isPublic : boolean ,
322
- needsSlide : boolean
414
+ needsSlide : boolean ,
415
+ needsASForm : boolean ,
416
+ asAttendanceForm : string
323
417
) : Promise < InteractionPayload > {
324
418
// This method became very complicated very quickly, so we'll break this down.
325
419
// Create arrays to store our payload contents temporarily. We'll put this in our embed
@@ -339,6 +433,9 @@ export default class Checkin extends Command {
339
433
const expressCheckinURL = new URL ( 'https://members.acmucsd.com/checkin' ) ;
340
434
expressCheckinURL . searchParams . set ( 'code' , event . attendanceCode ) ;
341
435
436
+ const asFormFilledURL = new URL ( asAttendanceForm + event . title . replace ( ' ' , '+' ) ) ;
437
+ // +'&entry.570464428='+event.foodItems.replace(' ', '+') — for food items
438
+
342
439
// Add the Event's title and make it a hyperlink to the express check-in URL.
343
440
description . push ( `*[${ event . title } ](${ expressCheckinURL } )*` ) ;
344
441
// Add the check-in code for those who want to copy-paste it.
@@ -347,7 +444,13 @@ export default class Checkin extends Command {
347
444
description . push ( '\n' ) ;
348
445
349
446
try {
350
- const qrCodeDataUrl = await this . generateQRCodeURL ( event , expressCheckinURL , needsSlide ) ;
447
+ const qrCodeDataUrl = await this . generateQRCodeURL (
448
+ event ,
449
+ expressCheckinURL ,
450
+ needsASForm ,
451
+ asFormFilledURL ,
452
+ needsSlide
453
+ ) ;
351
454
// Do some Discord.js shenanigans to generate an attachment from the image.
352
455
// Apparently, the Data URL MIME type of an image needs to be removed before given to
353
456
// Discord.js. Probably because the base64 encode is enough,
0 commit comments