@@ -2,119 +2,162 @@ import {
22  Client , 
33  GatewayIntentBits , 
44  Events , 
5-   type  GuildScheduledEventCreateOptions , 
5+   type  GuildScheduledEventCreateOptions ,   // Use this for the payload type 
66  GuildScheduledEventEntityType , 
77  GuildScheduledEventPrivacyLevel , 
8-   GuildScheduledEvent , 
9-   GuildScheduledEventStatus , 
8+   DiscordAPIError , 
109}  from  "discord.js" ; 
1110import  {  type  EventPostRequest  }  from  "../routes/events.js" ; 
1211import  moment  from  "moment-timezone" ; 
1312
14- import  {  FastifyBaseLogger  }  from  "fastify" ; 
13+ import  {  type   FastifyBaseLogger  }  from  "fastify" ; 
1514import  {  DiscordEventError  }  from  "../../common/errors/index.js" ; 
16- import  {  type  SecretConfig  }  from  "../../common/config.js" ; 
1715
18- // https://stackoverflow.com/a/3809435/5684541 
19- // https://calendar-buff.acmuiuc.pages.dev/calendar?id=dd7af73a-3df6-4e12-b228-0d2dac34fda7&date=2024-08-30 
20- // https://www.acm.illinois.edu/calendar?id=dd7af73a-3df6-4e12-b228-0d2dac34fda7&date=2024-08-30 
21- 
22- export  type  IUpdateDiscord  =  EventPostRequest  &  {  id : string  } ; 
23- 
24- const  urlRegex  =  / h t t p s : \/ \/ [ a - z 0 - 9 . - ] + \/ c a l e n d a r \? i d = ( [ a - f 0 - 9 - ] + ) / ; 
16+ export  type  IUpdateDiscord  =  EventPostRequest  &  { 
17+   id : string ; 
18+   discordEventId ?: string ; 
19+ } ; 
2520
21+ /** 
22+  * Creates, updates, or deletes a Discord scheduled event directly using its ID. 
23+  * @param  config - Bot configuration containing the token and guild ID. 
24+  * @param  event - The event data, including an optional discordEventId for updates/deletions. 
25+  * @param  actor - The user performing the action, for logging purposes. 
26+  * @param  isDelete - A flag to indicate if the event should be deleted. 
27+  * @param  logger - The logger instance. 
28+  * @returns  The Discord event ID if created/updated, or null if deleted or the operation is skipped. 
29+  */ 
2630export  const  updateDiscord  =  async  ( 
2731  config : {  botToken : string ;  guildId : string  } , 
2832  event : IUpdateDiscord , 
2933  actor : string , 
3034  isDelete : boolean  =  false , 
3135  logger : FastifyBaseLogger , 
32- ) : Promise < null  |  GuildScheduledEventCreateOptions >  =>  { 
36+ ) : Promise < string  |  null >  =>  { 
37+   if  ( ! config . botToken )  { 
38+     logger . error ( "No Discord bot token found in secrets!" ) ; 
39+     throw  new  DiscordEventError ( { 
40+       message : "Discord bot token is not configured." , 
41+     } ) ; 
42+   } 
43+ 
3344  const  client  =  new  Client ( {  intents : [ GatewayIntentBits . Guilds ]  } ) ; 
34-   let  payload : GuildScheduledEventCreateOptions  |  null  =  null ; 
35-   client . once ( Events . ClientReady ,  async  ( readyClient : Client < true > )  =>  { 
36-     logger . debug ( `Logged in as ${ readyClient . user . tag }  ` ) ; 
37-     const  guildID  =  config . guildId ; 
38-     const  guild  =  await  client . guilds . fetch ( guildID ?. toString ( )  ||  "" ) ; 
39-     const  discordEvents  =  await  guild . scheduledEvents . fetch ( ) ; 
40-     const  snowflakeMeetingLookup  =  discordEvents . reduce ( 
41-       ( 
42-         o : Record < string ,  GuildScheduledEvent < GuildScheduledEventStatus > > , 
43-         event : GuildScheduledEvent < GuildScheduledEventStatus > , 
44-       )  =>  { 
45-         const  {  description }  =  event ; 
46-         // Find url in description using regex and extract the slug 
47-         const  url  =  ( description  ||  "" ) . match ( urlRegex ) ; 
48-         if  ( url )  { 
49-           const  id  =  url [ 1 ] ; 
50-           o [ id ]  =  event ; 
51-         } 
52-         return  o ; 
53-       } , 
54-       { }  as  Record < string ,  GuildScheduledEvent < GuildScheduledEventStatus > > , 
55-     ) ; 
56-     const  {  id }  =  event ; 
5745
58-     const  existingMetadata  =  snowflakeMeetingLookup [ id ] ; 
46+   try  { 
47+     const  result  =  await  new  Promise < string  |  null > ( ( resolve ,  reject )  =>  { 
48+       client . once ( Events . ClientReady ,  async  ( readyClient : Client < true > )  =>  { 
49+         logger . debug ( `Logged in to Discord as ${ readyClient . user . tag }  ` ) ; 
50+         try  { 
51+           const  guild  =  await  client . guilds . fetch ( config . guildId ) ; 
5952
60-     if  ( isDelete )  { 
61-       if  ( existingMetadata )  { 
62-         await  guild . scheduledEvents . delete ( existingMetadata . id ) ; 
63-       }  else  { 
64-         logger . warn ( `Event with id ${ id }   not found in Discord` ) ; 
65-       } 
66-       await  client . destroy ( ) ; 
67-       logger . debug ( "Logged out of Discord." ) ; 
68-       return  null ; 
69-     } 
53+           if  ( isDelete )  { 
54+             if  ( event . discordEventId )  { 
55+               await  guild . scheduledEvents . delete ( event . discordEventId ) ; 
56+               logger . info ( 
57+                 `Successfully deleted Discord event ${ event . discordEventId }  ` , 
58+               ) ; 
59+               return  resolve ( null ) ; 
60+             } 
61+             logger . warn ( 
62+               `Cannot delete event with internal ID ${ event . id }  : no discordEventId was provided.` , 
63+             ) ; 
64+             return  resolve ( null ) ; 
65+           } 
7066
71-     // Handle creation or update 
72-     const  {  title,  description,  start,  end,  location,  host }  =  event ; 
73-     const  dateStart  =  moment . tz ( start ,  "America/Chicago" ) . format ( "YYYY-MM-DD" ) ; 
74-     const  calendarURL  =  `https://www.acm.illinois.edu/calendar?id=${ id }  &date=${ dateStart }  ` ; 
75-     const  fullDescription  =  `${ description }  \n${ calendarURL }  ` ; 
76-     const  fullTitle  =  title . toLowerCase ( ) . includes ( host . toLowerCase ( ) ) 
77-       ? title 
78-       : `${ host }   - ${ title }  ` ; 
67+           const  {  id,  title,  description,  start,  end,  location,  host }  =  event ; 
68+           const  dateStart  =  moment 
69+             . tz ( start ,  "America/Chicago" ) 
70+             . format ( "YYYY-MM-DD" ) ; 
71+           const  calendarURL  =  `https://www.acm.illinois.edu/calendar?id=${ id }  &date=${ dateStart }  ` ; 
72+           const  fullDescription  =  `${ description }  \n\nView on ACM Calendar: ${ calendarURL }  ` ; 
73+           const  fullTitle  = 
74+             title . toLowerCase ( ) . includes ( host . toLowerCase ( ) )  ||  host  ===  "ACM" 
75+               ? title 
76+               : `${ host }   - ${ title }  ` ; 
7977
80-     payload  =  { 
81-       entityType : GuildScheduledEventEntityType . External , 
82-       privacyLevel : GuildScheduledEventPrivacyLevel . GuildOnly , 
83-       name : fullTitle , 
84-       description : fullDescription , 
85-       scheduledStartTime : moment . tz ( start ,  "America/Chicago" ) . utc ( ) . toDate ( ) , 
86-       scheduledEndTime : end  &&  moment . tz ( end ,  "America/Chicago" ) . utc ( ) . toDate ( ) , 
87-       image : existingMetadata ?. coverImageURL ( { } )  ||  undefined , 
88-       entityMetadata : { 
89-         location, 
90-       } , 
91-       reason : `${ existingMetadata  ? "Modified"  : "Created" }   by ${ actor }  .` , 
92-     } ; 
78+           const  payload : GuildScheduledEventCreateOptions  =  { 
79+             name : fullTitle , 
80+             description : fullDescription , 
81+             scheduledStartTime : moment 
82+               . tz ( start ,  "America/Chicago" ) 
83+               . utc ( ) 
84+               . toDate ( ) , 
85+             scheduledEndTime : end 
86+               ? moment . tz ( end ,  "America/Chicago" ) . utc ( ) . toDate ( ) 
87+               : undefined , 
88+             entityType : GuildScheduledEventEntityType . External , 
89+             privacyLevel : GuildScheduledEventPrivacyLevel . GuildOnly , 
90+             entityMetadata : {  location } , 
91+           } ; 
9392
94-     if  ( existingMetadata )  { 
95-       if  ( existingMetadata . creator ?. bot  !==  true )  { 
96-         logger . warn ( `Refusing to edit non-bot event "${ title }  "` ) ; 
97-       }  else  { 
98-         await  guild . scheduledEvents . edit ( existingMetadata . id ,  payload ) ; 
99-       } 
100-     }  else  if  ( payload . scheduledStartTime  <  new  Date ( ) )  { 
101-       logger . warn ( `Refusing to create past event "${ title }  "` ) ; 
102-     }  else  { 
103-       await  guild . scheduledEvents . create ( payload ) ; 
104-     } 
93+           if  ( event . discordEventId )  { 
94+             const  existingEvent  =  await  guild . scheduledEvents 
95+               . fetch ( event . discordEventId ) 
96+               . catch ( ( )  =>  null ) ; 
10597
106-     await  client . destroy ( ) ; 
107-     logger . debug ( "Logged out of Discord." ) ; 
108-     return  payload ; 
109-   } ) ; 
98+             if  ( ! existingEvent )  { 
99+               logger . warn ( 
100+                 `Discord event ${ event . discordEventId }   not found for update. Attempting to create a new one instead.` , 
101+               ) ; 
102+             }  else  { 
103+               logger . info ( 
104+                 `Updating Discord event ${ existingEvent . id }   for "${ title }  "` , 
105+               ) ; 
106+               const  updatedEvent  =  await  guild . scheduledEvents . edit ( 
107+                 existingEvent . id , 
108+                 { 
109+                   ...payload , 
110+                   reason : `Modified by ${ actor }  .` , 
111+                 } , 
112+               ) ; 
113+               return  resolve ( updatedEvent . id ) ; 
114+             } 
115+           } 
110116
111-   const  token  =  config . botToken ; 
117+           if  ( payload . scheduledStartTime  <  new  Date ( ) )  { 
118+             logger . warn ( `Refusing to create past event "${ title }  "` ) ; 
119+             return  resolve ( null ) ; 
120+           } 
112121
113-   if  ( ! token )  { 
114-     logger . error ( "No Discord bot token found in secrets!" ) ; 
115-     throw  new  DiscordEventError ( { } ) ; 
116-   } 
122+           logger . info ( `Creating new Discord event for "${ title }  "` ) ; 
123+           const  newEvent  =  await  guild . scheduledEvents . create ( { 
124+             ...payload , 
125+             reason : `Created by ${ actor }  .` , 
126+           } ) ; 
127+           return  resolve ( newEvent . id ) ; 
128+         }  catch  ( error )  { 
129+           if  ( 
130+             error  instanceof  DiscordAPIError  && 
131+             error . status  ===  404  && 
132+             error . method  ===  "DELETE"  && 
133+             isDelete 
134+           )  { 
135+             logger . warn ( `Event ${ event . id }   was already deleted from Discord!` ) ; 
136+             return  resolve ( null ) ; 
137+           } 
138+           logger . error ( 
139+             error , 
140+             "An error occurred while managing a Discord scheduled event." , 
141+           ) ; 
142+           reject ( 
143+             new  DiscordEventError ( { 
144+               message : "An error occurred while interacting with Discord." , 
145+             } ) , 
146+           ) ; 
147+         } 
148+       } ) ; 
149+ 
150+       client . login ( config . botToken ) . catch ( ( loginError )  =>  { 
151+         logger . error ( loginError ,  "Failed to log in to Discord." ) ; 
152+         reject ( new  DiscordEventError ( {  message : "Discord login failed."  } ) ) ; 
153+       } ) ; 
154+     } ) ; 
117155
118-   client . login ( token . toString ( ) ) ; 
119-   return  payload ; 
156+     return  result ; 
157+   }  finally  { 
158+     if  ( client . readyAt )  { 
159+       await  client . destroy ( ) ; 
160+       logger . debug ( "Logged out of Discord." ) ; 
161+     } 
162+   } 
120163} ; 
0 commit comments