1- // Copyright 2022 ThatsNoMoon
1+ // Copyright 2023 ThatsNoMoon
22// Licensed under the Open Software License version 3.0
33
44//! Functions for sending, editing, and deleting notifications.
55
6- use std:: { collections:: HashMap , fmt:: Write as _, ops:: Range , time:: Duration } ;
6+ use std:: {
7+ cmp:: min, collections:: HashMap , fmt:: Write as _, ops:: Range , time:: Duration ,
8+ } ;
79
8- use anyhow:: { anyhow, bail, Context as _, Result } ;
9- use futures_util:: { stream, StreamExt , TryStreamExt } ;
10+ use anyhow:: { anyhow, bail, Context as _, Error , Result } ;
11+ use futures_util:: {
12+ stream, stream:: FuturesUnordered , StreamExt , TryFutureExt , TryStreamExt ,
13+ } ;
1014use indoc:: indoc;
1115use lazy_regex:: regex;
1216use serenity:: {
1317 builder:: { CreateEmbed , CreateMessage , EditMessage } ,
1418 client:: Context ,
15- http:: { error:: ErrorResponse , HttpError } ,
19+ http:: { error:: ErrorResponse , HttpError , StatusCode } ,
1620 model:: {
1721 application:: interaction:: application_command:: ApplicationCommandInteraction as Command ,
1822 channel:: { Channel , Message } ,
@@ -22,8 +26,11 @@ use serenity::{
2226 Error as SerenityError ,
2327} ;
2428use tinyvec:: TinyVec ;
25- use tokio:: { select, time:: sleep} ;
26- use tracing:: { debug, error} ;
29+ use tokio:: {
30+ select,
31+ time:: { interval, sleep} ,
32+ } ;
33+ use tracing:: { debug, error, info_span} ;
2734
2835use crate :: {
2936 bot:: util:: { followup_eph, user_can_read_channel} ,
@@ -191,7 +198,7 @@ pub(crate) async fn notify_keywords(
191198 message. content = content;
192199
193200 let keywords = stream:: iter ( keywords)
194- . map ( Ok :: < _ , anyhow :: Error > ) // convert to a TryStream
201+ . map ( Ok :: < _ , Error > ) // convert to a TryStream
195202 . try_filter_map ( |keyword| async {
196203 Ok ( should_notify_keyword (
197204 & ctx,
@@ -473,34 +480,44 @@ async fn send_notification_message(
473480
474481/// Deletes the given notification messages sent to the corresponding users.
475482#[ tracing:: instrument( skip( ctx) ) ]
476- pub ( crate ) async fn delete_sent_notifications (
483+ pub ( crate ) async fn clear_sent_notifications (
477484 ctx : & Context ,
478485 notification_messages : & [ ( UserId , MessageId ) ] ,
479486) {
480- for ( user_id, message_id) in notification_messages {
481- let result: Result < ( ) > = async {
482- let dm_channel = user_id. create_dm_channel ( ctx) . await ?;
483-
484- dm_channel
485- . edit_message ( ctx, message_id, |m| {
486- m. embed ( |e| {
487- e. description ( "*Original message deleted*" )
488- . color ( ERROR_COLOR )
489- } )
490- } )
491- . await
492- . context ( "Failed to edit notification message" ) ?;
493-
494- Ok ( ( ) )
495- }
496- . await ;
497-
498- if let Err ( e) = result {
487+ for & ( user_id, message_id) in notification_messages {
488+ if let Err ( e) = clear_sent_notification (
489+ ctx,
490+ user_id,
491+ message_id,
492+ "*Original message deleted*" ,
493+ )
494+ . await
495+ {
499496 error ! ( "{:?}" , e) ;
500497 }
501498 }
502499}
503500
501+ /// Replaces the given direct message with the given placeholder.
502+ #[ tracing:: instrument( skip( ctx, placeholder) ) ]
503+ async fn clear_sent_notification (
504+ ctx : & Context ,
505+ user_id : UserId ,
506+ message_id : MessageId ,
507+ placeholder : impl ToString ,
508+ ) -> Result < ( ) > {
509+ let dm_channel = user_id. create_dm_channel ( ctx) . await ?;
510+
511+ dm_channel
512+ . edit_message ( ctx, message_id, |m| {
513+ m. embed ( |e| e. description ( placeholder) . color ( ERROR_COLOR ) )
514+ } )
515+ . await
516+ . context ( "Failed to edit notification message" ) ?;
517+
518+ Ok ( ( ) )
519+ }
520+
504521/// Updates sent notifications after a message edit.
505522///
506523/// Edits the content of each notification to reflect the new content of the
@@ -576,7 +593,7 @@ pub(crate) async fn update_sent_notifications(
576593 }
577594 }
578595
579- delete_sent_notifications ( ctx, & to_delete) . await ;
596+ clear_sent_notifications ( ctx, & to_delete) . await ;
580597
581598 for ( _, notification_message) in to_delete {
582599 if let Err ( e) =
@@ -703,6 +720,62 @@ pub(crate) async fn warn_for_failed_dm(
703720 . await
704721}
705722
723+ pub ( super ) fn start_notification_clearing ( ctx : Context ) {
724+ if let Some ( lifetime) = settings ( ) . behavior . notification_lifetime {
725+ debug ! ( "Starting notification clearing" ) ;
726+ tokio:: spawn ( async move {
727+ let span = info_span ! ( parent: None , "notification_clearing" ) ;
728+ let _entered = span. enter ( ) ;
729+ let step = min ( lifetime / 2 , Duration :: from_secs ( 60 * 60 ) ) ;
730+ let mut timer = interval ( step) ;
731+ loop {
732+ if let Err ( e) = clear_old_notifications ( & ctx, lifetime) . await {
733+ error ! ( "Failed to clear old notifications: {e}\n {e:?}" ) ;
734+ }
735+ timer. tick ( ) . await ;
736+ }
737+ } ) ;
738+ }
739+ }
740+
741+ async fn clear_old_notifications (
742+ ctx : & Context ,
743+ lifetime : Duration ,
744+ ) -> Result < ( ) > {
745+ debug ! ( "Clearing old notifications" ) ;
746+ Notification :: old_notifications ( lifetime)
747+ . await ?
748+ . into_iter ( )
749+ . map ( |notification| {
750+ clear_sent_notification (
751+ ctx,
752+ notification. user_id ,
753+ notification. notification_message ,
754+ "*Notification expired*" ,
755+ )
756+ . or_else ( |e| async move {
757+ match e. downcast_ref :: < SerenityError > ( ) {
758+ Some ( SerenityError :: Http ( inner) ) => match & * * inner {
759+ HttpError :: UnsuccessfulRequest ( ErrorResponse {
760+ status_code : StatusCode :: NOT_FOUND ,
761+ ..
762+ } ) => Ok ( ( ) ) ,
763+
764+ _ => Err ( e) ,
765+ } ,
766+ _ => Err ( e) ,
767+ }
768+ } )
769+ } )
770+ . collect :: < FuturesUnordered < _ > > ( )
771+ . try_for_each ( |_| async { Ok ( ( ) ) } )
772+ . await ?;
773+
774+ Notification :: delete_old_notifications ( lifetime) . await ?;
775+
776+ Ok ( ( ) )
777+ }
778+
706779#[ cfg( test) ]
707780mod tests {
708781 use super :: * ;
0 commit comments