diff --git a/client/lib/generated/api/settings.pb.dart b/client/lib/generated/api/settings.pb.dart index bce9280..3b0247b 100644 --- a/client/lib/generated/api/settings.pb.dart +++ b/client/lib/generated/api/settings.pb.dart @@ -793,6 +793,8 @@ class UpdateDiscordReminderSettingsRequest extends $pb.GeneratedMessage { $fixnum.Int64? discordEndReminderMins, $core.String? discordStartReminderMessage, $core.String? discordEndReminderMessage, + $core.bool? discordAutoDeleteStartReminder, + $core.bool? discordAutoDeleteEndReminder, }) { final result = create(); if (discordStartReminderMins != null) @@ -803,6 +805,10 @@ class UpdateDiscordReminderSettingsRequest extends $pb.GeneratedMessage { result.discordStartReminderMessage = discordStartReminderMessage; if (discordEndReminderMessage != null) result.discordEndReminderMessage = discordEndReminderMessage; + if (discordAutoDeleteStartReminder != null) + result.discordAutoDeleteStartReminder = discordAutoDeleteStartReminder; + if (discordAutoDeleteEndReminder != null) + result.discordAutoDeleteEndReminder = discordAutoDeleteEndReminder; return result; } @@ -824,6 +830,8 @@ class UpdateDiscordReminderSettingsRequest extends $pb.GeneratedMessage { ..aInt64(2, _omitFieldNames ? '' : 'discordEndReminderMins') ..aOS(3, _omitFieldNames ? '' : 'discordStartReminderMessage') ..aOS(4, _omitFieldNames ? '' : 'discordEndReminderMessage') + ..aOB(5, _omitFieldNames ? '' : 'discordAutoDeleteStartReminder') + ..aOB(6, _omitFieldNames ? '' : 'discordAutoDeleteEndReminder') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -884,6 +892,24 @@ class UpdateDiscordReminderSettingsRequest extends $pb.GeneratedMessage { $core.bool hasDiscordEndReminderMessage() => $_has(3); @$pb.TagNumber(4) void clearDiscordEndReminderMessage() => $_clearField(4); + + @$pb.TagNumber(5) + $core.bool get discordAutoDeleteStartReminder => $_getBF(4); + @$pb.TagNumber(5) + set discordAutoDeleteStartReminder($core.bool value) => $_setBool(4, value); + @$pb.TagNumber(5) + $core.bool hasDiscordAutoDeleteStartReminder() => $_has(4); + @$pb.TagNumber(5) + void clearDiscordAutoDeleteStartReminder() => $_clearField(5); + + @$pb.TagNumber(6) + $core.bool get discordAutoDeleteEndReminder => $_getBF(5); + @$pb.TagNumber(6) + set discordAutoDeleteEndReminder($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasDiscordAutoDeleteEndReminder() => $_has(5); + @$pb.TagNumber(6) + void clearDiscordAutoDeleteEndReminder() => $_clearField(6); } class UpdateDiscordReminderSettingsResponse extends $pb.GeneratedMessage { diff --git a/client/lib/generated/api/settings.pbjson.dart b/client/lib/generated/api/settings.pbjson.dart index 5899cc2..92f6389 100644 --- a/client/lib/generated/api/settings.pbjson.dart +++ b/client/lib/generated/api/settings.pbjson.dart @@ -340,12 +340,32 @@ const UpdateDiscordReminderSettingsRequest$json = { '10': 'discordEndReminderMessage', '17': true }, + { + '1': 'discord_auto_delete_start_reminder', + '3': 5, + '4': 1, + '5': 8, + '9': 4, + '10': 'discordAutoDeleteStartReminder', + '17': true + }, + { + '1': 'discord_auto_delete_end_reminder', + '3': 6, + '4': 1, + '5': 8, + '9': 5, + '10': 'discordAutoDeleteEndReminder', + '17': true + }, ], '8': [ {'1': '_discord_start_reminder_mins'}, {'1': '_discord_end_reminder_mins'}, {'1': '_discord_start_reminder_message'}, {'1': '_discord_end_reminder_message'}, + {'1': '_discord_auto_delete_start_reminder'}, + {'1': '_discord_auto_delete_end_reminder'}, ], }; @@ -356,9 +376,14 @@ final $typed_data.Uint8List updateDiscordReminderSettingsRequestDescriptor = $co 'aXNjb3JkX2VuZF9yZW1pbmRlcl9taW5zGAIgASgDSAFSFmRpc2NvcmRFbmRSZW1pbmRlck1pbn' 'OIAQESSAoeZGlzY29yZF9zdGFydF9yZW1pbmRlcl9tZXNzYWdlGAMgASgJSAJSG2Rpc2NvcmRT' 'dGFydFJlbWluZGVyTWVzc2FnZYgBARJEChxkaXNjb3JkX2VuZF9yZW1pbmRlcl9tZXNzYWdlGA' - 'QgASgJSANSGWRpc2NvcmRFbmRSZW1pbmRlck1lc3NhZ2WIAQFCHgocX2Rpc2NvcmRfc3RhcnRf' - 'cmVtaW5kZXJfbWluc0IcChpfZGlzY29yZF9lbmRfcmVtaW5kZXJfbWluc0IhCh9fZGlzY29yZF' - '9zdGFydF9yZW1pbmRlcl9tZXNzYWdlQh8KHV9kaXNjb3JkX2VuZF9yZW1pbmRlcl9tZXNzYWdl'); + 'QgASgJSANSGWRpc2NvcmRFbmRSZW1pbmRlck1lc3NhZ2WIAQESTwoiZGlzY29yZF9hdXRvX2Rl' + 'bGV0ZV9zdGFydF9yZW1pbmRlchgFIAEoCEgEUh5kaXNjb3JkQXV0b0RlbGV0ZVN0YXJ0UmVtaW' + '5kZXKIAQESSwogZGlzY29yZF9hdXRvX2RlbGV0ZV9lbmRfcmVtaW5kZXIYBiABKAhIBVIcZGlz' + 'Y29yZEF1dG9EZWxldGVFbmRSZW1pbmRlcogBAUIeChxfZGlzY29yZF9zdGFydF9yZW1pbmRlcl' + '9taW5zQhwKGl9kaXNjb3JkX2VuZF9yZW1pbmRlcl9taW5zQiEKH19kaXNjb3JkX3N0YXJ0X3Jl' + 'bWluZGVyX21lc3NhZ2VCHwodX2Rpc2NvcmRfZW5kX3JlbWluZGVyX21lc3NhZ2VCJQojX2Rpc2' + 'NvcmRfYXV0b19kZWxldGVfc3RhcnRfcmVtaW5kZXJCIwohX2Rpc2NvcmRfYXV0b19kZWxldGVf' + 'ZW5kX3JlbWluZGVy'); @$core.Deprecated('Use updateDiscordReminderSettingsResponseDescriptor instead') const UpdateDiscordReminderSettingsResponse$json = { diff --git a/client/lib/generated/db/db.pb.dart b/client/lib/generated/db/db.pb.dart index e3902f8..ddd7119 100644 --- a/client/lib/generated/db/db.pb.dart +++ b/client/lib/generated/db/db.pb.dart @@ -579,12 +579,14 @@ class Notification extends $pb.GeneratedMessage { $core.String? sessionId, $core.String? teamMemberId, $core.bool? sent, + $core.String? discordMessageId, }) { final result = create(); if (notificationType != null) result.notificationType = notificationType; if (sessionId != null) result.sessionId = sessionId; if (teamMemberId != null) result.teamMemberId = teamMemberId; if (sent != null) result.sent = sent; + if (discordMessageId != null) result.discordMessageId = discordMessageId; return result; } @@ -606,6 +608,7 @@ class Notification extends $pb.GeneratedMessage { ..aOS(2, _omitFieldNames ? '' : 'sessionId') ..aOS(3, _omitFieldNames ? '' : 'teamMemberId') ..aOB(4, _omitFieldNames ? '' : 'sent') + ..aOS(5, _omitFieldNames ? '' : 'discordMessageId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -662,6 +665,15 @@ class Notification extends $pb.GeneratedMessage { $core.bool hasSent() => $_has(3); @$pb.TagNumber(4) void clearSent() => $_clearField(4); + + @$pb.TagNumber(5) + $core.String get discordMessageId => $_getSZ(4); + @$pb.TagNumber(5) + set discordMessageId($core.String value) => $_setString(4, value); + @$pb.TagNumber(5) + $core.bool hasDiscordMessageId() => $_has(4); + @$pb.TagNumber(5) + void clearDiscordMessageId() => $_clearField(5); } class Logo extends $pb.GeneratedMessage { @@ -889,6 +901,8 @@ class Settings extends $pb.GeneratedMessage { $core.Iterable? leaderboardMemberTypes, $core.bool? discordRsvpReactionsEnabled, $core.String? discordNotificationChannelId, + $core.bool? discordAutoDeleteStartReminder, + $core.bool? discordAutoDeleteEndReminder, }) { final result = create(); if (nextSessionThresholdSecs != null) @@ -933,6 +947,10 @@ class Settings extends $pb.GeneratedMessage { result.discordRsvpReactionsEnabled = discordRsvpReactionsEnabled; if (discordNotificationChannelId != null) result.discordNotificationChannelId = discordNotificationChannelId; + if (discordAutoDeleteStartReminder != null) + result.discordAutoDeleteStartReminder = discordAutoDeleteStartReminder; + if (discordAutoDeleteEndReminder != null) + result.discordAutoDeleteEndReminder = discordAutoDeleteEndReminder; return result; } @@ -977,6 +995,8 @@ class Settings extends $pb.GeneratedMessage { defaultEnumValue: TeamMemberType.STUDENT) ..aOB(23, _omitFieldNames ? '' : 'discordRsvpReactionsEnabled') ..aOS(24, _omitFieldNames ? '' : 'discordNotificationChannelId') + ..aOB(25, _omitFieldNames ? '' : 'discordAutoDeleteStartReminder') + ..aOB(26, _omitFieldNames ? '' : 'discordAutoDeleteEndReminder') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1208,6 +1228,24 @@ class Settings extends $pb.GeneratedMessage { $core.bool hasDiscordNotificationChannelId() => $_has(23); @$pb.TagNumber(24) void clearDiscordNotificationChannelId() => $_clearField(24); + + @$pb.TagNumber(25) + $core.bool get discordAutoDeleteStartReminder => $_getBF(24); + @$pb.TagNumber(25) + set discordAutoDeleteStartReminder($core.bool value) => $_setBool(24, value); + @$pb.TagNumber(25) + $core.bool hasDiscordAutoDeleteStartReminder() => $_has(24); + @$pb.TagNumber(25) + void clearDiscordAutoDeleteStartReminder() => $_clearField(25); + + @$pb.TagNumber(26) + $core.bool get discordAutoDeleteEndReminder => $_getBF(25); + @$pb.TagNumber(26) + set discordAutoDeleteEndReminder($core.bool value) => $_setBool(25, value); + @$pb.TagNumber(26) + $core.bool hasDiscordAutoDeleteEndReminder() => $_has(25); + @$pb.TagNumber(26) + void clearDiscordAutoDeleteEndReminder() => $_clearField(26); } const $core.bool _omitFieldNames = diff --git a/client/lib/generated/db/db.pbjson.dart b/client/lib/generated/db/db.pbjson.dart index 4af19cd..3b0dc18 100644 --- a/client/lib/generated/db/db.pbjson.dart +++ b/client/lib/generated/db/db.pbjson.dart @@ -268,9 +268,19 @@ const Notification$json = { '17': true }, {'1': 'sent', '3': 4, '4': 1, '5': 8, '10': 'sent'}, + { + '1': 'discord_message_id', + '3': 5, + '4': 1, + '5': 9, + '9': 1, + '10': 'discordMessageId', + '17': true + }, ], '8': [ {'1': '_team_member_id'}, + {'1': '_discord_message_id'}, ], }; @@ -279,7 +289,8 @@ final $typed_data.Uint8List notificationDescriptor = $convert.base64Decode( 'CgxOb3RpZmljYXRpb24SRAoRbm90aWZpY2F0aW9uX3R5cGUYASABKA4yFy50ay5kYi5Ob3RpZm' 'ljYXRpb25UeXBlUhBub3RpZmljYXRpb25UeXBlEh0KCnNlc3Npb25faWQYAiABKAlSCXNlc3Np' 'b25JZBIpCg50ZWFtX21lbWJlcl9pZBgDIAEoCUgAUgx0ZWFtTWVtYmVySWSIAQESEgoEc2VudB' - 'gEIAEoCFIEc2VudEIRCg9fdGVhbV9tZW1iZXJfaWQ='); + 'gEIAEoCFIEc2VudBIxChJkaXNjb3JkX21lc3NhZ2VfaWQYBSABKAlIAVIQZGlzY29yZE1lc3Nh' + 'Z2VJZIgBAUIRCg9fdGVhbV9tZW1iZXJfaWRCFQoTX2Rpc2NvcmRfbWVzc2FnZV9pZA=='); @$core.Deprecated('Use logoDescriptor instead') const Logo$json = { @@ -473,6 +484,20 @@ const Settings$json = { '5': 8, '10': 'discordRsvpReactionsEnabled' }, + { + '1': 'discord_auto_delete_start_reminder', + '3': 25, + '4': 1, + '5': 8, + '10': 'discordAutoDeleteStartReminder' + }, + { + '1': 'discord_auto_delete_end_reminder', + '3': 26, + '4': 1, + '5': 8, + '10': 'discordAutoDeleteEndReminder' + }, ], }; @@ -503,4 +528,7 @@ final $typed_data.Uint8List settingsDescriptor = $convert.base64Decode( 'ZF9zaG93X292ZXJ0aW1lGBUgASgIUhdsZWFkZXJib2FyZFNob3dPdmVydGltZRJPChhsZWFkZX' 'Jib2FyZF9tZW1iZXJfdHlwZXMYFiADKA4yFS50ay5kYi5UZWFtTWVtYmVyVHlwZVIWbGVhZGVy' 'Ym9hcmRNZW1iZXJUeXBlcxJDCh5kaXNjb3JkX3JzdnBfcmVhY3Rpb25zX2VuYWJsZWQYFyABKA' - 'hSG2Rpc2NvcmRSc3ZwUmVhY3Rpb25zRW5hYmxlZA=='); + 'hSG2Rpc2NvcmRSc3ZwUmVhY3Rpb25zRW5hYmxlZBJKCiJkaXNjb3JkX2F1dG9fZGVsZXRlX3N0' + 'YXJ0X3JlbWluZGVyGBkgASgIUh5kaXNjb3JkQXV0b0RlbGV0ZVN0YXJ0UmVtaW5kZXISRgogZG' + 'lzY29yZF9hdXRvX2RlbGV0ZV9lbmRfcmVtaW5kZXIYGiABKAhSHGRpc2NvcmRBdXRvRGVsZXRl' + 'RW5kUmVtaW5kZXI='); diff --git a/client/lib/views/setup/integrations_setup.dart b/client/lib/views/setup/integrations_setup.dart index f34a0f6..b22b3d8 100644 --- a/client/lib/views/setup/integrations_setup.dart +++ b/client/lib/views/setup/integrations_setup.dart @@ -27,6 +27,8 @@ class IntegrationsSetupTab extends HookConsumerWidget { final endReminderMinsController = useTextEditingController(); final startReminderMessageController = useTextEditingController(); final endReminderMessageController = useTextEditingController(); + final autoDeleteStartReminder = useState(false); + final autoDeleteEndReminder = useState(false); final rsvpReactionsEnabled = useState(true); final selfLinkEnabled = useState(false); final nameSyncEnabled = useState(true); @@ -67,6 +69,8 @@ class IntegrationsSetupTab extends HookConsumerWidget { : '15'; startReminderMessageController.text = s.discordStartReminderMessage; endReminderMessageController.text = s.discordEndReminderMessage; + autoDeleteStartReminder.value = s.discordAutoDeleteStartReminder; + autoDeleteEndReminder.value = s.discordAutoDeleteEndReminder; rsvpReactionsEnabled.value = s.discordRsvpReactionsEnabled; selfLinkEnabled.value = s.discordSelfLinkEnabled; nameSyncEnabled.value = s.discordNameSyncEnabled; @@ -123,6 +127,9 @@ class IntegrationsSetupTab extends HookConsumerWidget { discordStartReminderMessage: startReminderMessageController.text, discordEndReminderMessage: endReminderMessageController.text, + discordAutoDeleteStartReminder: + autoDeleteStartReminder.value, + discordAutoDeleteEndReminder: autoDeleteEndReminder.value, ), ), ); @@ -312,6 +319,25 @@ class IntegrationsSetupTab extends HookConsumerWidget { onUpdate: updateDiscordReminder, ), const SizedBox(height: 24), + SettingRow( + label: 'Auto Delete Start Reminders', + description: + 'Automatically delete the start reminder message from Discord once the session start time has passed.', + child: Row( + children: [ + Switch( + value: autoDeleteStartReminder.value, + onChanged: (value) { + autoDeleteStartReminder.value = value; + updateDiscordReminder(); + }, + ), + const SizedBox(width: 8), + Text(autoDeleteStartReminder.value ? 'Enabled' : 'Disabled'), + ], + ), + ), + const SizedBox(height: 24), SettingRow( label: 'Start Reminder RSVP Reactions', description: @@ -354,6 +380,25 @@ class IntegrationsSetupTab extends HookConsumerWidget { onUpdate: updateDiscordReminder, ), const SizedBox(height: 24), + SettingRow( + label: 'Auto Delete End Reminders', + description: + 'Automatically delete the end reminder message from Discord once the session end time has passed.', + child: Row( + children: [ + Switch( + value: autoDeleteEndReminder.value, + onChanged: (value) { + autoDeleteEndReminder.value = value; + updateDiscordReminder(); + }, + ), + const SizedBox(width: 8), + Text(autoDeleteEndReminder.value ? 'Enabled' : 'Disabled'), + ], + ), + ), + const SizedBox(height: 24), SettingRow( label: 'Self-Linking', description: diff --git a/protos/api/settings.proto b/protos/api/settings.proto index a7cb0c3..5ef919b 100644 --- a/protos/api/settings.proto +++ b/protos/api/settings.proto @@ -98,6 +98,8 @@ message UpdateDiscordReminderSettingsRequest { optional int64 discord_end_reminder_mins = 2; optional string discord_start_reminder_message = 3; optional string discord_end_reminder_message = 4; + optional bool discord_auto_delete_start_reminder = 5; + optional bool discord_auto_delete_end_reminder = 6; } message UpdateDiscordReminderSettingsResponse {} diff --git a/protos/db/db.proto b/protos/db/db.proto index b09a6c0..4a6d46f 100644 --- a/protos/db/db.proto +++ b/protos/db/db.proto @@ -63,6 +63,7 @@ message Notification { string session_id = 2; optional string team_member_id = 3; bool sent = 4; + optional string discord_message_id = 5; } message Logo { @@ -110,4 +111,6 @@ message Settings { bool leaderboard_show_overtime = 21; repeated TeamMemberType leaderboard_member_types = 22; bool discord_rsvp_reactions_enabled = 23; + bool discord_auto_delete_start_reminder = 25; + bool discord_auto_delete_end_reminder = 26; } diff --git a/server/src/generated/tk.api.rs b/server/src/generated/tk.api.rs index c234d24..dec379c 100644 --- a/server/src/generated/tk.api.rs +++ b/server/src/generated/tk.api.rs @@ -3258,6 +3258,10 @@ pub struct UpdateDiscordReminderSettingsRequest { pub discord_end_reminder_message: ::core::option::Option< ::prost::alloc::string::String, >, + #[prost(bool, optional, tag = "5")] + pub discord_auto_delete_start_reminder: ::core::option::Option, + #[prost(bool, optional, tag = "6")] + pub discord_auto_delete_end_reminder: ::core::option::Option, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct UpdateDiscordReminderSettingsResponse {} diff --git a/server/src/generated/tk.db.rs b/server/src/generated/tk.db.rs index 0674472..5441358 100644 --- a/server/src/generated/tk.db.rs +++ b/server/src/generated/tk.db.rs @@ -72,6 +72,8 @@ pub struct Notification { pub team_member_id: ::core::option::Option<::prost::alloc::string::String>, #[prost(bool, tag = "4")] pub sent: bool, + #[prost(string, optional, tag = "5")] + pub discord_message_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Logo { @@ -144,6 +146,10 @@ pub struct Settings { pub leaderboard_member_types: ::prost::alloc::vec::Vec, #[prost(bool, tag = "23")] pub discord_rsvp_reactions_enabled: bool, + #[prost(bool, tag = "25")] + pub discord_auto_delete_start_reminder: bool, + #[prost(bool, tag = "26")] + pub discord_auto_delete_end_reminder: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] diff --git a/server/src/modules/discord/service.rs b/server/src/modules/discord/service.rs index ce414f2..9284e1a 100644 --- a/server/src/modules/discord/service.rs +++ b/server/src/modules/discord/service.rs @@ -126,83 +126,142 @@ impl ScheduledService for DiscordNotificationService { // --- Session Start/End Reminders --- for (id, session) in &sessions { - if session.finished { - continue; - } - let start_secs = session.start_time.as_ref().map_or(0, |t| t.seconds); let end_secs = session.end_time.as_ref().map_or(0, |t| t.seconds); - let location = locations.get(&session.location_id).map_or("Unknown", |l| l.location.as_str()); - - // Session starting soon reminder - if start_reminder_secs > 0 { - let time_until_start = start_secs - now_secs; - if time_until_start > 0 - && time_until_start <= start_reminder_secs - && !Notification::exists(NotificationType::SessionStartReminder, id, None)? - { - let mins = time_until_start / 60; - let msg = DiscordNotificationService::replace_placeholders( - start_msg_template, - location, - start_secs, - end_secs, - tz, - Some(mins), - ); - - match block_on(async { announcement_channel.say(&http, &msg).await }) { - Ok(sent_msg) => { - if settings.discord_rsvp_reactions_enabled { - let _ = block_on(async { sent_msg.react(&http, ReactionType::Unicode("👍".to_string())).await }); - let _ = block_on(async { sent_msg.react(&http, ReactionType::Unicode("👎".to_string())).await }); - - if let Err(e) = SessionRsvpMessage::set(&sent_msg.id.to_string(), id) { - log::error!("[DiscordNotificationService] Failed to store RSVP message mapping: {e}"); + + // Send reminders only for sessions that haven't finished + if !session.finished { + let location = locations.get(&session.location_id).map_or("Unknown", |l| l.location.as_str()); + + // Session starting soon reminder + if start_reminder_secs > 0 { + let time_until_start = start_secs - now_secs; + if time_until_start > 0 + && time_until_start <= start_reminder_secs + && !Notification::exists(NotificationType::SessionStartReminder, id, None)? + { + let mins = time_until_start / 60; + let msg = DiscordNotificationService::replace_placeholders( + start_msg_template, + location, + start_secs, + end_secs, + tz, + Some(mins), + ); + + match block_on(async { announcement_channel.say(&http, &msg).await }) { + Ok(sent_msg) => { + let discord_message_id = sent_msg.id.to_string(); + + if settings.discord_rsvp_reactions_enabled { + let _ = block_on(async { sent_msg.react(&http, ReactionType::Unicode("👍".to_string())).await }); + let _ = block_on(async { sent_msg.react(&http, ReactionType::Unicode("👎".to_string())).await }); + + if let Err(e) = SessionRsvpMessage::set(&discord_message_id, id) { + log::error!("[DiscordNotificationService] Failed to store RSVP message mapping: {e}"); + } } - } - Notification::add(&Notification { - notification_type: NotificationType::SessionStartReminder as i32, - session_id: id.clone(), - team_member_id: None, - sent: true, - })?; + Notification::add(&Notification { + notification_type: NotificationType::SessionStartReminder as i32, + session_id: id.clone(), + team_member_id: None, + sent: true, + discord_message_id: Some(discord_message_id), + })?; + } + Err(e) => { + log::error!("[DiscordNotificationService] Failed to send start reminder: {e}"); + } } - Err(e) => { - log::error!("[DiscordNotificationService] Failed to send start reminder: {e}"); + } + } + + // Session ending soon reminder + if end_reminder_secs > 0 { + let time_until_end = end_secs - now_secs; + if time_until_end > 0 + && time_until_end <= end_reminder_secs + && now_secs >= start_secs + && !Notification::exists(NotificationType::SessionEndReminder, id, None)? + { + let mins = time_until_end / 60; + let msg = DiscordNotificationService::replace_placeholders( + end_msg_template, + location, + start_secs, + end_secs, + tz, + Some(mins), + ); + + match block_on(async { announcement_channel.say(&http, &msg).await }) { + Ok(sent_msg) => { + Notification::add(&Notification { + notification_type: NotificationType::SessionEndReminder as i32, + session_id: id.clone(), + team_member_id: None, + sent: true, + discord_message_id: Some(sent_msg.id.to_string()), + })?; + } + Err(e) => { + log::error!("[DiscordNotificationService] Failed to send end reminder: {e}"); + } } } } } - // Session ending soon reminder - if end_reminder_secs > 0 { - let time_until_end = end_secs - now_secs; - if time_until_end > 0 - && time_until_end <= end_reminder_secs - && now_secs >= start_secs - && !Notification::exists(NotificationType::SessionEndReminder, id, None)? - { - let mins = time_until_end / 60; - let msg = DiscordNotificationService::replace_placeholders( - end_msg_template, - location, - start_secs, - end_secs, - tz, - Some(mins), - ); - - if let Err(e) = block_on(async { announcement_channel.say(&http, &msg).await }) { - log::error!("[DiscordNotificationService] Failed to send end reminder: {e}"); - } else { - Notification::add(&Notification { - notification_type: NotificationType::SessionEndReminder as i32, - session_id: id.clone(), - team_member_id: None, - sent: true, - })?; + // Auto-delete start reminder when session has started (time-based, independent of finished state) + if settings.discord_auto_delete_start_reminder && start_secs > 0 && now_secs >= start_secs { + let session_notifs = Notification::get_by_session_id(id)?; + if let Some((notif_id, notif)) = session_notifs.into_iter().find(|(_, n)| { + n.notification_type == NotificationType::SessionStartReminder as i32 && n.discord_message_id.is_some() + }) { + let discord_msg_id = notif.discord_message_id.as_ref().unwrap(); + let msg_id: u64 = discord_msg_id.parse().unwrap_or(0); + if msg_id > 0 + && let Err(e) = block_on(async { + announcement_channel + .delete_message(&http, serenity::model::id::MessageId::new(msg_id)) + .await + }) + { + log::warn!("[DiscordNotificationService] Failed to delete start reminder message {discord_msg_id}: {e}"); + } + // Clear the message ID so we don't attempt deletion again + let mut updated = notif.clone(); + updated.discord_message_id = None; + if let Err(e) = Notification::update(¬if_id, &updated) { + log::error!("[DiscordNotificationService] Failed to clear start reminder message ID: {e}"); + } + } + } + + // Auto-delete end reminder when session has ended (time-based, independent of finished state) + if settings.discord_auto_delete_end_reminder && end_secs > 0 && now_secs >= end_secs { + let session_notifs = Notification::get_by_session_id(id)?; + if let Some((notif_id, notif)) = session_notifs.into_iter().find(|(_, n)| { + n.notification_type == NotificationType::SessionEndReminder as i32 && n.discord_message_id.is_some() + }) { + let discord_msg_id = notif.discord_message_id.as_ref().unwrap(); + let msg_id: u64 = discord_msg_id.parse().unwrap_or(0); + if msg_id > 0 + && let Err(e) = block_on(async { + announcement_channel + .delete_message(&http, serenity::model::id::MessageId::new(msg_id)) + .await + }) + { + log::warn!("[DiscordNotificationService] Failed to delete end reminder message {discord_msg_id}: {e}"); + } + // Clear the message ID so we don't attempt deletion again + let mut updated = notif.clone(); + updated.discord_message_id = None; + if let Err(e) = Notification::update(¬if_id, &updated) { + log::error!("[DiscordNotificationService] Failed to clear end reminder message ID: {e}"); } } } @@ -271,6 +330,7 @@ impl ScheduledService for DiscordNotificationService { session_id: pes.session_id.clone(), team_member_id: Some(ms.team_member_id.clone()), sent: true, + discord_message_id: None, }) { log::error!( diff --git a/server/src/modules/notification/api.rs b/server/src/modules/notification/api.rs index 6348c23..ab532be 100644 --- a/server/src/modules/notification/api.rs +++ b/server/src/modules/notification/api.rs @@ -108,6 +108,7 @@ impl NotificationService for NotificationApi { session_id: req.session_id, team_member_id: req.team_member_id, sent: req.sent, + discord_message_id: None, }; Notification::add(¬ification).map_err(|e| Status::internal(format!("Failed to create notification: {e}")))?; @@ -137,6 +138,7 @@ impl NotificationService for NotificationApi { session_id: req.session_id, team_member_id: req.team_member_id, sent: req.sent, + discord_message_id: None, }; Notification::update(&req.id, ¬ification) .map_err(|e| Status::internal(format!("Failed to update notification: {e}")))?; diff --git a/server/src/modules/session/service.rs b/server/src/modules/session/service.rs index c318f4e..6090092 100644 --- a/server/src/modules/session/service.rs +++ b/server/src/modules/session/service.rs @@ -58,6 +58,7 @@ impl ScheduledService for SessionService { session_id: pes.session_id.clone(), team_member_id: Some(ms.team_member_id.clone()), sent: false, + discord_message_id: None, }; if let Err(e) = Notification::add(¬ification) { log::error!( diff --git a/server/src/modules/settings/api.rs b/server/src/modules/settings/api.rs index d5e2d55..369a2f9 100644 --- a/server/src/modules/settings/api.rs +++ b/server/src/modules/settings/api.rs @@ -190,6 +190,14 @@ impl SettingsService for SettingsApi { settings.discord_end_reminder_message = v; } + if let Some(v) = req.discord_auto_delete_start_reminder { + settings.discord_auto_delete_start_reminder = v; + } + + if let Some(v) = req.discord_auto_delete_end_reminder { + settings.discord_auto_delete_end_reminder = v; + } + Self::save_settings(&settings)?; Ok(Response::new(UpdateDiscordReminderSettingsResponse {})) } diff --git a/server/src/modules/settings/repository.rs b/server/src/modules/settings/repository.rs index 5d16c87..c7310ff 100644 --- a/server/src/modules/settings/repository.rs +++ b/server/src/modules/settings/repository.rs @@ -83,6 +83,8 @@ impl SettingsRepository for Settings { leaderboard_show_overtime: true, leaderboard_member_types: vec![TeamMemberType::Student as i32, TeamMemberType::Mentor as i32], discord_rsvp_reactions_enabled: true, + discord_auto_delete_start_reminder: false, + discord_auto_delete_end_reminder: false, }; if let Some(s) = table.get::(SETTINGS_KEY)? {