feat: user slowmode events (#760)
* feat: user slowmode events Signed-off-by: ispik <ispik@ispik.dev> * fix: remove debug print statement for slowmodes Signed-off-by: ispik <ispik@ispik.dev> * refactor: Send user slowmodes as websocket connects instead of trying to send it in ready payload Signed-off-by: ispik <ispik@ispik.dev> * refactor: optimize user slowmode handling with bulk operations Signed-off-by: ispik <ispik@ispik.dev> * chore: specify release version Release-As: 0.13.6 --------- Signed-off-by: ispik <ispik@ispik.dev>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use redis_kiss::AsyncCommands;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
events::client::{EventV1, ReadyPayloadFields},
|
events::client::{EventV1, ReadyPayloadFields},
|
||||||
util::permissions::DatabasePermissionQuery,
|
util::permissions::DatabasePermissionQuery,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use futures::{
|
|||||||
stream::{SplitSink, SplitStream},
|
stream::{SplitSink, SplitStream},
|
||||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||||
};
|
};
|
||||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
use redis_kiss::{get_connection, AsyncCommands, PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||||
use revolt_config::report_internal_error;
|
use revolt_config::report_internal_error;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
events::{client::EventV1, server::ClientMessage},
|
events::{client::EventV1, server::ClientMessage},
|
||||||
@@ -32,6 +32,7 @@ use sentry::Level;
|
|||||||
|
|
||||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||||
use crate::events::state::{State, SubscriptionStateChange};
|
use crate::events::state::{State, SubscriptionStateChange};
|
||||||
|
use revolt_models::v0;
|
||||||
|
|
||||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||||
@@ -128,6 +129,14 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let slowmodes = fetch_user_slowmodes(&user_id).await.unwrap_or_default();
|
||||||
|
if !slowmodes.is_empty() {
|
||||||
|
let event = EventV1::UserSlowmodes { slowmodes };
|
||||||
|
if report_internal_error!(write.send(config.encode(&event)).await).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create presence session.
|
// Create presence session.
|
||||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||||
|
|
||||||
@@ -527,4 +536,43 @@ async fn worker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn fetch_user_slowmodes(user_id: &str) -> Option<Vec<v0::ChannelSlowmode>> {
|
||||||
|
let mut conn = get_connection().await.ok()?.into_inner();
|
||||||
|
let idx_key = format!("slowmode_idx:{}", user_id);
|
||||||
|
|
||||||
|
let channel_ids: Vec<String> = conn.smembers(&idx_key).await.unwrap_or_default();
|
||||||
|
if channel_ids.is_empty() {
|
||||||
|
return Some(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk fetch all TTLs in one round trip
|
||||||
|
let mut pipe = redis_kiss::redis::pipe();
|
||||||
|
for channel_id in &channel_ids {
|
||||||
|
pipe.ttl(format!("slowmode:{}:{}", user_id, channel_id));
|
||||||
|
}
|
||||||
|
let ttls: Vec<i64> = pipe.query_async(&mut conn).await.unwrap_or_default();
|
||||||
|
|
||||||
|
// Partition into alive/expired in one pass
|
||||||
|
let mut slowmodes = vec![];
|
||||||
|
let mut expired = vec![];
|
||||||
|
for (channel_id, ttl) in channel_ids.iter().zip(ttls.iter()) {
|
||||||
|
if *ttl > 0 {
|
||||||
|
slowmodes.push(v0::ChannelSlowmode {
|
||||||
|
channel_id: channel_id.clone(),
|
||||||
|
duration: *ttl as u64,
|
||||||
|
retry_after: *ttl as u64,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expired.push(channel_id.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk remove all expired members in one SREM call
|
||||||
|
if !expired.is_empty() {
|
||||||
|
conn.srem::<_, _, ()>(&idx_key, expired).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(slowmodes)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ use revolt_result::Error;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revolt_models::v0::{
|
use revolt_models::v0::{
|
||||||
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
|
AppendMessage, Channel, ChannelSlowmode, ChannelUnread, ChannelVoiceState, Emoji,
|
||||||
|
FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser,
|
||||||
|
FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji,
|
||||||
|
PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState,
|
||||||
|
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings,
|
||||||
|
UserVoiceState, Webhook,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Database;
|
use crate::Database;
|
||||||
@@ -51,9 +56,13 @@ impl Default for ReadyPayloadFields {
|
|||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum EventV1 {
|
pub enum EventV1 {
|
||||||
/// Multiple events
|
/// Multiple events
|
||||||
Bulk { v: Vec<EventV1> },
|
Bulk {
|
||||||
|
v: Vec<EventV1>,
|
||||||
|
},
|
||||||
/// Error event
|
/// Error event
|
||||||
Error { data: Error },
|
Error {
|
||||||
|
data: Error,
|
||||||
|
},
|
||||||
|
|
||||||
/// Successfully authenticated
|
/// Successfully authenticated
|
||||||
Authenticated,
|
Authenticated,
|
||||||
@@ -84,7 +93,9 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Ping response
|
/// Ping response
|
||||||
Pong { data: Ping },
|
Pong {
|
||||||
|
data: Ping,
|
||||||
|
},
|
||||||
/// New message
|
/// New message
|
||||||
Message(Message),
|
Message(Message),
|
||||||
|
|
||||||
@@ -105,7 +116,10 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Delete message
|
/// Delete message
|
||||||
MessageDelete { id: String, channel: String },
|
MessageDelete {
|
||||||
|
id: String,
|
||||||
|
channel: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// New reaction to a message
|
/// New reaction to a message
|
||||||
MessageReact {
|
MessageReact {
|
||||||
@@ -131,7 +145,10 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Bulk delete messages
|
/// Bulk delete messages
|
||||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
BulkMessageDelete {
|
||||||
|
channel: String,
|
||||||
|
ids: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// New server
|
/// New server
|
||||||
ServerCreate {
|
ServerCreate {
|
||||||
@@ -139,7 +156,7 @@ pub enum EventV1 {
|
|||||||
server: Server,
|
server: Server,
|
||||||
channels: Vec<Channel>,
|
channels: Vec<Channel>,
|
||||||
emojis: Vec<Emoji>,
|
emojis: Vec<Emoji>,
|
||||||
voice_states: Vec<ChannelVoiceState>
|
voice_states: Vec<ChannelVoiceState>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Update existing server
|
/// Update existing server
|
||||||
@@ -151,7 +168,9 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Delete server
|
/// Delete server
|
||||||
ServerDelete { id: String },
|
ServerDelete {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Update existing server member
|
/// Update existing server member
|
||||||
ServerMemberUpdate {
|
ServerMemberUpdate {
|
||||||
@@ -187,10 +206,16 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Server role deleted
|
/// Server role deleted
|
||||||
ServerRoleDelete { id: String, role_id: String },
|
ServerRoleDelete {
|
||||||
|
id: String,
|
||||||
|
role_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Server roles ranks updated
|
/// Server roles ranks updated
|
||||||
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
|
ServerRoleRanksUpdate {
|
||||||
|
id: String,
|
||||||
|
ranks: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Update existing user
|
/// Update existing user
|
||||||
UserUpdate {
|
UserUpdate {
|
||||||
@@ -202,9 +227,15 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Relationship with another user changed
|
/// Relationship with another user changed
|
||||||
UserRelationship { id: String, user: User },
|
UserRelationship {
|
||||||
|
id: String,
|
||||||
|
user: User,
|
||||||
|
},
|
||||||
/// Settings updated remotely
|
/// Settings updated remotely
|
||||||
UserSettingsUpdate { id: String, update: UserSettings },
|
UserSettingsUpdate {
|
||||||
|
id: String,
|
||||||
|
update: UserSettings,
|
||||||
|
},
|
||||||
|
|
||||||
/// User has been platform banned or deleted their account
|
/// User has been platform banned or deleted their account
|
||||||
///
|
///
|
||||||
@@ -215,7 +246,10 @@ pub enum EventV1 {
|
|||||||
/// - Server Memberships
|
/// - Server Memberships
|
||||||
///
|
///
|
||||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||||
UserPlatformWipe { user_id: String, flags: i32 },
|
UserPlatformWipe {
|
||||||
|
user_id: String,
|
||||||
|
flags: i32,
|
||||||
|
},
|
||||||
/// New emoji
|
/// New emoji
|
||||||
EmojiCreate(Emoji),
|
EmojiCreate(Emoji),
|
||||||
|
|
||||||
@@ -226,7 +260,9 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Delete emoji
|
/// Delete emoji
|
||||||
EmojiDelete { id: String },
|
EmojiDelete {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// New report
|
/// New report
|
||||||
ReportCreate(Report),
|
ReportCreate(Report),
|
||||||
@@ -242,19 +278,33 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Delete channel
|
/// Delete channel
|
||||||
ChannelDelete { id: String },
|
ChannelDelete {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// User joins a group
|
/// User joins a group
|
||||||
ChannelGroupJoin { id: String, user: String },
|
ChannelGroupJoin {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// User leaves a group
|
/// User leaves a group
|
||||||
ChannelGroupLeave { id: String, user: String },
|
ChannelGroupLeave {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// User started typing in a channel
|
/// User started typing in a channel
|
||||||
ChannelStartTyping { id: String, user: String },
|
ChannelStartTyping {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// User stopped typing in a channel
|
/// User stopped typing in a channel
|
||||||
ChannelStopTyping { id: String, user: String },
|
ChannelStopTyping {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// User acknowledged message in channel
|
/// User acknowledged message in channel
|
||||||
ChannelAck {
|
ChannelAck {
|
||||||
@@ -274,7 +324,9 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Delete webhook
|
/// Delete webhook
|
||||||
WebhookDelete { id: String },
|
WebhookDelete {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Auth events
|
/// Auth events
|
||||||
Auth(AuthifierEvent),
|
Auth(AuthifierEvent),
|
||||||
@@ -292,7 +344,7 @@ pub enum EventV1 {
|
|||||||
user: String,
|
user: String,
|
||||||
from: String,
|
from: String,
|
||||||
to: String,
|
to: String,
|
||||||
state: UserVoiceState
|
state: UserVoiceState,
|
||||||
},
|
},
|
||||||
UserVoiceStateUpdate {
|
UserVoiceStateUpdate {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -304,7 +356,11 @@ pub enum EventV1 {
|
|||||||
from: String,
|
from: String,
|
||||||
to: String,
|
to: String,
|
||||||
token: String,
|
token: String,
|
||||||
}
|
},
|
||||||
|
/// User's active slowmodes
|
||||||
|
UserSlowmodes {
|
||||||
|
slowmodes: Vec<ChannelSlowmode>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventV1 {
|
impl EventV1 {
|
||||||
|
|||||||
@@ -314,6 +314,12 @@ auto_derived!(
|
|||||||
/// Only used when the user is the first one connected.
|
/// Only used when the user is the first one connected.
|
||||||
pub recipients: Option<Vec<String>>,
|
pub recipients: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ChannelSlowmode {
|
||||||
|
pub channel_id: String,
|
||||||
|
pub duration: u64,
|
||||||
|
pub retry_after: u64,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use redis_kiss::{get_connection, redis, AsyncCommands};
|
use redis_kiss::{get_connection, redis, AsyncCommands};
|
||||||
|
use revolt_database::events::client::EventV1;
|
||||||
use revolt_database::util::permissions::DatabasePermissionQuery;
|
use revolt_database::util::permissions::DatabasePermissionQuery;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
|
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
|
||||||
};
|
};
|
||||||
use revolt_database::{Channel, Interactions, Message, AMQP};
|
use revolt_database::{Channel, Interactions, Message, AMQP};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
|
use revolt_models::v0::ChannelSlowmode;
|
||||||
use revolt_permissions::PermissionQuery;
|
use revolt_permissions::PermissionQuery;
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
@@ -84,6 +86,16 @@ pub async fn message_send(
|
|||||||
.await
|
.await
|
||||||
.unwrap_or(None);
|
.unwrap_or(None);
|
||||||
|
|
||||||
|
if set_result.is_some() {
|
||||||
|
let idx_key = format!("slowmode_idx:{}", user.id);
|
||||||
|
conn.sadd::<_, _, ()>(&idx_key, channel_id.as_str())
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
conn.expire::<_, ()>(&idx_key, *channel_slowmode as usize)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
// If `set_result` is None, the `NX` condition failed because the key already exists.
|
// If `set_result` is None, the `NX` condition failed because the key already exists.
|
||||||
// This means the user is currently in slowmode.
|
// This means the user is currently in slowmode.
|
||||||
if set_result.is_none() {
|
if set_result.is_none() {
|
||||||
@@ -92,10 +104,29 @@ pub async fn message_send(
|
|||||||
|
|
||||||
// Redis returns positive integers for valid TTLs
|
// Redis returns positive integers for valid TTLs
|
||||||
if ttl > 0 {
|
if ttl > 0 {
|
||||||
|
EventV1::UserSlowmodes {
|
||||||
|
slowmodes: vec![ChannelSlowmode {
|
||||||
|
channel_id: channel_id.to_string(),
|
||||||
|
duration: *channel_slowmode,
|
||||||
|
retry_after: ttl as u64,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
.private(user.id.clone())
|
||||||
|
.await;
|
||||||
return Err(create_error!(InSlowmode {
|
return Err(create_error!(InSlowmode {
|
||||||
retry_after: ttl as u64
|
retry_after: ttl as u64
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
EventV1::UserSlowmodes {
|
||||||
|
slowmodes: vec![ChannelSlowmode {
|
||||||
|
channel_id: channel_id.to_string(),
|
||||||
|
duration: *channel_slowmode,
|
||||||
|
retry_after: *channel_slowmode,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
.private(user.id.clone())
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If Redis connection fails, just skip the slowmode check
|
// If Redis connection fails, just skip the slowmode check
|
||||||
|
|||||||
Reference in New Issue
Block a user