@@ -23,12 +23,15 @@ export default class Checkin extends Command {
2323 . addBooleanOption ( option =>
2424 option
2525 . 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!' )
2727 . setRequired ( false )
2828 )
2929 . addBooleanOption ( option =>
3030 option . setName ( 'widescreen' ) . setDescription ( 'Include a slide for the QR code.' ) . setRequired ( false )
3131 )
32+ . addBooleanOption ( option =>
33+ option . setName ( 'asform' ) . setDescription ( 'Generate a second QR code for AS Funding' ) . setRequired ( false )
34+ )
3235 . addStringOption ( option =>
3336 option . setName ( 'date' ) . setDescription ( 'The date to check for events. Use MM/DD format.' ) . setRequired ( false )
3437 )
@@ -71,6 +74,7 @@ export default class Checkin extends Command {
7174 // Get arguments. Get rid of the null types by checking them.
7275 const publicArgument = interaction . options . getBoolean ( 'public' ) ;
7376 const widescreenArgument = interaction . options . getBoolean ( 'widescreen' ) ;
77+ const asFormArgument = interaction . options . getBoolean ( 'asform' ) ;
7478 const dateArgument = interaction . options . getString ( 'date' ) ;
7579
7680 // 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 {
101105 const isPublic = publicArgument !== null ? publicArgument : false ;
102106 // By default, we want to include the slide.
103107 const needsSlide = widescreenArgument !== null ? widescreenArgument : true ;
108+ // By default, we want to generate the dual AS Form
109+ const needsASForm = asFormArgument !== null ? asFormArgument : true ;
104110
105111 // Defer the reply ephemerally only if it's a private command call.
106112 await super . defer ( interaction , ! isPublic ) ;
@@ -136,18 +142,31 @@ export default class Checkin extends Command {
136142
137143 // Now we finally check the command argument.
138144 // If we just had `checkin` in our call, no arguments...
145+ const { asAttendanceForm } = this . client . settings ;
139146 if ( ! isPublic ) {
140147 const author = await this . client . users . fetch ( interaction . member ! . user . id ) ;
141148 // 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+ ) ;
143156 await author . send ( privateMessage ) ;
144157 await super . edit ( interaction , {
145158 content : 'Check your DM.' ,
146159 ephemeral : true ,
147160 } ) ;
148161 await interaction . followUp ( `**/checkin** was used privately by ${ interaction . user } !` ) ;
149162 } 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+ ) ;
151170 await super . edit ( interaction , publicMessage ) ;
152171 }
153172 } catch ( e ) {
@@ -192,14 +211,28 @@ export default class Checkin extends Command {
192211 * Generate the QR Code for the given event and and return the Data URL for the code.
193212 * @param event Portal Event to create the QR code for.
194213 * @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
195217 * @returns URL of the generated QR code.
196218 */
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+ ) {
198226 // Doesn't need landscape QR slide. Return the QR code by itself
199227 let qrCodeDataUrl ;
200228 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+ }
203236 } else {
204237 const eventQrCode = QR . generateQR (
205238 expressCheckinURL . toString ( ) ,
@@ -216,9 +249,10 @@ export default class Checkin extends Command {
216249 * Creates a slide with the given QR Code and returns its URL.
217250 * @param event Portal Event to create the slide for.
218251 * @param eventQrCode QR Code for the event.
252+ * @param asFormQrCode Prefilled QR Code for AS Funding Form.
219253 * @returns URL of the generated slide.
220254 */
221- private static async createQRSlide ( event : PortalEvent , eventQrCode : string ) {
255+ private static async createQRSlide ( event : PortalEvent , eventQrCode : string , asFormQrCode ?: string ) {
222256 /**
223257 * Rescales the font; makes the font size smaller if the text is longer
224258 * and bigger if the text is shorter.
@@ -241,9 +275,68 @@ export default class Checkin extends Command {
241275
242276 // Creating slide with Canvas
243277 // Helpful resource: https://blog.logrocket.com/creating-saving-images-node-canvas/
244- const slide = createCanvas ( 1920 , 1080 ) ;
278+ const slide = createCanvas ( 1920 , 1280 ) ;
245279 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
247340
248341 // Draw background
249342 const background = await loadImage ( './src/assets/qr-slide-background.png' ) ;
@@ -296,8 +389,7 @@ export default class Checkin extends Command {
296389
297390 // Get the Data URL of the image (base-64 encoded string of image).
298391 // Easier to attach than saving files.
299- const qrCodeDataUrl = await slide . toDataURL ( ) ;
300- return qrCodeDataUrl ;
392+ return slide . toDataURL ( ) ;
301393 }
302394
303395 /**
@@ -319,7 +411,9 @@ export default class Checkin extends Command {
319411 private static async getCheckinMessage (
320412 events : PortalEvent [ ] ,
321413 isPublic : boolean ,
322- needsSlide : boolean
414+ needsSlide : boolean ,
415+ needsASForm : boolean ,
416+ asAttendanceForm : string
323417 ) : Promise < InteractionPayload > {
324418 // This method became very complicated very quickly, so we'll break this down.
325419 // 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 {
339433 const expressCheckinURL = new URL ( 'https://members.acmucsd.com/checkin' ) ;
340434 expressCheckinURL . searchParams . set ( 'code' , event . attendanceCode ) ;
341435
436+ const asFormFilledURL = new URL ( asAttendanceForm + event . title . replace ( ' ' , '+' ) ) ;
437+ // +'&entry.570464428='+event.foodItems.replace(' ', '+') — for food items
438+
342439 // Add the Event's title and make it a hyperlink to the express check-in URL.
343440 description . push ( `*[${ event . title } ](${ expressCheckinURL } )*` ) ;
344441 // Add the check-in code for those who want to copy-paste it.
@@ -347,7 +444,13 @@ export default class Checkin extends Command {
347444 description . push ( '\n' ) ;
348445
349446 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+ ) ;
351454 // Do some Discord.js shenanigans to generate an attachment from the image.
352455 // Apparently, the Data URL MIME type of an image needs to be removed before given to
353456 // Discord.js. Probably because the base64 encode is enough,
0 commit comments