refactor(transport): apply rustfmt formatting
This commit is contained in:
parent
06c08148cb
commit
12eaa83b87
@ -1,7 +1,7 @@
|
|||||||
|
use redis::AsyncCommands;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use redis::AsyncCommands;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MessageAck {
|
pub struct MessageAck {
|
||||||
@ -66,13 +66,17 @@ impl AckTracker {
|
|||||||
status: AckStatus::Pending,
|
status: AckStatus::Pending,
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
let value = serde_json::to_string(&ack)
|
let value =
|
||||||
|
serde_json::to_string(&ack).map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let _: () = conn
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.set_ex(&key, &value, self.timeout.as_secs())
|
||||||
|
|
||||||
let _: () = conn.set_ex(&key, &value, self.timeout.as_secs())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
@ -84,7 +88,8 @@ impl AckTracker {
|
|||||||
message_id: Uuid,
|
message_id: Uuid,
|
||||||
room_id: Uuid,
|
room_id: Uuid,
|
||||||
) -> Result<(), crate::error::AppTransportError> {
|
) -> Result<(), crate::error::AppTransportError> {
|
||||||
self.update_status(message_id, room_id, AckStatus::Received).await
|
self.update_status(message_id, room_id, AckStatus::Received)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mark_persisted(
|
pub async fn mark_persisted(
|
||||||
@ -101,13 +106,17 @@ impl AckTracker {
|
|||||||
status: AckStatus::Persisted,
|
status: AckStatus::Persisted,
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
let value = serde_json::to_string(&ack)
|
let value =
|
||||||
|
serde_json::to_string(&ack).map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let _: () = conn
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.set_ex(&key, &value, self.timeout.as_secs())
|
||||||
|
|
||||||
let _: () = conn.set_ex(&key, &value, self.timeout.as_secs())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
@ -119,10 +128,14 @@ impl AckTracker {
|
|||||||
message_id: Uuid,
|
message_id: Uuid,
|
||||||
room_id: Uuid,
|
room_id: Uuid,
|
||||||
) -> Result<(), crate::error::AppTransportError> {
|
) -> Result<(), crate::error::AppTransportError> {
|
||||||
self.update_status(message_id, room_id, AckStatus::Delivered).await?;
|
self.update_status(message_id, room_id, AckStatus::Delivered)
|
||||||
|
.await?;
|
||||||
let key = format!("ack:pending:{}:{}", room_id, message_id);
|
let key = format!("ack:pending:{}:{}", room_id, message_id);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let _: Result<(), redis::RedisError> = conn.del(&key).await;
|
let _: Result<(), redis::RedisError> = conn.del(&key).await;
|
||||||
@ -134,7 +147,8 @@ impl AckTracker {
|
|||||||
message_id: Uuid,
|
message_id: Uuid,
|
||||||
room_id: Uuid,
|
room_id: Uuid,
|
||||||
) -> Result<(), crate::error::AppTransportError> {
|
) -> Result<(), crate::error::AppTransportError> {
|
||||||
self.update_status(message_id, room_id, AckStatus::Failed).await
|
self.update_status(message_id, room_id, AckStatus::Failed)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_status(
|
pub async fn get_status(
|
||||||
@ -144,10 +158,15 @@ impl AckTracker {
|
|||||||
) -> Result<Option<MessageAck>, crate::error::AppTransportError> {
|
) -> Result<Option<MessageAck>, crate::error::AppTransportError> {
|
||||||
let key = format!("ack:pending:{}:{}", room_id, message_id);
|
let key = format!("ack:pending:{}:{}", room_id, message_id);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let value: Option<String> = conn.get(&key).await
|
let value: Option<String> = conn
|
||||||
|
.get(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
@ -173,10 +192,14 @@ impl AckTracker {
|
|||||||
let value = serde_json::to_string(&ack)
|
let value = serde_json::to_string(&ack)
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let _: () = conn.set_ex(&key, &value, self.timeout.as_secs())
|
let _: () = conn
|
||||||
|
.set_ex(&key, &value, self.timeout.as_secs())
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,13 +49,10 @@ impl NatsTransport {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = opts
|
let client = opts.connect(&url).await.map_err(|e| {
|
||||||
.connect(&url)
|
warn!(error = %e, "NATS connect failed");
|
||||||
.await
|
AppTransportError::Internal
|
||||||
.map_err(|e| {
|
})?;
|
||||||
warn!(error = %e, "NATS connect failed");
|
|
||||||
AppTransportError::Internal
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let jetstream = jetstream::new(client);
|
let jetstream = jetstream::new(client);
|
||||||
|
|
||||||
@ -150,7 +147,10 @@ impl Transport for NatsTransport {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut messages = match stream.get_or_create_consumer(&durable, config.clone()).await {
|
let mut messages = match stream
|
||||||
|
.get_or_create_consumer(&durable, config.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(c) => match c.messages().await {
|
Ok(c) => match c.messages().await {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -200,19 +200,17 @@ impl Transport for NatsTransport {
|
|||||||
.get_or_create_consumer(&durable, config.clone())
|
.get_or_create_consumer(&durable, config.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(new_consumer) => {
|
Ok(new_consumer) => match new_consumer.messages().await {
|
||||||
match new_consumer.messages().await {
|
Ok(new_messages) => {
|
||||||
Ok(new_messages) => {
|
info!(subject = %subject, "NATS consumer reconnected");
|
||||||
info!(subject = %subject, "NATS consumer reconnected");
|
messages = new_messages;
|
||||||
messages = new_messages;
|
reconnect_retries = 0;
|
||||||
reconnect_retries = 0;
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!(subject = %subject, error = %e, "Failed to get messages from reconnected NATS consumer");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => {
|
||||||
|
warn!(subject = %subject, error = %e, "Failed to get messages from reconnected NATS consumer");
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(subject = %subject, error = %e, "Failed to recreate NATS consumer in reconnect loop");
|
warn!(subject = %subject, error = %e, "Failed to recreate NATS consumer in reconnect loop");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,10 @@ impl DeduplicationManager {
|
|||||||
) -> Result<bool, crate::error::AppTransportError> {
|
) -> Result<bool, crate::error::AppTransportError> {
|
||||||
let key = format!("dedup:{}:{}", room_id, message_id);
|
let key = format!("dedup:{}:{}", room_id, message_id);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
// Use atomic SET NX EX to prevent race conditions.
|
// Use atomic SET NX EX to prevent race conditions.
|
||||||
@ -48,10 +51,15 @@ impl DeduplicationManager {
|
|||||||
) -> Result<bool, crate::error::AppTransportError> {
|
) -> Result<bool, crate::error::AppTransportError> {
|
||||||
let key = format!("dedup:{}:{}", room_id, message_id);
|
let key = format!("dedup:{}:{}", room_id, message_id);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let exists: bool = conn.exists(&key).await
|
let exists: bool = conn
|
||||||
|
.exists(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
|
|||||||
@ -7,8 +7,7 @@ pub struct EncryptedMessage {
|
|||||||
pub recipient_key_id: String,
|
pub recipient_key_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct E2EEncryption {
|
pub struct E2EEncryption {}
|
||||||
}
|
|
||||||
|
|
||||||
impl E2EEncryption {
|
impl E2EEncryption {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|||||||
@ -8,6 +8,44 @@ pub enum AppTransportError {
|
|||||||
TokenInvalidOrExpired,
|
TokenInvalidOrExpired,
|
||||||
#[error("renewal limit exceeded")]
|
#[error("renewal limit exceeded")]
|
||||||
RenewalLimitExceeded,
|
RenewalLimitExceeded,
|
||||||
|
#[error("rate limit exceeded")]
|
||||||
|
RateLimitExceeded,
|
||||||
|
#[error("room not found")]
|
||||||
|
RoomNotFound,
|
||||||
|
#[error("access denied")]
|
||||||
|
AccessDenied,
|
||||||
#[error("internal error")]
|
#[error("internal error")]
|
||||||
Internal,
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<room::error::RoomError> for AppTransportError {
|
||||||
|
fn from(e: room::error::RoomError) -> Self {
|
||||||
|
match e {
|
||||||
|
room::error::RoomError::Unauthorized => AppTransportError::Unauthorized,
|
||||||
|
room::error::RoomError::NoPower => AppTransportError::AccessDenied,
|
||||||
|
room::error::RoomError::NotFound(_) => AppTransportError::RoomNotFound,
|
||||||
|
room::error::RoomError::RateLimited(_) => AppTransportError::RateLimitExceeded,
|
||||||
|
room::error::RoomError::BadRequest(_) => AppTransportError::Internal,
|
||||||
|
room::error::RoomError::Database(_) => AppTransportError::Internal,
|
||||||
|
room::error::RoomError::RoleParseError => AppTransportError::Internal,
|
||||||
|
room::error::RoomError::Internal(_) => AppTransportError::Internal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppTransportError {
|
||||||
|
/// Map error to a (code, error_type) pair for WS error messages.
|
||||||
|
/// Frontend can use these to distinguish auth vs rate-limit vs internal errors.
|
||||||
|
pub fn ws_error_code(&self) -> (u16, &'static str) {
|
||||||
|
match self {
|
||||||
|
AppTransportError::Unauthorized => (401, "unauthorized"),
|
||||||
|
AppTransportError::TokenInvalidOrExpired => (401, "token_invalid"),
|
||||||
|
AppTransportError::AccessDenied => (403, "access_denied"),
|
||||||
|
AppTransportError::RateLimitExceeded => (429, "rate_limit_exceeded"),
|
||||||
|
AppTransportError::RoomNotFound => (404, "not_found"),
|
||||||
|
AppTransportError::InvalidSession => (401, "invalid_session"),
|
||||||
|
AppTransportError::RenewalLimitExceeded => (429, "renewal_limit"),
|
||||||
|
AppTransportError::Internal => (500, "internal_error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use models::{RoomCategoryId, ProjectId, UserId};
|
use models::{ProjectId, RoomCategoryId, UserId};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use models::{RoomId, Uuid};
|
|
||||||
use super::message::MessageNewService;
|
use super::message::MessageNewService;
|
||||||
|
use models::{RoomId, Uuid};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SearchResultService {
|
pub struct SearchResultService {
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use models::RoomId;
|
|||||||
use queue::{ReactionGroup, RoomMessageEvent, RoomMessageStreamChunkEvent, TypingEvent};
|
use queue::{ReactionGroup, RoomMessageEvent, RoomMessageStreamChunkEvent, TypingEvent};
|
||||||
use room::types::NotificationEvent;
|
use room::types::NotificationEvent;
|
||||||
|
|
||||||
use crate::event::{member, message, reaction, notify};
|
|
||||||
use super::types::WsOutEvent;
|
use super::types::WsOutEvent;
|
||||||
|
use crate::event::{member, message, notify, reaction};
|
||||||
|
|
||||||
pub struct EventDispatcher;
|
pub struct EventDispatcher;
|
||||||
|
|
||||||
@ -117,11 +117,15 @@ impl EventDispatcher {
|
|||||||
reactions: reactions
|
reactions: reactions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| reaction::ReactionGroup {
|
.map(|g| reaction::ReactionGroup {
|
||||||
emoji: g.emoji.clone(),
|
emoji: g.emoji.clone(),
|
||||||
count: g.count as i64,
|
count: g.count as i64,
|
||||||
reacted_by_me: g.reacted_by_me,
|
reacted_by_me: g.reacted_by_me,
|
||||||
users: g.users.iter().filter_map(|u| u.parse::<uuid::Uuid>().ok()).collect(),
|
users: g
|
||||||
})
|
.users
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| u.parse::<uuid::Uuid>().ok())
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,11 @@ pub(crate) async fn message_list(
|
|||||||
emoji: r.emoji.clone(),
|
emoji: r.emoji.clone(),
|
||||||
count: r.count as i64,
|
count: r.count as i64,
|
||||||
reacted_by_me: r.reacted_by_me,
|
reacted_by_me: r.reacted_by_me,
|
||||||
users: r.users.iter().filter_map(|u| uuid::Uuid::parse_str(u).ok()).collect(),
|
users: r
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| uuid::Uuid::parse_str(u).ok())
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -171,10 +171,12 @@ pub(crate) async fn custom_status_update(
|
|||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
let evt = session
|
let evt = session.service.room.set_custom_status(
|
||||||
.service
|
session.user.user_id,
|
||||||
.room
|
emoji.clone(),
|
||||||
.set_custom_status(session.user.user_id, emoji.clone(), text.clone(), expires_at);
|
text.clone(),
|
||||||
|
expires_at,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(data) = evt {
|
if let Some(data) = evt {
|
||||||
Ok(Some(WsOutEvent::CustomStatusUpdated {
|
Ok(Some(WsOutEvent::CustomStatusUpdated {
|
||||||
@ -221,27 +223,40 @@ pub(crate) async fn ban_remove() -> Result<Option<WsOutEvent>, AppTransportError
|
|||||||
|
|
||||||
// ─── Voice (stubs) ────────────────────────────────────────────────────
|
// ─── Voice (stubs) ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub(crate) async fn voice_join(room: models::RoomId) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn voice_join(
|
||||||
|
room: models::RoomId,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%room, "Voice join");
|
tracing::info!(%room, "Voice join");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn voice_leave(room: models::RoomId) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn voice_leave(
|
||||||
|
room: models::RoomId,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%room, "Voice leave");
|
tracing::info!(%room, "Voice leave");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn voice_mute(room: models::RoomId, muted: bool) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn voice_mute(
|
||||||
|
room: models::RoomId,
|
||||||
|
muted: bool,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%room, %muted, "Voice mute");
|
tracing::info!(%room, %muted, "Voice mute");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn voice_deaf(room: models::RoomId, deafened: bool) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn voice_deaf(
|
||||||
|
room: models::RoomId,
|
||||||
|
deafened: bool,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%room, %deafened, "Voice deaf");
|
tracing::info!(%room, %deafened, "Voice deaf");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn screen_share(room: models::RoomId, start: bool) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn screen_share(
|
||||||
|
room: models::RoomId,
|
||||||
|
start: bool,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%room, %start, "Screen share");
|
tracing::info!(%room, %start, "Screen share");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,9 @@ use crate::error::AppTransportError;
|
|||||||
use crate::handler::session::TransportSession;
|
use crate::handler::session::TransportSession;
|
||||||
use crate::handler::types::{WsInMessage, WsOutEvent};
|
use crate::handler::types::{WsInMessage, WsOutEvent};
|
||||||
|
|
||||||
|
mod message;
|
||||||
mod misc;
|
mod misc;
|
||||||
mod msg;
|
mod msg;
|
||||||
mod message;
|
|
||||||
mod reaction;
|
mod reaction;
|
||||||
mod room;
|
mod room;
|
||||||
|
|
||||||
@ -21,14 +21,25 @@ impl MessageHandler {
|
|||||||
WsInMessage::Unsubscribe { room } => msg::unsubscribe(session, room).await,
|
WsInMessage::Unsubscribe { room } => msg::unsubscribe(session, room).await,
|
||||||
WsInMessage::TypingStart { room } => msg::typing_start(session, room).await,
|
WsInMessage::TypingStart { room } => msg::typing_start(session, room).await,
|
||||||
WsInMessage::TypingStop { room } => msg::typing_stop(session, room).await,
|
WsInMessage::TypingStop { room } => msg::typing_stop(session, room).await,
|
||||||
WsInMessage::ReadReceipt { room, last_read_seq } => {
|
WsInMessage::ReadReceipt {
|
||||||
msg::read_receipt(session, room, last_read_seq).await
|
room,
|
||||||
}
|
last_read_seq,
|
||||||
WsInMessage::MessageList { room, before_seq, after_seq, limit } => {
|
} => msg::read_receipt(session, room, last_read_seq).await,
|
||||||
message::message_list(session, room, before_seq, after_seq, limit).await
|
WsInMessage::MessageList {
|
||||||
}
|
room,
|
||||||
WsInMessage::MessageCreate { room, content, content_type, thread, in_reply_to } => {
|
before_seq,
|
||||||
message::message_create(session, room, content, content_type, thread, in_reply_to).await
|
after_seq,
|
||||||
|
limit,
|
||||||
|
} => message::message_list(session, room, before_seq, after_seq, limit).await,
|
||||||
|
WsInMessage::MessageCreate {
|
||||||
|
room,
|
||||||
|
content,
|
||||||
|
content_type,
|
||||||
|
thread,
|
||||||
|
in_reply_to,
|
||||||
|
} => {
|
||||||
|
message::message_create(session, room, content, content_type, thread, in_reply_to)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
WsInMessage::MessageUpdate { message, content } => {
|
WsInMessage::MessageUpdate { message, content } => {
|
||||||
message::message_update(session, message, content).await
|
message::message_update(session, message, content).await
|
||||||
@ -37,28 +48,44 @@ impl MessageHandler {
|
|||||||
message::message_revoke(session, message).await
|
message::message_revoke(session, message).await
|
||||||
}
|
}
|
||||||
WsInMessage::RoomGet { room } => room::room_get(session, room).await,
|
WsInMessage::RoomGet { room } => room::room_get(session, room).await,
|
||||||
WsInMessage::RoomCreate { project, room_name, public, category } => {
|
WsInMessage::RoomCreate {
|
||||||
room::room_create(session, project, room_name, public, category).await
|
project,
|
||||||
}
|
room_name,
|
||||||
WsInMessage::RoomUpdate { room, room_name, public, category } => {
|
public,
|
||||||
room::room_update(session, room, room_name, public, category).await
|
category,
|
||||||
}
|
} => room::room_create(session, project, room_name, public, category).await,
|
||||||
|
WsInMessage::RoomUpdate {
|
||||||
|
room,
|
||||||
|
room_name,
|
||||||
|
public,
|
||||||
|
category,
|
||||||
|
} => room::room_update(session, room, room_name, public, category).await,
|
||||||
WsInMessage::RoomDelete { room } => room::room_delete(session, room).await,
|
WsInMessage::RoomDelete { room } => room::room_delete(session, room).await,
|
||||||
WsInMessage::CategoryCreate { project, name, position } => {
|
WsInMessage::CategoryCreate {
|
||||||
room::category_create(session, project, name, position).await
|
project,
|
||||||
}
|
name,
|
||||||
|
position,
|
||||||
|
} => room::category_create(session, project, name, position).await,
|
||||||
WsInMessage::CategoryUpdate { id, name, position } => {
|
WsInMessage::CategoryUpdate { id, name, position } => {
|
||||||
room::category_update(session, id, name, position).await
|
room::category_update(session, id, name, position).await
|
||||||
}
|
}
|
||||||
WsInMessage::CategoryDelete { id } => room::category_delete(session, id).await,
|
WsInMessage::CategoryDelete { id } => room::category_delete(session, id).await,
|
||||||
WsInMessage::AccessGrant { room, user } => room::access_grant(session, room, user).await,
|
WsInMessage::AccessGrant { room, user } => {
|
||||||
WsInMessage::AccessRevoke { room, user } => room::access_revoke(session, room, user).await,
|
room::access_grant(session, room, user).await
|
||||||
WsInMessage::ReactionAdd { room, message, emoji } => {
|
|
||||||
reaction::reaction_add(session, room, message, emoji).await
|
|
||||||
}
|
}
|
||||||
WsInMessage::ReactionRemove { room, message, emoji } => {
|
WsInMessage::AccessRevoke { room, user } => {
|
||||||
reaction::reaction_remove(session, room, message, emoji).await
|
room::access_revoke(session, room, user).await
|
||||||
}
|
}
|
||||||
|
WsInMessage::ReactionAdd {
|
||||||
|
room,
|
||||||
|
message,
|
||||||
|
emoji,
|
||||||
|
} => reaction::reaction_add(session, room, message, emoji).await,
|
||||||
|
WsInMessage::ReactionRemove {
|
||||||
|
room,
|
||||||
|
message,
|
||||||
|
emoji,
|
||||||
|
} => reaction::reaction_remove(session, room, message, emoji).await,
|
||||||
WsInMessage::ThreadCreate { room, parent } => {
|
WsInMessage::ThreadCreate { room, parent } => {
|
||||||
reaction::thread_create(session, room, parent).await
|
reaction::thread_create(session, room, parent).await
|
||||||
}
|
}
|
||||||
@ -79,15 +106,35 @@ impl MessageHandler {
|
|||||||
WsInMessage::NotificationArchive { id } => {
|
WsInMessage::NotificationArchive { id } => {
|
||||||
misc::notification_archive(session, id).await
|
misc::notification_archive(session, id).await
|
||||||
}
|
}
|
||||||
WsInMessage::Search { q, room, start_time, end_time, sender_id, content_type, limit, offset } => {
|
WsInMessage::Search {
|
||||||
misc::search(session, q, room, start_time, end_time, sender_id, content_type, limit, offset).await
|
q,
|
||||||
}
|
room,
|
||||||
WsInMessage::PresenceUpdate { status } => {
|
start_time,
|
||||||
misc::presence_update(session, status).await
|
end_time,
|
||||||
}
|
sender_id,
|
||||||
WsInMessage::CustomStatusUpdate { emoji, text, expires_at } => {
|
content_type,
|
||||||
misc::custom_status_update(session, emoji, text, expires_at).await
|
limit,
|
||||||
|
offset,
|
||||||
|
} => {
|
||||||
|
misc::search(
|
||||||
|
session,
|
||||||
|
q,
|
||||||
|
room,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
sender_id,
|
||||||
|
content_type,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
WsInMessage::PresenceUpdate { status } => misc::presence_update(session, status).await,
|
||||||
|
WsInMessage::CustomStatusUpdate {
|
||||||
|
emoji,
|
||||||
|
text,
|
||||||
|
expires_at,
|
||||||
|
} => misc::custom_status_update(session, emoji, text, expires_at).await,
|
||||||
WsInMessage::InviteCreate { .. } => misc::invite_create().await,
|
WsInMessage::InviteCreate { .. } => misc::invite_create().await,
|
||||||
WsInMessage::InviteAccept { .. } => misc::invite_accept().await,
|
WsInMessage::InviteAccept { .. } => misc::invite_accept().await,
|
||||||
WsInMessage::InviteRevoke { .. } => misc::invite_revoke().await,
|
WsInMessage::InviteRevoke { .. } => misc::invite_revoke().await,
|
||||||
@ -99,19 +146,44 @@ impl MessageHandler {
|
|||||||
WsInMessage::VoiceDeaf { room, deafened } => misc::voice_deaf(room, deafened).await,
|
WsInMessage::VoiceDeaf { room, deafened } => misc::voice_deaf(room, deafened).await,
|
||||||
WsInMessage::ScreenShare { room, start } => misc::screen_share(room, start).await,
|
WsInMessage::ScreenShare { room, start } => misc::screen_share(room, start).await,
|
||||||
WsInMessage::AiList { room } => misc::ai_list(session, room).await,
|
WsInMessage::AiList { room } => misc::ai_list(session, room).await,
|
||||||
WsInMessage::AiUpsert { room, model, version, system_prompt, temperature, max_tokens, stream } => {
|
WsInMessage::AiUpsert {
|
||||||
misc::ai_upsert(session, room, model, version, system_prompt, temperature, max_tokens, stream).await
|
room,
|
||||||
|
model,
|
||||||
|
version,
|
||||||
|
system_prompt,
|
||||||
|
temperature,
|
||||||
|
max_tokens,
|
||||||
|
stream,
|
||||||
|
} => {
|
||||||
|
misc::ai_upsert(
|
||||||
|
session,
|
||||||
|
room,
|
||||||
|
model,
|
||||||
|
version,
|
||||||
|
system_prompt,
|
||||||
|
temperature,
|
||||||
|
max_tokens,
|
||||||
|
stream,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
WsInMessage::AiDelete { room, agent_id } => {
|
WsInMessage::AiDelete { room, agent_id } => {
|
||||||
misc::ai_delete(session, room, agent_id).await
|
misc::ai_delete(session, room, agent_id).await
|
||||||
}
|
}
|
||||||
WsInMessage::AiStop { room } => misc::ai_stop(session, room).await,
|
WsInMessage::AiStop { room } => misc::ai_stop(session, room).await,
|
||||||
WsInMessage::UserSummary { username } => misc::user_summary(session, username).await,
|
WsInMessage::UserSummary { username } => misc::user_summary(session, username).await,
|
||||||
WsInMessage::StateSetReadSeq { room, last_read_seq } => {
|
WsInMessage::StateSetReadSeq {
|
||||||
misc::state_set_read_seq(session, room, last_read_seq).await
|
room,
|
||||||
}
|
last_read_seq,
|
||||||
WsInMessage::StateUpdateDnd { room, do_not_disturb, dnd_start_hour, dnd_end_hour } => {
|
} => misc::state_set_read_seq(session, room, last_read_seq).await,
|
||||||
misc::state_update_dnd(session, room, do_not_disturb, dnd_start_hour, dnd_end_hour).await
|
WsInMessage::StateUpdateDnd {
|
||||||
|
room,
|
||||||
|
do_not_disturb,
|
||||||
|
dnd_start_hour,
|
||||||
|
dnd_end_hour,
|
||||||
|
} => {
|
||||||
|
misc::state_update_dnd(session, room, do_not_disturb, dnd_start_hour, dnd_end_hour)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use room::ws_context::WsUserContext;
|
|||||||
use crate::error::AppTransportError;
|
use crate::error::AppTransportError;
|
||||||
use crate::handler::session::TransportSession;
|
use crate::handler::session::TransportSession;
|
||||||
use crate::handler::types::WsOutEvent;
|
use crate::handler::types::WsOutEvent;
|
||||||
|
use room::connection::MAX_ROOMS_PER_SESSION;
|
||||||
|
|
||||||
pub(crate) async fn ping() -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn ping() -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
Ok(Some(WsOutEvent::Pong {
|
Ok(Some(WsOutEvent::Pong {
|
||||||
@ -14,10 +15,11 @@ pub(crate) async fn subscribe(
|
|||||||
session: &TransportSession,
|
session: &TransportSession,
|
||||||
room: models::RoomId,
|
room: models::RoomId,
|
||||||
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
let sub = session.subscribe_room(room).await.map_err(|e| {
|
// Per-session room subscription limit
|
||||||
tracing::warn!(error = %e, "subscribe_room failed");
|
if session.subscriptions.len() >= MAX_ROOMS_PER_SESSION {
|
||||||
AppTransportError::Internal
|
return Err(AppTransportError::RateLimitExceeded);
|
||||||
})?;
|
}
|
||||||
|
let sub = session.subscribe_room(room).await?;
|
||||||
session.subscriptions.insert(room, sub);
|
session.subscriptions.insert(room, sub);
|
||||||
session.service.room.spawn_room_workers(room);
|
session.service.room.spawn_room_workers(room);
|
||||||
session.refresh_project().await;
|
session.refresh_project().await;
|
||||||
|
|||||||
@ -60,7 +60,11 @@ fn build_reaction_batch(
|
|||||||
emoji: g.emoji,
|
emoji: g.emoji,
|
||||||
count: g.count as i64,
|
count: g.count as i64,
|
||||||
reacted_by_me: g.reacted_by_me,
|
reacted_by_me: g.reacted_by_me,
|
||||||
users: g.users.iter().filter_map(|u| u.parse::<Uuid>().ok()).collect(),
|
users: g
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.filter_map(|u| u.parse::<Uuid>().ok())
|
||||||
|
.collect(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
@ -76,7 +80,11 @@ pub(crate) async fn thread_create(
|
|||||||
session
|
session
|
||||||
.service
|
.service
|
||||||
.room
|
.room
|
||||||
.room_thread_create(room, room::RoomThreadCreateRequest { parent_seq: parent }, &ctx)
|
.room_thread_create(
|
||||||
|
room,
|
||||||
|
room::RoomThreadCreateRequest { parent_seq: parent },
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::warn!(error = %e, "room_thread_create failed");
|
tracing::warn!(error = %e, "room_thread_create failed");
|
||||||
@ -85,12 +93,16 @@ pub(crate) async fn thread_create(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn thread_resolve(thread_id: models::RoomThreadId) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn thread_resolve(
|
||||||
|
thread_id: models::RoomThreadId,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%thread_id, "Thread resolved");
|
tracing::info!(%thread_id, "Thread resolved");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn thread_archive(thread_id: models::RoomThreadId) -> Result<Option<WsOutEvent>, AppTransportError> {
|
pub(crate) async fn thread_archive(
|
||||||
|
thread_id: models::RoomThreadId,
|
||||||
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
||||||
tracing::info!(%thread_id, "Thread archived");
|
tracing::info!(%thread_id, "Thread archived");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,11 +170,7 @@ pub(crate) async fn category_update(
|
|||||||
session
|
session
|
||||||
.service
|
.service
|
||||||
.room
|
.room
|
||||||
.room_category_update(
|
.room_category_update(id, room::RoomCategoryUpdateRequest { name, position }, &ctx)
|
||||||
id,
|
|
||||||
room::RoomCategoryUpdateRequest { name, position },
|
|
||||||
&ctx,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::warn!(error = %e, "room_category_update failed");
|
tracing::warn!(error = %e, "room_category_update failed");
|
||||||
|
|||||||
@ -11,4 +11,4 @@ pub use inbound::MessageHandler;
|
|||||||
pub use poll::poll_subscriptions;
|
pub use poll::poll_subscriptions;
|
||||||
pub use session::{TransportSession, WsUserCtx};
|
pub use session::{TransportSession, WsUserCtx};
|
||||||
pub use sse::ws_ai_stream;
|
pub use sse::ws_ai_stream;
|
||||||
pub use types::{WsError, WsInMessage, WsOutEvent, WS_PROTOCOL_VERSION};
|
pub use types::{WS_PROTOCOL_VERSION, WsError, WsInMessage, WsOutEvent};
|
||||||
|
|||||||
@ -84,10 +84,7 @@ pub struct TransportSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TransportSession {
|
impl TransportSession {
|
||||||
pub fn new(
|
pub fn new(user: WsUserCtx, service: Arc<AppService>) -> Self {
|
||||||
user: WsUserCtx,
|
|
||||||
service: Arc<AppService>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
user,
|
user,
|
||||||
subscriptions: Arc::new(DashMap::new()),
|
subscriptions: Arc::new(DashMap::new()),
|
||||||
@ -116,7 +113,11 @@ impl TransportSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsubscribe_room(&self, room_id: RoomId) {
|
pub async fn unsubscribe_room(&self, room_id: RoomId) {
|
||||||
self.service.room.room_manager.unsubscribe(room_id, self.user.user_id).await;
|
self.service
|
||||||
|
.room
|
||||||
|
.room_manager
|
||||||
|
.unsubscribe(room_id, self.user.user_id)
|
||||||
|
.await;
|
||||||
self.subscriptions.remove(&room_id);
|
self.subscriptions.remove(&room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +130,11 @@ impl TransportSession {
|
|||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
sender_type: Some("user".to_string()),
|
sender_type: Some("user".to_string()),
|
||||||
};
|
};
|
||||||
self.service.room.room_manager.broadcast_typing(room_id, event).await;
|
self.service
|
||||||
|
.room
|
||||||
|
.room_manager
|
||||||
|
.broadcast_typing(room_id, event)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current project context from cache (populated on first subscription).
|
/// Get the current project context from cache (populated on first subscription).
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
use actix_web::{HttpRequest, HttpResponse, web};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
|
use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use service::AppService;
|
|
||||||
use queue::RoomMessageStreamChunkEvent;
|
use queue::RoomMessageStreamChunkEvent;
|
||||||
|
use service::AppService;
|
||||||
|
|
||||||
/// SSE endpoint: GET /ws/ai-stream/{room_id}/{message_id}
|
/// SSE endpoint: GET /ws/ai-stream/{room_id}/{message_id}
|
||||||
pub async fn ws_ai_stream(
|
pub async fn ws_ai_stream(
|
||||||
@ -30,7 +30,9 @@ pub async fn ws_ai_stream(
|
|||||||
return Err(actix_web::error::ErrorUnauthorized("invalid auth header"));
|
return Err(actix_web::error::ErrorUnauthorized("invalid auth header"));
|
||||||
}
|
}
|
||||||
} else if let Some(token) = req.uri().query().and_then(|q| {
|
} else if let Some(token) = req.uri().query().and_then(|q| {
|
||||||
q.split('&').find(|p| p.starts_with("token=")).and_then(|p| p.split('=').nth(1))
|
q.split('&')
|
||||||
|
.find(|p| p.starts_with("token="))
|
||||||
|
.and_then(|p| p.split('=').nth(1))
|
||||||
}) {
|
}) {
|
||||||
match service.ws_token.validate_token(token).await {
|
match service.ws_token.validate_token(token).await {
|
||||||
Ok(uid) => uid,
|
Ok(uid) => uid,
|
||||||
@ -55,21 +57,23 @@ pub async fn ws_ai_stream(
|
|||||||
None => return Err(actix_web::error::ErrorNotFound("stream not found").into()),
|
None => return Err(actix_web::error::ErrorNotFound("stream not found").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sse_stream = BroadcastStream::new(stream_rx)
|
let sse_stream = BroadcastStream::new(stream_rx).map(move |result| match result {
|
||||||
.map(move |result| match result {
|
Ok(chunk) => {
|
||||||
Ok(chunk) => {
|
let data = format_sse_chunk(&chunk);
|
||||||
let data = format_sse_chunk(&chunk);
|
if chunk.done {
|
||||||
if chunk.done {
|
Ok::<_, std::io::Error>(Bytes::from(format!("{}event: done\ndata: \n\n", data)))
|
||||||
Ok::<_, std::io::Error>(Bytes::from(format!("{}event: done\ndata: \n\n", data)))
|
} else {
|
||||||
} else {
|
Ok::<_, std::io::Error>(Bytes::from(data))
|
||||||
Ok::<_, std::io::Error>(Bytes::from(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(BroadcastStreamRecvError::Lagged(_)) => {
|
}
|
||||||
tracing::warn!(message_id = %message_id, "SSE subscriber lagged");
|
Err(BroadcastStreamRecvError::Lagged(_)) => {
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "stream lagged"))
|
tracing::warn!(message_id = %message_id, "SSE subscriber lagged");
|
||||||
}
|
Err(std::io::Error::new(
|
||||||
});
|
std::io::ErrorKind::TimedOut,
|
||||||
|
"stream lagged",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
.content_type("text/event-stream")
|
.content_type("text/event-stream")
|
||||||
|
|||||||
@ -11,56 +11,215 @@ pub const WS_PROTOCOL_VERSION: u32 = 1;
|
|||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum WsInMessage {
|
pub enum WsInMessage {
|
||||||
Ping,
|
Ping,
|
||||||
Subscribe { room: RoomId },
|
Subscribe {
|
||||||
Unsubscribe { room: RoomId },
|
room: RoomId,
|
||||||
TypingStart { room: RoomId },
|
},
|
||||||
TypingStop { room: RoomId },
|
Unsubscribe {
|
||||||
ReadReceipt { room: RoomId, last_read_seq: i64 },
|
room: RoomId,
|
||||||
MessageList { room: RoomId, before_seq: Option<i64>, after_seq: Option<i64>, limit: Option<u64> },
|
},
|
||||||
MessageCreate { room: RoomId, content: String, content_type: Option<String>, thread: Option<RoomThreadId>, in_reply_to: Option<Uuid> },
|
TypingStart {
|
||||||
MessageUpdate { message: Uuid, content: String },
|
room: RoomId,
|
||||||
MessageRevoke { message: Uuid },
|
},
|
||||||
RoomGet { room: RoomId },
|
TypingStop {
|
||||||
RoomCreate { project: ProjectId, room_name: String, public: bool, category: Option<Uuid> },
|
room: RoomId,
|
||||||
RoomUpdate { room: RoomId, room_name: Option<String>, public: Option<bool>, category: Option<Uuid> },
|
},
|
||||||
RoomDelete { room: RoomId },
|
ReadReceipt {
|
||||||
CategoryCreate { project: ProjectId, name: String, position: Option<i32> },
|
room: RoomId,
|
||||||
CategoryUpdate { id: Uuid, name: Option<String>, position: Option<i32> },
|
last_read_seq: i64,
|
||||||
CategoryDelete { id: Uuid },
|
},
|
||||||
AccessGrant { room: RoomId, user: UserId },
|
MessageList {
|
||||||
AccessRevoke { room: RoomId, user: UserId },
|
room: RoomId,
|
||||||
StateSetReadSeq { room: RoomId, last_read_seq: i64 },
|
before_seq: Option<i64>,
|
||||||
StateUpdateDnd { room: RoomId, do_not_disturb: Option<bool>, dnd_start_hour: Option<i16>, dnd_end_hour: Option<i16> },
|
after_seq: Option<i64>,
|
||||||
ReactionAdd { room: RoomId, message: Uuid, emoji: String },
|
limit: Option<u64>,
|
||||||
ReactionRemove { room: RoomId, message: Uuid, emoji: String },
|
},
|
||||||
ThreadCreate { room: RoomId, parent: i64 },
|
MessageCreate {
|
||||||
ThreadResolve { thread_id: RoomThreadId },
|
room: RoomId,
|
||||||
ThreadArchive { thread_id: RoomThreadId },
|
content: String,
|
||||||
PinAdd { room: RoomId, message: Uuid },
|
content_type: Option<String>,
|
||||||
PinRemove { room: RoomId, message: Uuid },
|
thread: Option<RoomThreadId>,
|
||||||
DraftSave { room: RoomId, content: String },
|
in_reply_to: Option<Uuid>,
|
||||||
DraftClear { room: RoomId },
|
},
|
||||||
Search { q: String, room: Option<RoomId>, start_time: Option<DateTime<Utc>>, end_time: Option<DateTime<Utc>>, sender_id: Option<Uuid>, content_type: Option<String>, limit: Option<u64>, offset: Option<u64> },
|
MessageUpdate {
|
||||||
NotificationMarkRead { id: Uuid },
|
message: Uuid,
|
||||||
NotificationMarkAllRead { project_id: Option<ProjectId> },
|
content: String,
|
||||||
NotificationArchive { id: Uuid },
|
},
|
||||||
PresenceUpdate { status: crate::event::presence::UserPresenceStatus },
|
MessageRevoke {
|
||||||
CustomStatusUpdate { emoji: Option<String>, text: Option<String>, expires_at: Option<DateTime<Utc>> },
|
message: Uuid,
|
||||||
InviteCreate { project: ProjectId, room: Option<RoomId>, max_uses: Option<i32>, expires_at: Option<DateTime<Utc>> },
|
},
|
||||||
InviteAccept { code: String },
|
RoomGet {
|
||||||
InviteRevoke { id: Uuid },
|
room: RoomId,
|
||||||
BanCreate { project: ProjectId, user: UserId, reason: Option<String>, expires_at: Option<DateTime<Utc>> },
|
},
|
||||||
BanRemove { project: ProjectId, user: UserId },
|
RoomCreate {
|
||||||
VoiceJoin { room: RoomId },
|
project: ProjectId,
|
||||||
VoiceLeave { room: RoomId },
|
room_name: String,
|
||||||
VoiceMute { room: RoomId, muted: bool },
|
public: bool,
|
||||||
VoiceDeaf { room: RoomId, deafened: bool },
|
category: Option<Uuid>,
|
||||||
ScreenShare { room: RoomId, start: bool },
|
},
|
||||||
AiList { room: RoomId },
|
RoomUpdate {
|
||||||
AiUpsert { room: RoomId, model: Uuid, version: Option<Uuid>, system_prompt: Option<String>, temperature: Option<f64>, max_tokens: Option<i64>, stream: Option<bool> },
|
room: RoomId,
|
||||||
AiDelete { room: RoomId, agent_id: Uuid },
|
room_name: Option<String>,
|
||||||
AiStop { room: RoomId },
|
public: Option<bool>,
|
||||||
UserSummary { username: String },
|
category: Option<Uuid>,
|
||||||
|
},
|
||||||
|
RoomDelete {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
CategoryCreate {
|
||||||
|
project: ProjectId,
|
||||||
|
name: String,
|
||||||
|
position: Option<i32>,
|
||||||
|
},
|
||||||
|
CategoryUpdate {
|
||||||
|
id: Uuid,
|
||||||
|
name: Option<String>,
|
||||||
|
position: Option<i32>,
|
||||||
|
},
|
||||||
|
CategoryDelete {
|
||||||
|
id: Uuid,
|
||||||
|
},
|
||||||
|
AccessGrant {
|
||||||
|
room: RoomId,
|
||||||
|
user: UserId,
|
||||||
|
},
|
||||||
|
AccessRevoke {
|
||||||
|
room: RoomId,
|
||||||
|
user: UserId,
|
||||||
|
},
|
||||||
|
StateSetReadSeq {
|
||||||
|
room: RoomId,
|
||||||
|
last_read_seq: i64,
|
||||||
|
},
|
||||||
|
StateUpdateDnd {
|
||||||
|
room: RoomId,
|
||||||
|
do_not_disturb: Option<bool>,
|
||||||
|
dnd_start_hour: Option<i16>,
|
||||||
|
dnd_end_hour: Option<i16>,
|
||||||
|
},
|
||||||
|
ReactionAdd {
|
||||||
|
room: RoomId,
|
||||||
|
message: Uuid,
|
||||||
|
emoji: String,
|
||||||
|
},
|
||||||
|
ReactionRemove {
|
||||||
|
room: RoomId,
|
||||||
|
message: Uuid,
|
||||||
|
emoji: String,
|
||||||
|
},
|
||||||
|
ThreadCreate {
|
||||||
|
room: RoomId,
|
||||||
|
parent: i64,
|
||||||
|
},
|
||||||
|
ThreadResolve {
|
||||||
|
thread_id: RoomThreadId,
|
||||||
|
},
|
||||||
|
ThreadArchive {
|
||||||
|
thread_id: RoomThreadId,
|
||||||
|
},
|
||||||
|
PinAdd {
|
||||||
|
room: RoomId,
|
||||||
|
message: Uuid,
|
||||||
|
},
|
||||||
|
PinRemove {
|
||||||
|
room: RoomId,
|
||||||
|
message: Uuid,
|
||||||
|
},
|
||||||
|
DraftSave {
|
||||||
|
room: RoomId,
|
||||||
|
content: String,
|
||||||
|
},
|
||||||
|
DraftClear {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
Search {
|
||||||
|
q: String,
|
||||||
|
room: Option<RoomId>,
|
||||||
|
start_time: Option<DateTime<Utc>>,
|
||||||
|
end_time: Option<DateTime<Utc>>,
|
||||||
|
sender_id: Option<Uuid>,
|
||||||
|
content_type: Option<String>,
|
||||||
|
limit: Option<u64>,
|
||||||
|
offset: Option<u64>,
|
||||||
|
},
|
||||||
|
NotificationMarkRead {
|
||||||
|
id: Uuid,
|
||||||
|
},
|
||||||
|
NotificationMarkAllRead {
|
||||||
|
project_id: Option<ProjectId>,
|
||||||
|
},
|
||||||
|
NotificationArchive {
|
||||||
|
id: Uuid,
|
||||||
|
},
|
||||||
|
PresenceUpdate {
|
||||||
|
status: crate::event::presence::UserPresenceStatus,
|
||||||
|
},
|
||||||
|
CustomStatusUpdate {
|
||||||
|
emoji: Option<String>,
|
||||||
|
text: Option<String>,
|
||||||
|
expires_at: Option<DateTime<Utc>>,
|
||||||
|
},
|
||||||
|
InviteCreate {
|
||||||
|
project: ProjectId,
|
||||||
|
room: Option<RoomId>,
|
||||||
|
max_uses: Option<i32>,
|
||||||
|
expires_at: Option<DateTime<Utc>>,
|
||||||
|
},
|
||||||
|
InviteAccept {
|
||||||
|
code: String,
|
||||||
|
},
|
||||||
|
InviteRevoke {
|
||||||
|
id: Uuid,
|
||||||
|
},
|
||||||
|
BanCreate {
|
||||||
|
project: ProjectId,
|
||||||
|
user: UserId,
|
||||||
|
reason: Option<String>,
|
||||||
|
expires_at: Option<DateTime<Utc>>,
|
||||||
|
},
|
||||||
|
BanRemove {
|
||||||
|
project: ProjectId,
|
||||||
|
user: UserId,
|
||||||
|
},
|
||||||
|
VoiceJoin {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
VoiceLeave {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
VoiceMute {
|
||||||
|
room: RoomId,
|
||||||
|
muted: bool,
|
||||||
|
},
|
||||||
|
VoiceDeaf {
|
||||||
|
room: RoomId,
|
||||||
|
deafened: bool,
|
||||||
|
},
|
||||||
|
ScreenShare {
|
||||||
|
room: RoomId,
|
||||||
|
start: bool,
|
||||||
|
},
|
||||||
|
AiList {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
AiUpsert {
|
||||||
|
room: RoomId,
|
||||||
|
model: Uuid,
|
||||||
|
version: Option<Uuid>,
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
temperature: Option<f64>,
|
||||||
|
max_tokens: Option<i64>,
|
||||||
|
stream: Option<bool>,
|
||||||
|
},
|
||||||
|
AiDelete {
|
||||||
|
room: RoomId,
|
||||||
|
agent_id: Uuid,
|
||||||
|
},
|
||||||
|
AiStop {
|
||||||
|
room: RoomId,
|
||||||
|
},
|
||||||
|
UserSummary {
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WsInMessage {
|
impl WsInMessage {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
mod in_message;
|
mod in_message;
|
||||||
mod out_event;
|
mod out_event;
|
||||||
|
|
||||||
pub use in_message::{WsInMessage, WS_PROTOCOL_VERSION};
|
pub use in_message::{WS_PROTOCOL_VERSION, WsInMessage};
|
||||||
pub use out_event::{WsError, WsOutEvent};
|
pub use out_event::{WsError, WsOutEvent};
|
||||||
@ -1,11 +1,11 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use models::{ProjectId, RoomId, UserId};
|
|
||||||
use crate::event::{
|
use crate::event::{
|
||||||
ai, attachment, ban, category, draft, invite, member, message, notify, pin, presence, project,
|
ai, attachment, ban, category, draft, invite, member, message, notify, pin, presence, project,
|
||||||
reaction, rooms, search, thread, voice,
|
reaction, rooms, search, thread, voice,
|
||||||
};
|
};
|
||||||
|
use models::{ProjectId, RoomId, UserId};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::panic::AssertUnwindSafe;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse, web};
|
||||||
use actix_ws::Message as WsMessage;
|
use actix_ws::Message as WsMessage;
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -11,7 +11,10 @@ use service::AppService;
|
|||||||
|
|
||||||
use super::inbound::MessageHandler;
|
use super::inbound::MessageHandler;
|
||||||
use super::poll::{poll_notifications, poll_subscriptions};
|
use super::poll::{poll_notifications, poll_subscriptions};
|
||||||
use super::session::{TransportSession, WsUserCtx, HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, MAX_IDLE_TIMEOUT, MAX_TEXT_MESSAGE_LEN, MAX_MESSAGES_PER_SECOND};
|
use super::session::{
|
||||||
|
HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, MAX_IDLE_TIMEOUT, MAX_MESSAGES_PER_SECOND,
|
||||||
|
MAX_TEXT_MESSAGE_LEN, TransportSession, WsUserCtx,
|
||||||
|
};
|
||||||
use super::types::{WsInMessage, WsOutEvent};
|
use super::types::{WsInMessage, WsOutEvent};
|
||||||
|
|
||||||
/// Universal WebSocket endpoint: `/ws`
|
/// Universal WebSocket endpoint: `/ws`
|
||||||
@ -183,10 +186,11 @@ pub async fn ws_handler(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(user_id = %user_id, error = %e, "WS message processing failed");
|
tracing::warn!(user_id = %user_id, error = %e, "WS message processing failed");
|
||||||
let rid = request_id.unwrap_or(Uuid::nil());
|
let rid = request_id.unwrap_or(Uuid::nil());
|
||||||
|
let (code, error_type) = e.ws_error_code();
|
||||||
let err_json = serde_json::json!({
|
let err_json = serde_json::json!({
|
||||||
"type": "error",
|
"type": "error",
|
||||||
"code": 500,
|
"code": code,
|
||||||
"error": "internal_error",
|
"error": error_type,
|
||||||
"message": e.to_string(),
|
"message": e.to_string(),
|
||||||
"_request_id": rid
|
"_request_id": rid
|
||||||
});
|
});
|
||||||
@ -202,14 +206,21 @@ pub async fn ws_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(WsMessage::Binary(_))) => { break; }
|
Some(Ok(WsMessage::Binary(_))) => {
|
||||||
|
let _ = ws_session.close(Some(actix_ws::CloseCode::Unsupported.into())).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
Some(Ok(WsMessage::Continuation(_))) => {}
|
Some(Ok(WsMessage::Continuation(_))) => {}
|
||||||
Some(Ok(WsMessage::Nop)) => {}
|
Some(Ok(WsMessage::Nop)) => {}
|
||||||
Some(Ok(WsMessage::Close(reason))) => {
|
Some(Ok(WsMessage::Close(reason))) => {
|
||||||
let _ = ws_session.close(reason).await;
|
let _ = ws_session.close(reason).await;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Some(Err(e)) => { tracing::warn!(error = %e, "WS transport error"); break; }
|
Some(Err(e)) => {
|
||||||
|
tracing::warn!(error = %e, "WS transport error");
|
||||||
|
let _ = ws_session.close(Some(actix_ws::CloseCode::Protocol.into())).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,6 +232,9 @@ pub async fn ws_handler(
|
|||||||
manager.unsubscribe(sub.room_id, user_id).await;
|
manager.unsubscribe(sub.room_id, user_id).await;
|
||||||
}
|
}
|
||||||
manager.unsubscribe_user_notification(user_id).await;
|
manager.unsubscribe_user_notification(user_id).await;
|
||||||
|
// Remove presence entry so disconnected users don't appear online for up to 10 minutes
|
||||||
|
let project_id = session.project_id.lock().await;
|
||||||
|
session.service.room.remove_user_presence(user_id, *project_id).await;
|
||||||
manager.metrics.ws_connections_active.decrement(1.0);
|
manager.metrics.ws_connections_active.decrement(1.0);
|
||||||
manager.metrics.ws_disconnections_total.increment(1);
|
manager.metrics.ws_disconnections_total.increment(1);
|
||||||
}).catch_unwind();
|
}).catch_unwind();
|
||||||
@ -264,13 +278,20 @@ async fn authenticate_ws(
|
|||||||
if let Ok(auth_str) = auth_header.to_str() {
|
if let Ok(auth_str) = auth_header.to_str() {
|
||||||
if let Some(token) = auth_str.strip_prefix("Bearer ") {
|
if let Some(token) = auth_str.strip_prefix("Bearer ") {
|
||||||
match service.ws_token.validate_token_ctx(token).await {
|
match service.ws_token.validate_token_ctx(token).await {
|
||||||
Ok(ctx) => return Ok(crate::token::AppTransportTokenContext {
|
Ok(ctx) => {
|
||||||
user_id: ctx.user_id,
|
return Ok(crate::token::AppTransportTokenContext {
|
||||||
device_id: ctx.device_id.unwrap_or_default(),
|
user_id: ctx.user_id,
|
||||||
client_id: ctx.client_id.unwrap_or_default(),
|
device_id: ctx.device_id.unwrap_or_default(),
|
||||||
}),
|
client_id: ctx.client_id.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
service.room.room_manager.metrics.ws_auth_failures.increment(1);
|
service
|
||||||
|
.room
|
||||||
|
.room_manager
|
||||||
|
.metrics
|
||||||
|
.ws_auth_failures
|
||||||
|
.increment(1);
|
||||||
return Err(actix_web::error::ErrorUnauthorized("token auth failed"));
|
return Err(actix_web::error::ErrorUnauthorized("token auth failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,21 +301,35 @@ async fn authenticate_ws(
|
|||||||
|
|
||||||
// Fallback: token in query string (deprecated, kept for backward compatibility)
|
// Fallback: token in query string (deprecated, kept for backward compatibility)
|
||||||
if let Some(token) = req.uri().query().and_then(|q| {
|
if let Some(token) = req.uri().query().and_then(|q| {
|
||||||
q.split('&').find(|p| p.starts_with("token=")).and_then(|p| p.split('=').nth(1))
|
q.split('&')
|
||||||
|
.find(|p| p.starts_with("token="))
|
||||||
|
.and_then(|p| p.split('=').nth(1))
|
||||||
}) {
|
}) {
|
||||||
match service.ws_token.validate_token_ctx(token).await {
|
match service.ws_token.validate_token_ctx(token).await {
|
||||||
Ok(ctx) => return Ok(crate::token::AppTransportTokenContext {
|
Ok(ctx) => {
|
||||||
user_id: ctx.user_id,
|
return Ok(crate::token::AppTransportTokenContext {
|
||||||
device_id: ctx.device_id.unwrap_or_default(),
|
user_id: ctx.user_id,
|
||||||
client_id: ctx.client_id.unwrap_or_default(),
|
device_id: ctx.device_id.unwrap_or_default(),
|
||||||
}),
|
client_id: ctx.client_id.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
service.room.room_manager.metrics.ws_auth_failures.increment(1);
|
service
|
||||||
|
.room
|
||||||
|
.room_manager
|
||||||
|
.metrics
|
||||||
|
.ws_auth_failures
|
||||||
|
.increment(1);
|
||||||
return Err(actix_web::error::ErrorUnauthorized("token auth failed"));
|
return Err(actix_web::error::ErrorUnauthorized("token auth failed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.room.room_manager.metrics.ws_auth_failures.increment(1);
|
service
|
||||||
|
.room
|
||||||
|
.room_manager
|
||||||
|
.metrics
|
||||||
|
.ws_auth_failures
|
||||||
|
.increment(1);
|
||||||
Err(actix_web::error::ErrorUnauthorized("no auth provided"))
|
Err(actix_web::error::ErrorUnauthorized("no auth provided"))
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use crate::seq::SeqAllocator;
|
||||||
use config::AppConfig;
|
use config::AppConfig;
|
||||||
use db::cache::AppCache;
|
use db::cache::AppCache;
|
||||||
use db::database::AppDatabase;
|
use db::database::AppDatabase;
|
||||||
use service::AppService;
|
use service::AppService;
|
||||||
use crate::seq::SeqAllocator;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppTransport {
|
pub struct AppTransport {
|
||||||
@ -103,7 +103,11 @@ impl AppTransport {
|
|||||||
Self::build(service, config, nats)
|
Self::build(service, config, nats)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(service: AppService, config: AppConfig, nats: Option<async_nats::Client>) -> Result<Self, crate::error::AppTransportError> {
|
fn build(
|
||||||
|
service: AppService,
|
||||||
|
config: AppConfig,
|
||||||
|
nats: Option<async_nats::Client>,
|
||||||
|
) -> Result<Self, crate::error::AppTransportError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: service.db.clone(),
|
db: service.db.clone(),
|
||||||
cache: service.cache.clone(),
|
cache: service.cache.clone(),
|
||||||
@ -151,7 +155,10 @@ impl AppTransport {
|
|||||||
self.seq.seq(room).await
|
self.seq.seq(room).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bootstrap_seq(&self, room: models::RoomId) -> Result<i64, crate::error::AppTransportError> {
|
pub async fn bootstrap_seq(
|
||||||
|
&self,
|
||||||
|
room: models::RoomId,
|
||||||
|
) -> Result<i64, crate::error::AppTransportError> {
|
||||||
self.seq.bootstrap(room).await
|
self.seq.bootstrap(room).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,22 +19,27 @@ impl TransportMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment_sent(&self) {
|
pub fn increment_sent(&self) {
|
||||||
self.messages_sent.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
self.messages_sent
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment_received(&self) {
|
pub fn increment_received(&self) {
|
||||||
self.messages_received.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
self.messages_received
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment_failed(&self) {
|
pub fn increment_failed(&self) {
|
||||||
self.messages_failed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
self.messages_failed
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment_connections(&self) {
|
pub fn increment_connections(&self) {
|
||||||
self.active_connections.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
self.active_connections
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrement_connections(&self) {
|
pub fn decrement_connections(&self) {
|
||||||
self.active_connections.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
self.active_connections
|
||||||
|
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,8 +49,8 @@ impl MessagePagination {
|
|||||||
&self,
|
&self,
|
||||||
params: PaginationParams,
|
params: PaginationParams,
|
||||||
) -> Result<MessagePage, crate::error::AppTransportError> {
|
) -> Result<MessagePage, crate::error::AppTransportError> {
|
||||||
use sea_orm::*;
|
|
||||||
use models::rooms::room_message;
|
use models::rooms::room_message;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
let limit = std::cmp::Ord::min(params.limit, 100);
|
let limit = std::cmp::Ord::min(params.limit, 100);
|
||||||
let cursor_seq = if let Some(cursor) = params.cursor {
|
let cursor_seq = if let Some(cursor) = params.cursor {
|
||||||
@ -59,18 +59,16 @@ impl MessagePagination {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut query = room_message::Entity::find()
|
let mut query =
|
||||||
.filter(room_message::Column::Room.eq(params.room_id));
|
room_message::Entity::find().filter(room_message::Column::Room.eq(params.room_id));
|
||||||
|
|
||||||
query = match (params.direction, cursor_seq) {
|
query = match (params.direction, cursor_seq) {
|
||||||
(PaginationDirection::Before, Some(seq)) => {
|
(PaginationDirection::Before, Some(seq)) => query
|
||||||
query.filter(room_message::Column::Seq.lt(seq))
|
.filter(room_message::Column::Seq.lt(seq))
|
||||||
.order_by_desc(room_message::Column::Seq)
|
.order_by_desc(room_message::Column::Seq),
|
||||||
}
|
(PaginationDirection::After, Some(seq)) => query
|
||||||
(PaginationDirection::After, Some(seq)) => {
|
.filter(room_message::Column::Seq.gt(seq))
|
||||||
query.filter(room_message::Column::Seq.gt(seq))
|
.order_by_asc(room_message::Column::Seq),
|
||||||
.order_by_asc(room_message::Column::Seq)
|
|
||||||
}
|
|
||||||
_ => query.order_by_desc(room_message::Column::Seq),
|
_ => query.order_by_desc(room_message::Column::Seq),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,8 +117,8 @@ impl MessagePagination {
|
|||||||
message_id: Uuid,
|
message_id: Uuid,
|
||||||
context_size: u64,
|
context_size: u64,
|
||||||
) -> Result<MessagePage, crate::error::AppTransportError> {
|
) -> Result<MessagePage, crate::error::AppTransportError> {
|
||||||
use sea_orm::*;
|
|
||||||
use models::rooms::room_message;
|
use models::rooms::room_message;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
let target = room_message::Entity::find_by_id(message_id)
|
let target = room_message::Entity::find_by_id(message_id)
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
use redis::AsyncCommands;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use redis::AsyncCommands;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ClientState {
|
pub struct ClientState {
|
||||||
@ -40,10 +40,14 @@ impl ReconnectManager {
|
|||||||
let key = format!("client:state:{}:{}", user_id, room_id);
|
let key = format!("client:state:{}:{}", user_id, room_id);
|
||||||
let value = last_seq.to_string();
|
let value = last_seq.to_string();
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let _: () = conn.set_ex(&key, &value, 86400)
|
let _: () = conn
|
||||||
|
.set_ex(&key, &value, 86400)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
@ -57,10 +61,15 @@ impl ReconnectManager {
|
|||||||
) -> Result<Option<i64>, crate::error::AppTransportError> {
|
) -> Result<Option<i64>, crate::error::AppTransportError> {
|
||||||
let key = format!("client:state:{}:{}", user_id, room_id);
|
let key = format!("client:state:{}:{}", user_id, room_id);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let value: Option<String> = conn.get(&key).await
|
let value: Option<String> = conn
|
||||||
|
.get(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
Ok(value.and_then(|v| v.parse::<i64>().ok()))
|
Ok(value.and_then(|v| v.parse::<i64>().ok()))
|
||||||
@ -72,8 +81,8 @@ impl ReconnectManager {
|
|||||||
room_id: Uuid,
|
room_id: Uuid,
|
||||||
since_seq: i64,
|
since_seq: i64,
|
||||||
) -> Result<Vec<MissedMessage>, crate::error::AppTransportError> {
|
) -> Result<Vec<MissedMessage>, crate::error::AppTransportError> {
|
||||||
use sea_orm::*;
|
|
||||||
use models::rooms::room_message;
|
use models::rooms::room_message;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
let messages = room_message::Entity::find()
|
let messages = room_message::Entity::find()
|
||||||
.filter(room_message::Column::Room.eq(room_id))
|
.filter(room_message::Column::Room.eq(room_id))
|
||||||
@ -107,7 +116,9 @@ impl ReconnectManager {
|
|||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
for (room_id, client_seq) in room_states {
|
for (room_id, client_seq) in room_states {
|
||||||
let missed = self.get_missed_messages(user_id, room_id, client_seq).await?;
|
let missed = self
|
||||||
|
.get_missed_messages(user_id, room_id, client_seq)
|
||||||
|
.await?;
|
||||||
if !missed.is_empty() {
|
if !missed.is_empty() {
|
||||||
result.insert(room_id, missed);
|
result.insert(room_id, missed);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,7 @@ pub enum BlockType {
|
|||||||
Image,
|
Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RichTextRenderer {
|
pub struct RichTextRenderer {}
|
||||||
}
|
|
||||||
|
|
||||||
impl RichTextRenderer {
|
impl RichTextRenderer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@ -59,14 +58,29 @@ impl RichTextRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_to_html(&self, blocks: &[RichTextBlock]) -> String {
|
pub fn render_to_html(&self, blocks: &[RichTextBlock]) -> String {
|
||||||
blocks.iter()
|
blocks
|
||||||
|
.iter()
|
||||||
.map(|block| match block.block_type {
|
.map(|block| match block.block_type {
|
||||||
BlockType::Text => format!("<p>{}</p>", html_escape(&block.content)),
|
BlockType::Text => format!("<p>{}</p>", html_escape(&block.content)),
|
||||||
BlockType::Code => format!("<pre><code>{}</code></pre>", html_escape(&block.content)),
|
BlockType::Code => {
|
||||||
BlockType::Quote => format!("<blockquote>{}</blockquote>", html_escape(&block.content)),
|
format!("<pre><code>{}</code></pre>", html_escape(&block.content))
|
||||||
BlockType::Link => format!("<a href=\"{}\">{}</a>", html_escape(&block.content), html_escape(&block.content)),
|
}
|
||||||
BlockType::Mention => format!("<span class=\"mention\">@{}</span>", html_escape(&block.content)),
|
BlockType::Quote => {
|
||||||
BlockType::Emoji => format!("<span class=\"emoji\">{}</span>", html_escape(&block.content)),
|
format!("<blockquote>{}</blockquote>", html_escape(&block.content))
|
||||||
|
}
|
||||||
|
BlockType::Link => format!(
|
||||||
|
"<a href=\"{}\">{}</a>",
|
||||||
|
html_escape(&block.content),
|
||||||
|
html_escape(&block.content)
|
||||||
|
),
|
||||||
|
BlockType::Mention => format!(
|
||||||
|
"<span class=\"mention\">@{}</span>",
|
||||||
|
html_escape(&block.content)
|
||||||
|
),
|
||||||
|
BlockType::Emoji => format!(
|
||||||
|
"<span class=\"emoji\">{}</span>",
|
||||||
|
html_escape(&block.content)
|
||||||
|
),
|
||||||
BlockType::Image => format!("<img src=\"{}\" />", html_escape(&block.content)),
|
BlockType::Image => format!("<img src=\"{}\" />", html_escape(&block.content)),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|||||||
@ -40,8 +40,8 @@ impl SearchEngine {
|
|||||||
&self,
|
&self,
|
||||||
query: SearchQuery,
|
query: SearchQuery,
|
||||||
) -> Result<SearchResult, crate::error::AppTransportError> {
|
) -> Result<SearchResult, crate::error::AppTransportError> {
|
||||||
use sea_orm::*;
|
|
||||||
use models::rooms::room_message;
|
use models::rooms::room_message;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
let mut db_query = room_message::Entity::find();
|
let mut db_query = room_message::Entity::find();
|
||||||
|
|
||||||
@ -53,14 +53,18 @@ impl SearchEngine {
|
|||||||
db_query = db_query.filter(room_message::Column::SenderId.eq(user_id));
|
db_query = db_query.filter(room_message::Column::SenderId.eq(user_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
let escaped_query = query.query
|
let escaped_query = query
|
||||||
|
.query
|
||||||
.replace('\\', "\\\\")
|
.replace('\\', "\\\\")
|
||||||
.replace('%', "\\%")
|
.replace('%', "\\%")
|
||||||
.replace('_', "\\_");
|
.replace('_', "\\_");
|
||||||
let search_term = format!("%{}%", escaped_query);
|
let search_term = format!("%{}%", escaped_query);
|
||||||
db_query = db_query.filter(room_message::Column::Content.like(&search_term));
|
db_query = db_query.filter(room_message::Column::Content.like(&search_term));
|
||||||
|
|
||||||
let total = db_query.clone().count(&self.db).await
|
let total = db_query
|
||||||
|
.clone()
|
||||||
|
.count(&self.db)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let messages = db_query
|
let messages = db_query
|
||||||
|
|||||||
@ -25,7 +25,10 @@ impl RateLimiter {
|
|||||||
) -> Result<bool, crate::error::AppTransportError> {
|
) -> Result<bool, crate::error::AppTransportError> {
|
||||||
let key = format!("ratelimit:{}:{}", user_id, action);
|
let key = format!("ratelimit:{}:{}", user_id, action);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
// Atomic INCR with EX NX — sets TTL only on first creation
|
// Atomic INCR with EX NX — sets TTL only on first creation
|
||||||
@ -57,10 +60,15 @@ impl RateLimiter {
|
|||||||
) -> Result<u32, crate::error::AppTransportError> {
|
) -> Result<u32, crate::error::AppTransportError> {
|
||||||
let key = format!("ratelimit:{}:{}", user_id, action);
|
let key = format!("ratelimit:{}:{}", user_id, action);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let count: Option<u32> = conn.get(&key).await
|
let count: Option<u32> = conn
|
||||||
|
.get(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let current = count.unwrap_or(0);
|
let current = count.unwrap_or(0);
|
||||||
@ -85,10 +93,14 @@ impl CsrfProtection {
|
|||||||
let token = Uuid::new_v4().to_string();
|
let token = Uuid::new_v4().to_string();
|
||||||
let key = format!("csrf:{}:{}", user_id, token);
|
let key = format!("csrf:{}:{}", user_id, token);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let _: () = conn.set_ex(&key, "1", 3600)
|
let _: () = conn
|
||||||
|
.set_ex(&key, "1", 3600)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
@ -102,14 +114,21 @@ impl CsrfProtection {
|
|||||||
) -> Result<bool, crate::error::AppTransportError> {
|
) -> Result<bool, crate::error::AppTransportError> {
|
||||||
let key = format!("csrf:{}:{}", user_id, token);
|
let key = format!("csrf:{}:{}", user_id, token);
|
||||||
|
|
||||||
let mut conn = self.cache.conn().await
|
let mut conn = self
|
||||||
|
.cache
|
||||||
|
.conn()
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
let exists: bool = conn.exists(&key).await
|
let exists: bool = conn
|
||||||
|
.exists(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
let _: () = conn.del(&key).await
|
let _: () = conn
|
||||||
|
.del(&key)
|
||||||
|
.await
|
||||||
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
.map_err(|_| crate::error::AppTransportError::Internal)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicI64, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicI64, Ordering};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use hmac::{Hmac, Mac, KeyInit};
|
use hmac::{Hmac, KeyInit, Mac};
|
||||||
use models::UserId;
|
use models::UserId;
|
||||||
use session::Session;
|
use session::Session;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
use crate::error::AppTransportError;
|
|
||||||
use crate::AppTransport;
|
use crate::AppTransport;
|
||||||
|
use crate::error::AppTransportError;
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ impl UnreadManager {
|
|||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
room_id: Uuid,
|
room_id: Uuid,
|
||||||
) -> Result<i64, crate::error::AppTransportError> {
|
) -> Result<i64, crate::error::AppTransportError> {
|
||||||
use models::rooms::{room_user_state, room_message};
|
use models::rooms::{room_message, room_user_state};
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
|
|
||||||
let state = room_user_state::Entity::find_by_id((room_id, user_id))
|
let state = room_user_state::Entity::find_by_id((room_id, user_id))
|
||||||
@ -93,7 +93,7 @@ impl UnreadManager {
|
|||||||
&self,
|
&self,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
) -> Result<Vec<UnreadCount>, crate::error::AppTransportError> {
|
) -> Result<Vec<UnreadCount>, crate::error::AppTransportError> {
|
||||||
use models::rooms::{room_user_state, room_message};
|
use models::rooms::{room_message, room_user_state};
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
|
|
||||||
let states = room_user_state::Entity::find()
|
let states = room_user_state::Entity::find()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user