Skip to content
This repository was archived by the owner on Mar 24, 2024. It is now read-only.

Commit 0418657

Browse files
committed
add time-based notification clearing
1 parent e673e91 commit 0418657

File tree

8 files changed

+166
-39
lines changed

8 files changed

+166
-39
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "highlights"
3-
version = "2.1.4"
3+
version = "2.1.5"
44
authors = ["ThatsNoMoon <git@thatsnomoon.dev>"]
55
repository = "https://github.com/ThatsNoMoon/highlights"
66
license = "OSL-3.0"

example_config.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ max_keywords = 100
1515
# Amount of time to wait for activity before sending a notification
1616
# Other examples: "1m 30sec", "5minutes"
1717
# See https://docs.rs/humantime/latest/humantime/fn.parse_duration.html for complete list
18-
patience = "2m"
18+
patience = "2min"
19+
# Amount of time to leave notifications visible
20+
# This uses the same format as patience
21+
# Other examples: "1y", "90d", "1M" (one month)
22+
#patience_lifetime = "1month"
1923

2024
[logging]
2125
# Discord webhook to send errors and panics to

src/bot/commands/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use crate::{
4949
};
5050

5151
// Create all slash commands globally, and in a test guild if configured.
52-
pub(crate) async fn create_commands(ctx: Context) -> Result<()> {
52+
pub(crate) async fn create_commands(ctx: &Context) -> Result<()> {
5353
debug!("Registering slash commands");
5454
let commands = COMMAND_INFO
5555
.iter()
@@ -59,13 +59,13 @@ pub(crate) async fn create_commands(ctx: Context) -> Result<()> {
5959
debug!("Registering commands in test guild");
6060

6161
guild
62-
.set_application_commands(&ctx, |create| {
62+
.set_application_commands(ctx, |create| {
6363
create.set_application_commands(commands.clone())
6464
})
6565
.await
6666
.context("Failed to create guild application commands")?;
6767
}
68-
ApplicationCommand::set_global_application_commands(&ctx, |create| {
68+
ApplicationCommand::set_global_application_commands(ctx, |create| {
6969
create.set_application_commands(commands)
7070
})
7171
.await

src/bot/highlighting.rs

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
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+
};
1014
use indoc::indoc;
1115
use lazy_regex::regex;
1216
use 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
};
2428
use 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

2835
use 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)]
707780
mod tests {
708781
use super::*;

src/bot/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use tracing::{
4545

4646
use self::highlighting::CachedMessages;
4747
use crate::{
48+
bot::highlighting::start_notification_clearing,
4849
db::{Ignore, Keyword, Notification},
4950
global::ERROR_COLOR,
5051
settings::settings,
@@ -140,12 +141,14 @@ async fn ready(ctx: Context) {
140141

141142
ctx.set_activity(Activity::listening("/help")).await;
142143

143-
if let Err(e) = commands::create_commands(ctx).await {
144+
if let Err(e) = commands::create_commands(&ctx).await {
144145
error!("{e}\n{e:?}");
145146
}
146147

147148
let _ = STARTED.set(Instant::now());
148149

150+
start_notification_clearing(ctx);
151+
149152
info!("Ready to highlight!");
150153
}
151154

@@ -230,7 +233,7 @@ async fn handle_update(
230233
}
231234

232235
/// Finds notifications for a deleted message and uses
233-
/// [`delete_sent_notifications`](highlighting::delete_sent_notifications) to
236+
/// [`delete_sent_notifications`](highlighting::clear_sent_notifications) to
234237
/// delete them.
235238
async fn handle_deletion(
236239
ctx: Context,
@@ -273,7 +276,7 @@ async fn handle_deletion(
273276
return;
274277
}
275278

276-
highlighting::delete_sent_notifications(&ctx, &notifications).await;
279+
highlighting::clear_sent_notifications(&ctx, &notifications).await;
277280

278281
if let Err(e) =
279282
Notification::delete_notifications_of_message(message_id).await

src/db/notification.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
// Copyright 2022 ThatsNoMoon
1+
// Copyright 2023 ThatsNoMoon
22
// Licensed under the Open Software License version 3.0
33

44
//! Handling for sent notification messages.
55
6+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
7+
68
use anyhow::Result;
79
use futures_util::TryStreamExt;
810
use sea_orm::{
@@ -16,6 +18,8 @@ use serenity::model::id::{MessageId, UserId};
1618

1719
use super::{connection, DbInt, IdDbExt};
1820

21+
const DISCORD_EPOCH: u64 = 1420070400000;
22+
1923
#[derive(
2024
Clone, Debug, PartialEq, Eq, DeriveEntityModel, DeriveActiveModelBehavior,
2125
)]
@@ -104,6 +108,40 @@ impl Notification {
104108

105109
Ok(())
106110
}
111+
112+
/// Gets notifications older than a certain duration from the DB.
113+
#[tracing::instrument]
114+
pub(crate) async fn old_notifications(
115+
age: Duration,
116+
) -> Result<Vec<Notification>> {
117+
Entity::find()
118+
.filter(Column::OriginalMessage.lte(age_to_oldest_snowflake(age)?))
119+
.stream(connection())
120+
.await?
121+
.map_err(Into::into)
122+
.map_ok(Notification::from)
123+
.try_collect()
124+
.await
125+
}
126+
127+
/// Deletes notifications older than a certain duration from the DB.
128+
#[tracing::instrument]
129+
pub(crate) async fn delete_old_notifications(age: Duration) -> Result<()> {
130+
Entity::delete_many()
131+
.filter(Column::OriginalMessage.lte(age_to_oldest_snowflake(age)?))
132+
.exec(connection())
133+
.await?;
134+
135+
Ok(())
136+
}
137+
}
138+
139+
fn age_to_oldest_snowflake(age: Duration) -> Result<u64> {
140+
let millis = age.as_millis() as u64;
141+
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
142+
let oldest_unix = now - millis;
143+
let oldest_discord = oldest_unix - DISCORD_EPOCH;
144+
Ok(oldest_discord << 22)
107145
}
108146

109147
impl From<Model> for Notification {

src/settings.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,15 @@ pub(crate) struct BehaviorSettings {
258258
#[cfg(feature = "bot")]
259259
pub(crate) patience: Duration,
260260

261+
/// Duration to wait before deleting notifications.
262+
#[serde(
263+
alias = "notificationlifetime",
264+
with = "humantime_serde::option",
265+
default
266+
)]
267+
#[cfg(feature = "bot")]
268+
pub(crate) notification_lifetime: Option<Duration>,
269+
261270
/// Deprecated method to specify patience.
262271
#[serde(
263272
deserialize_with = "deserialize_duration",

0 commit comments

Comments
 (0)