Expire calls where one of both parties doesn't talk for 10 minutes.

This commit is contained in:
2025-08-25 18:24:16 -03:00
parent a75fd9f092
commit 6d8217ed81
10 changed files with 252 additions and 26 deletions
Generated
+1 -1
View File
@@ -1990,7 +1990,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "payphone" name = "payphone"
version = "4.3.7" version = "4.3.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "payphone" name = "payphone"
version = "4.3.7" version = "4.3.10"
edition = "2024" edition = "2024"
default-run = "payphone" default-run = "payphone"
+40 -15
View File
@@ -19,6 +19,7 @@ use crate::{
components::Component, registry::ComponentRegistry, runner::InteractionRunner, components::Component, registry::ComponentRegistry, runner::InteractionRunner,
}, },
runtime::Runtime, runtime::Runtime,
tasks::Task,
}, },
utils::Bye, utils::Bye,
}; };
@@ -92,25 +93,26 @@ where
} }
} }
pub struct Bot<T: Send + Sync, E: Send + Sync = ()> { pub struct Bot<S: Send + Sync, E: Send + Sync = ()> {
shard: ShardId, shard: ShardId,
state: Arc<T>, state: Arc<S>,
presence: Option<UpdatePresencePayload>, presence: Option<UpdatePresencePayload>,
prefixes: Arc<dyn PrefixGetter<T, E> + Send + Sync>, prefixes: Arc<dyn PrefixGetter<S, E> + Send + Sync>,
intents: Intents, intents: Intents,
extras: Arc<dyn ExtrasGetter<T, Extras = E>>, extras: Arc<dyn ExtrasGetter<S, Extras = E>>,
cached_resource_types: Vec<ResourceType>, cached_resource_types: Vec<ResourceType>,
commands: Vec<Arc<dyn Command<T, E>>>, commands: Vec<Arc<dyn Command<S, E>>>,
components: Vec<Arc<dyn Component<T, E>>>, components: Vec<Arc<dyn Component<S, E>>>,
on_ready_handlers: Vec<Arc<dyn OnReadyHandler<T, E>>>, tasks: Vec<Arc<dyn Task<S, E>>>,
on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<T, E>>>, on_ready_handlers: Vec<Arc<dyn OnReadyHandler<S, E>>>,
on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<T, E>>>, on_reconnect_handlers: Vec<Arc<dyn OnReconnectHandler<S, E>>>,
on_message_handlers: Vec<Arc<dyn OnMessageHandler<T, E>>>, on_interaction_handlers: Vec<Arc<dyn OnInteractionHandler<S, E>>>,
on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<T, E>>>, on_message_handlers: Vec<Arc<dyn OnMessageHandler<S, E>>>,
on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<T, E>>>, on_message_edit_handlers: Vec<Arc<dyn OnMessageEditHandler<S, E>>>,
on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<T, E>>>, on_message_delete_handlers: Vec<Arc<dyn OnMessageDeleteHandler<S, E>>>,
on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<T, E>>>, on_server_create_handlers: Vec<Arc<dyn OnServerCreateHandler<S, E>>>,
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<T, E>>>, on_server_update_handlers: Vec<Arc<dyn OnServerUpdateHandler<S, E>>>,
on_server_delete_handlers: Vec<Arc<dyn OnServerDeleteHandler<S, E>>>,
} }
impl<T, E> Bot<T, E> impl<T, E> Bot<T, E>
@@ -133,6 +135,7 @@ where
cached_resource_types: Vec::new(), cached_resource_types: Vec::new(),
commands: Vec::new(), commands: Vec::new(),
components: Vec::new(), components: Vec::new(),
tasks: Vec::new(),
on_ready_handlers: Vec::new(), on_ready_handlers: Vec::new(),
on_reconnect_handlers: Vec::new(), on_reconnect_handlers: Vec::new(),
on_interaction_handlers: vec![Arc::new(InteractionRunner)], on_interaction_handlers: vec![Arc::new(InteractionRunner)],
@@ -170,6 +173,11 @@ where
self self
} }
pub fn task(mut self, task: impl Task<T, E> + 'static) -> Self {
self.tasks.push(Arc::new(task));
self
}
pub fn cache_resource_type(mut self, resource_type: ResourceType) -> Self { pub fn cache_resource_type(mut self, resource_type: ResourceType) -> Self {
if !self.cached_resource_types.contains(&resource_type) { if !self.cached_resource_types.contains(&resource_type) {
self.cached_resource_types.push(resource_type); self.cached_resource_types.push(resource_type);
@@ -273,6 +281,23 @@ where
cache, cache,
}); });
for task in bot.tasks.clone() {
let task_rt = runtime.clone();
tokio::spawn(async move {
loop {
task.run(&task_rt)
.await
.inspect_err(|e| {
log::error!("An error occurred while running task {task:?}! {e:?}")
})
.bye();
tokio::time::sleep(task.delay()).await;
}
});
}
let mut config = ConfigBuilder::new(token.to_string(), bot.intents); let mut config = ConfigBuilder::new(token.to_string(), bot.intents);
if let Some(presence) = &bot.presence { if let Some(presence) = &bot.presence {
+1
View File
@@ -161,3 +161,4 @@ pub mod events;
pub mod interactions; pub mod interactions;
pub mod messages; pub mod messages;
pub mod runtime; pub mod runtime;
pub mod tasks;
+12
View File
@@ -0,0 +1,12 @@
use std::{fmt::Debug, time::Duration};
use async_trait::async_trait;
use crate::framework::runtime::Runtime;
#[async_trait]
pub trait Task<S, E>: Send + Sync + Debug {
fn delay(&self) -> Duration;
async fn run(&self, rt: &Runtime<S, E>) -> anyhow::Result<()>;
}
+73 -1
View File
@@ -1,5 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use deadpool_redis::redis::AsyncTypedCommands;
use scc::HashSet; use scc::HashSet;
use time::UtcDateTime;
use twilight_model::id::Id; use twilight_model::id::Id;
use crate::{ use crate::{
@@ -115,12 +117,32 @@ impl Command<State, Extras> for CallCommand {
)) ))
.await .await
.bye(); .bye();
ctx.rt ctx.rt
.http .http
.create_message(Id::new(call.other_party_channel_id(channel.id))) .create_message(Id::new(call.other_party_channel_id(channel.id)))
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix))) .content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
.await .await
.bye(); .bye();
let mut redis = ctx.rt.state.redis.get().await?;
let timestamp = UtcDateTime::now().unix_timestamp();
redis
.zadd(
"active-calls",
format!("{}:{}", call.id, channel.id),
timestamp,
)
.await?;
redis
.zadd(
"active-calls",
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
timestamp,
)
.await?;
} else { } else {
ctx.send() ctx.send()
.content(format!( .content(format!(
@@ -215,6 +237,19 @@ impl Command<State, Extras> for SkipCommand {
log::error!("Failed to send message to other party: {e:?}"); log::error!("Failed to send message to other party: {e:?}");
})?; })?;
let mut redis = ctx.rt.state.redis.get().await?;
let channel_keys = [
format!("{}:{}", call.id, call.channel_ids[0]),
format!("{}:{}", call.id, call.channel_ids[1]),
];
redis
.zrem("active-calls", &channel_keys)
.await
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
.ok();
let call = Call::start(&channel, &ctx.rt.state.db) let call = Call::start(&channel, &ctx.rt.state.db)
.await .await
.inspect_err(|e| { .inspect_err(|e| {
@@ -236,6 +271,25 @@ impl Command<State, Extras> for SkipCommand {
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix))) .content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
.await .await
.bye(); .bye();
let mut redis = ctx.rt.state.redis.get().await?;
let timestamp = UtcDateTime::now().unix_timestamp();
redis
.zadd(
"active-calls",
format!("{}:{}", call.id, channel.id),
timestamp,
)
.await?;
redis
.zadd(
"active-calls",
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
timestamp,
)
.await?;
} else { } else {
ctx.send() ctx.send()
.content(format!( .content(format!(
@@ -304,6 +358,8 @@ impl Command<State, Extras> for HangupCommand {
} }
if let Some(call_id) = channel.call_id { if let Some(call_id) = channel.call_id {
let mut redis = ctx.rt.state.redis.get().await?;
let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db) let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db)
.await .await
.inspect_err(|e| log::error!("Failed to get call: {e:?}"))? .inspect_err(|e| log::error!("Failed to get call: {e:?}"))?
@@ -311,11 +367,27 @@ impl Command<State, Extras> for HangupCommand {
ctx.send().content("❌ The call could not be found.\n-# This is not supposed to happen. If the issue persists, contact a moderator in the official server.").await?; ctx.send().content("❌ The call could not be found.\n-# This is not supposed to happen. If the issue persists, contact a moderator in the official server.").await?;
channel.call_id = None; channel.call_id = None;
channel.upsert(&ctx.rt.state.db).await?; channel.upsert(&ctx.rt.state.db).await.bye();
redis
.zrem("active-calls", format!("{}:{}", call_id, channel.id))
.await
.bye();
return Err(CommandError::DatabaseInconsistentState); return Err(CommandError::DatabaseInconsistentState);
}; };
let channel_keys = [
format!("{call_id}:{}", call.channel_ids[0]),
format!("{call_id}:{}", call.channel_ids[1]),
];
redis
.zrem("active-calls", &channel_keys)
.await
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
.ok();
call.hangup(&ctx.rt.state.db) call.hangup(&ctx.rt.state.db)
.await .await
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?; .inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?;
+13 -4
View File
@@ -1,6 +1,7 @@
use std::{error::Error, sync::Arc}; use std::{error::Error, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use deadpool_redis::redis::AsyncTypedCommands;
use itertools::Itertools; use itertools::Itertools;
use time::{Duration, UtcDateTime}; use time::{Duration, UtcDateTime};
use twilight_http::{api_error::ApiError, error::ErrorType}; use twilight_http::{api_error::ApiError, error::ErrorType};
@@ -353,15 +354,13 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
.icon_url(ImageSource::url(avatar_url).unwrap()) .icon_url(ImageSource::url(avatar_url).unwrap())
.url(message_link), .url(message_link),
) )
.color( .color(if replying_to.data.author_mask.is_some() {
if replying_to.data.author_mask.is_some() {
COLOR_WHITE COLOR_WHITE
} else { } else {
replying_to_user replying_to_user
.and_then(|i| i.color.map(|c| c as u32)) .and_then(|i| i.color.map(|c| c as u32))
.unwrap_or(COLOR_WHITE) .unwrap_or(COLOR_WHITE)
} })
)
.description(ellipse(replying_to.data.content, 100)) .description(ellipse(replying_to.data.content, 100))
.build(), .build(),
); );
@@ -393,6 +392,16 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
} }
} }
let mut redis = rt.state.redis.get().await?;
redis
.zadd(
"active-calls",
format!("{}:{}", call.id, channel.id),
UtcDateTime::now().unix_timestamp(),
)
.await?;
let message_sent = match rt let message_sent = match rt
.http .http
.execute_webhook(Id::new(webhook_id), &webhook_token) .execute_webhook(Id::new(webhook_id), &webhook_token)
+1
View File
@@ -8,3 +8,4 @@ pub mod masks;
pub(super) mod moderation; pub(super) mod moderation;
pub(super) mod permissions; pub(super) mod permissions;
pub(super) mod strings; pub(super) mod strings;
pub mod tasks;
+105
View File
@@ -0,0 +1,105 @@
use async_trait::async_trait;
use deadpool_redis::redis::AsyncTypedCommands;
use time::{Duration, UtcDateTime};
use twilight_model::id::Id;
use crate::{
database::models::Call,
extras::Extras,
framework::{runtime::Runtime, tasks::Task},
state::State,
};
#[derive(Debug)]
pub struct EndInactiveCallsTask {
pub current_shard: u32,
pub total_shards: u32,
}
impl EndInactiveCallsTask {
pub fn new(current_shard: u32, total_shards: u32) -> Self {
Self {
current_shard,
total_shards,
}
}
}
#[async_trait]
impl Task<State, Extras> for EndInactiveCallsTask {
fn delay(&self) -> std::time::Duration {
std::time::Duration::from_secs(5)
}
async fn run(&self, rt: &Runtime<State, Extras>) -> anyhow::Result<()> {
let mut redis = rt.state.redis.get().await?;
let calls_and_channels = redis
.zrangebyscore(
"active-calls",
0,
(UtcDateTime::now() - Duration::minutes(10)).unix_timestamp(),
)
.await?;
let mut ended_call_ids = Vec::new();
for (call_id, channel_id) in calls_and_channels.iter().filter_map(|i| {
i.split_once(":").and_then(|(a, b)| {
let a = a.parse::<u64>().ok();
let b = b.parse::<u64>().ok();
a.zip(b)
})
}) {
if ended_call_ids.contains(&call_id) {
continue;
}
if (call_id) % self.total_shards as u64 != self.current_shard as u64 {
continue;
}
ended_call_ids.push(call_id);
let Ok(Some(call)) = Call::get_by_id(call_id, &rt.state.db).await else {
redis
.zrem("active-calls", format!("{call_id}:{channel_id}"))
.await
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
.ok();
continue;
};
let channel_keys = [
format!("{call_id}:{}", call.channel_ids[0]),
format!("{call_id}:{}", call.channel_ids[1]),
];
redis
.zrem("active-calls", &channel_keys)
.await
.inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}"))
.ok();
call.hangup(&rt.state.db)
.await
.inspect_err(|e| log::error!("Failed to hangup inactive call {call_id}: {e}"))
.ok();
for channel_id in call.channel_ids {
rt.http
.create_message(Id::new(channel_id))
.content("☎️ The call was hung up because one or both parties were gone for too long.")
.await
.inspect_err(|e| {
log::error!("Failed to send call end message to channel {channel_id}: {e}")
})
.ok();
}
}
Ok(())
}
}
+2 -1
View File
@@ -141,7 +141,7 @@ use crate::{
commands::{ commands::{
CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand, CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand,
}, },
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, tasks::EndInactiveCallsTask,
}, },
gateway::events::{ReadyHandler, ReconnectHandler}, gateway::events::{ReadyHandler, ReconnectHandler},
leveling::{commands::LeaderboardCommand, events::MessageLevelingHandler}, leveling::{commands::LeaderboardCommand, events::MessageLevelingHandler},
@@ -307,6 +307,7 @@ async fn start(
.cache_resource_type(ResourceType::CHANNEL) .cache_resource_type(ResourceType::CHANNEL)
.cache_resource_type(ResourceType::MEMBER) .cache_resource_type(ResourceType::MEMBER)
.cache_resource_type(ResourceType::ROLE) .cache_resource_type(ResourceType::ROLE)
.task(EndInactiveCallsTask::new(shard.number(), shard.total()))
.on_ready(ReadyHandler) .on_ready(ReadyHandler)
.on_reconnect(ReconnectHandler) .on_reconnect(ReconnectHandler)
.on_message(CallMessageHandler) .on_message(CallMessageHandler)