fixes and features
This commit is contained in:
+8
-4
@@ -12,7 +12,7 @@ use slug::slugify;
|
||||
|
||||
use crate::{
|
||||
db, queries,
|
||||
state::{NoteUpdate, SharedState},
|
||||
state::{NoteUpdate, RoomEvent, SharedState},
|
||||
};
|
||||
|
||||
const MAX_NAME_LENGTH: usize = 80;
|
||||
@@ -282,7 +282,7 @@ pub async fn restore(
|
||||
author: Some("restore".into()),
|
||||
owner_map: "[]".into(),
|
||||
};
|
||||
let _ = state.note_channel(&workspace_slug, ¬e_slug).await.send(update);
|
||||
let _ = state.note_channel(&workspace_slug, ¬e_slug).await.send(RoomEvent::Document(update));
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ pub async fn pad_restore(
|
||||
author: Some("restore".into()),
|
||||
owner_map,
|
||||
};
|
||||
let _ = state.pad_channel(&slug).await.send(update);
|
||||
let _ = state.pad_channel(&slug).await.send(RoomEvent::Document(update));
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
@@ -800,7 +800,11 @@ async fn serve_token_file(state: &SharedState, token: &str, filename: &str) -> R
|
||||
HeaderValue::from_str(mime.as_ref()).unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
|
||||
);
|
||||
response.headers_mut().insert(header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||
response.headers_mut().insert(header::CACHE_CONTROL, HeaderValue::from_static("public, max-age=600"));
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_str(&format!("public, max-age={}", state.file_cache_max_age_seconds))
|
||||
.expect("valid file cache-control header"),
|
||||
);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -11,7 +11,7 @@ use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer, trace::
|
||||
use crate::{api, auth, db, state::SharedState, websocket};
|
||||
use std::convert::Infallible;
|
||||
|
||||
pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize) -> Router {
|
||||
pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize, asset_cache_max_age_seconds: u64) -> Router {
|
||||
let asset_version = state.asset_version.clone();
|
||||
let asset_not_found = service_fn(move |_request| {
|
||||
let asset_version = asset_version.clone();
|
||||
@@ -28,6 +28,9 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
}
|
||||
});
|
||||
|
||||
let asset_cache_control = HeaderValue::from_str(&format!("public, max-age={asset_cache_max_age_seconds}"))
|
||||
.expect("valid asset cache-control header");
|
||||
|
||||
Router::new()
|
||||
.route("/", get(home))
|
||||
.route("/p/{slug}", get(pad))
|
||||
@@ -95,7 +98,7 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
ServiceBuilder::new()
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=600"),
|
||||
asset_cache_control,
|
||||
))
|
||||
.service(ServeDir::new(static_dir).not_found_service(asset_not_found)),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ pub struct Config {
|
||||
pub files_dir: String,
|
||||
pub upload_max_size_bytes: usize,
|
||||
pub asset_version: String,
|
||||
pub asset_cache_max_age_seconds: u64,
|
||||
pub file_cache_max_age_seconds: u64,
|
||||
pub smtp: Option<crate::state::SmtpConfig>,
|
||||
pub registration_enabled: bool,
|
||||
pub account_confirmation_required: bool,
|
||||
@@ -60,6 +62,8 @@ impl Config {
|
||||
.checked_mul(1024 * 1024)
|
||||
.ok_or("UPLOAD_MAX_SIZE_MB is too large")?,
|
||||
asset_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
asset_cache_max_age_seconds: env_nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?,
|
||||
file_cache_max_age_seconds: env_nonnegative_u64("FILE_CACHE_MAX_AGE_SECONDS", 600)?,
|
||||
smtp,
|
||||
registration_enabled: env_bool("REGISTRATION_ENABLED", false)?,
|
||||
account_confirmation_required: env_bool("ACCOUNT_CONFIRMATION_REQUIRED", false)?,
|
||||
@@ -100,3 +104,7 @@ fn env_positive_i64(name: &str, default: i64) -> Result<i64, Box<dyn std::error:
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn env_nonnegative_u64(name: &str, default: u64) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
Ok(env_var(name, &default.to_string()).parse()?)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
static_dir = %config.static_dir,
|
||||
files_dir = %config.files_dir,
|
||||
upload_max_size_bytes = config.upload_max_size_bytes,
|
||||
asset_cache_max_age_seconds = config.asset_cache_max_age_seconds,
|
||||
file_cache_max_age_seconds = config.file_cache_max_age_seconds,
|
||||
registration_enabled = config.registration_enabled,
|
||||
account_confirmation_required = config.account_confirmation_required,
|
||||
frontend_log_level = %config.frontend_log_level,
|
||||
@@ -56,6 +58,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
config.asset_version.clone(),
|
||||
config.files_dir.clone(),
|
||||
config.upload_max_size_bytes,
|
||||
config.file_cache_max_age_seconds,
|
||||
config.smtp.clone(),
|
||||
config.registration_enabled,
|
||||
config.account_confirmation_required,
|
||||
@@ -67,6 +70,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
state,
|
||||
&config.static_dir,
|
||||
config.upload_max_size_bytes,
|
||||
config.asset_cache_max_age_seconds,
|
||||
);
|
||||
let address = SocketAddr::new(config.host, config.port);
|
||||
let listener = TcpListener::bind(address).await?;
|
||||
|
||||
+54
-9
@@ -1,6 +1,7 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{collections::HashMap, sync::{Arc, atomic::{AtomicU64, Ordering}}};
|
||||
use crate::database::Database;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
use serde::Serialize;
|
||||
|
||||
const CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
@@ -18,36 +19,80 @@ pub struct NoteUpdate {
|
||||
pub owner_map: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PresenceUser {
|
||||
pub name: String,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RoomEvent {
|
||||
Document(NoteUpdate),
|
||||
Presence(Vec<PresenceUser>),
|
||||
Chat { sender: String, text: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppState {
|
||||
pub db: Database,
|
||||
pub asset_version: String,
|
||||
pub files_dir: String,
|
||||
pub upload_max_size_bytes: usize,
|
||||
pub file_cache_max_age_seconds: u64,
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
pub registration_enabled: bool,
|
||||
pub account_confirmation_required: bool,
|
||||
pub frontend_log_level: String,
|
||||
pub anonymous_access_token_ttl_days: i64,
|
||||
pub user_session_ttl_days: i64,
|
||||
channels: RwLock<HashMap<String, broadcast::Sender<NoteUpdate>>>,
|
||||
channels: RwLock<HashMap<String, broadcast::Sender<RoomEvent>>>,
|
||||
presence: RwLock<HashMap<String, HashMap<u64, PresenceUser>>>,
|
||||
next_connection_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db: Database, asset_version: String, files_dir: String, upload_max_size_bytes: usize, smtp: Option<SmtpConfig>, registration_enabled: bool, account_confirmation_required: bool, frontend_log_level: String, anonymous_access_token_ttl_days: i64, user_session_ttl_days: i64) -> Self {
|
||||
Self { db, asset_version, files_dir, upload_max_size_bytes, smtp, registration_enabled, account_confirmation_required, frontend_log_level, anonymous_access_token_ttl_days, user_session_ttl_days, channels: RwLock::new(HashMap::new()) }
|
||||
pub fn new(db: Database, asset_version: String, files_dir: String, upload_max_size_bytes: usize, file_cache_max_age_seconds: u64, smtp: Option<SmtpConfig>, registration_enabled: bool, account_confirmation_required: bool, frontend_log_level: String, anonymous_access_token_ttl_days: i64, user_session_ttl_days: i64) -> Self {
|
||||
Self { db, asset_version, files_dir, upload_max_size_bytes, file_cache_max_age_seconds, smtp, registration_enabled, account_confirmation_required, frontend_log_level, anonymous_access_token_ttl_days, user_session_ttl_days, channels: RwLock::new(HashMap::new()), presence: RwLock::new(HashMap::new()), next_connection_id: AtomicU64::new(1) }
|
||||
}
|
||||
async fn channel_for_key(&self, key: String) -> broadcast::Sender<NoteUpdate> {
|
||||
async fn channel_for_key(&self, key: String) -> broadcast::Sender<RoomEvent> {
|
||||
if let Some(sender) = self.channels.read().await.get(&key) { return sender.clone(); }
|
||||
let mut channels = self.channels.write().await;
|
||||
channels.entry(key).or_insert_with(|| broadcast::channel(CHANNEL_CAPACITY).0).clone()
|
||||
}
|
||||
pub async fn note_channel(&self, workspace_slug: &str, note_slug: &str) -> broadcast::Sender<NoteUpdate> {
|
||||
self.channel_for_key(format!("workspace:{workspace_slug}/{note_slug}")).await
|
||||
pub fn note_room_key(workspace_slug: &str, note_slug: &str) -> String { format!("workspace:{workspace_slug}/{note_slug}") }
|
||||
pub fn pad_room_key(slug: &str) -> String { format!("pad:{slug}") }
|
||||
pub async fn note_channel(&self, workspace_slug: &str, note_slug: &str) -> broadcast::Sender<RoomEvent> { self.channel_for_key(Self::note_room_key(workspace_slug, note_slug)).await }
|
||||
pub async fn pad_channel(&self, slug: &str) -> broadcast::Sender<RoomEvent> { self.channel_for_key(Self::pad_room_key(slug)).await }
|
||||
pub async fn join_room(&self, key: &str, nickname: String, color: Option<String>) -> (u64, Vec<PresenceUser>) {
|
||||
let id = self.next_connection_id.fetch_add(1, Ordering::Relaxed);
|
||||
let mut presence = self.presence.write().await;
|
||||
let room = presence.entry(key.to_owned()).or_default();
|
||||
room.insert(id, PresenceUser { name: nickname, color });
|
||||
(id, sorted_users(room))
|
||||
}
|
||||
pub async fn pad_channel(&self, slug: &str) -> broadcast::Sender<NoteUpdate> {
|
||||
self.channel_for_key(format!("pad:{slug}")).await
|
||||
pub async fn update_room_color(&self, key: &str, id: u64, color: Option<String>) -> Vec<PresenceUser> {
|
||||
let mut presence = self.presence.write().await;
|
||||
if let Some(room) = presence.get_mut(key) {
|
||||
if let Some(user) = room.get_mut(&id) { user.color = color; }
|
||||
sorted_users(room)
|
||||
} else { Vec::new() }
|
||||
}
|
||||
pub async fn leave_room(&self, key: &str, id: u64) -> Vec<PresenceUser> {
|
||||
let mut presence = self.presence.write().await;
|
||||
if let Some(room) = presence.get_mut(key) {
|
||||
room.remove(&id);
|
||||
let users = sorted_users(room);
|
||||
let empty = room.is_empty();
|
||||
if empty { presence.remove(key); }
|
||||
users
|
||||
} else { Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
fn sorted_users(room: &HashMap<u64, PresenceUser>) -> Vec<PresenceUser> {
|
||||
let mut users: Vec<PresenceUser> = room.values().cloned().collect();
|
||||
users.sort_by_key(|value| value.name.to_lowercase());
|
||||
users
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<AppState>;
|
||||
|
||||
+56
-10
@@ -2,13 +2,17 @@ use axum::{extract::{ws::{Message, WebSocket}, Path, State, WebSocketUpgrade}, r
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, info, warn};
|
||||
use crate::{auth, db, state::{NoteUpdate, SharedState}};
|
||||
use crate::{auth, db, state::{AppState, NoteUpdate, PresenceUser, RoomEvent, SharedState}};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ClientMessage {
|
||||
Authenticate { password: Option<String>, access_token: Option<String>, nickname: Option<String>, session_token: Option<String> },
|
||||
Authenticate { password: Option<String>, access_token: Option<String>, nickname: Option<String>, session_token: Option<String>, color: Option<String> },
|
||||
Update { content: String, owner_map: Option<String> },
|
||||
Ping { nonce: u64 },
|
||||
Chat { text: String },
|
||||
SetColor { color: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -16,6 +20,9 @@ enum ClientMessage {
|
||||
enum ServerMessage {
|
||||
Authenticated { workspace_title: String, note_title: String, content: String, owner_map: String },
|
||||
Document { content: String, revision_id: i64, updated_at: String, author: Option<String>, owner_map: String },
|
||||
Presence { users: Vec<PresenceUser> },
|
||||
Chat { sender: String, text: String },
|
||||
Pong { nonce: u64 },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
@@ -27,9 +34,9 @@ async fn handle_socket(mut socket: WebSocket, state: SharedState, workspace_slug
|
||||
info!(%workspace_slug, %note_slug, "note websocket connected");
|
||||
let Some(workspace) = db::find_workspace(&state.db, &workspace_slug).await.ok().flatten() else { warn!(%workspace_slug, %note_slug, "note websocket rejected: workspace not found"); let _=send_error(&mut socket,"Workspace not found").await; return; };
|
||||
let Some(note) = db::find_note(&state.db, workspace.id, ¬e_slug).await.ok().flatten() else { warn!(%workspace_slug, %note_slug, "note websocket rejected: note not found"); let _=send_error(&mut socket,"Note not found").await; return; };
|
||||
let (password, access_token, nickname, session_token) = match socket.recv().await {
|
||||
let (password, access_token, nickname, session_token, color) = match socket.recv().await {
|
||||
Some(Ok(Message::Text(text))) => match serde_json::from_str::<ClientMessage>(&text) {
|
||||
Ok(ClientMessage::Authenticate { password, access_token, nickname, session_token }) => (password, access_token, clean_nickname(nickname), session_token),
|
||||
Ok(ClientMessage::Authenticate { password, access_token, nickname, session_token, color }) => (password, access_token, clean_nickname(nickname), session_token, clean_color(color)),
|
||||
_ => { let _=send_error(&mut socket,"Wymagane uwierzytelnienie").await; return; }
|
||||
}, _ => return
|
||||
};
|
||||
@@ -37,8 +44,13 @@ async fn handle_socket(mut socket: WebSocket, state: SharedState, workspace_slug
|
||||
if workspace.password_hash.is_some() && !db::verify_workspace_password(&workspace, password.as_deref()) && !crate::api::verify_resource_access_token(&state, "workspace", &workspace_slug, access_token.as_deref()).await.unwrap_or(false) { warn!(workspace_id = workspace.id, note_id = note.id, "note websocket rejected: invalid workspace password"); let _=send_error(&mut socket,"Invalid password").await; return; }
|
||||
info!(workspace_id = workspace.id, note_id = note.id, nickname = ?nickname, "note websocket authenticated");
|
||||
if send(&mut socket,&ServerMessage::Authenticated { workspace_title:workspace.title.clone(), note_title:note.title.clone(), content:note.content.clone(), owner_map:note.owner_map.clone() }).await.is_err(){return;}
|
||||
let room_key = AppState::note_room_key(&workspace_slug, ¬e_slug);
|
||||
let channel=state.note_channel(&workspace_slug,¬e_slug).await;
|
||||
let mut updates=channel.subscribe();
|
||||
let display_name = nickname.clone().unwrap_or_else(|| "Guest".into());
|
||||
let (connection_id, users) = state.join_room(&room_key, display_name.clone(), color).await;
|
||||
let _ = channel.send(RoomEvent::Presence(users));
|
||||
let mut last_chat = Instant::now() - Duration::from_secs(1);
|
||||
let (mut sender,mut receiver)=socket.split();
|
||||
loop { tokio::select! {
|
||||
incoming=receiver.next()=>match incoming {
|
||||
@@ -47,25 +59,41 @@ async fn handle_socket(mut socket: WebSocket, state: SharedState, workspace_slug
|
||||
if content.len()>2_000_000 { let _=send_split(&mut sender,&ServerMessage::Error{message:"The document is too large".into()}).await; continue; }
|
||||
let owner_map=owner_map.unwrap_or_else(||"[]".into());
|
||||
match db::save_revision(&state.db,note.id,workspace.id,&content,nickname.as_deref(),&owner_map).await {
|
||||
Ok((revision_id,updated_at))=>{let _=channel.send(NoteUpdate{content,revision_id,updated_at,author:nickname.clone(),owner_map});}
|
||||
Ok((revision_id,updated_at))=>{let _=channel.send(RoomEvent::Document(NoteUpdate{content,revision_id,updated_at,author:nickname.clone(),owner_map}));}
|
||||
Err(error)=>warn!(%error, workspace_id = workspace.id, note_id = note.id, "failed to save revision"),
|
||||
}
|
||||
}
|
||||
Ok(ClientMessage::Ping{nonce})=>{ let _=send_split(&mut sender,&ServerMessage::Pong{nonce}).await; },
|
||||
Ok(ClientMessage::Chat{text})=>{
|
||||
let text=clean_chat(text);
|
||||
if !text.is_empty() && last_chat.elapsed() >= Duration::from_millis(500) { last_chat=Instant::now(); let _=channel.send(RoomEvent::Chat{sender:display_name.clone(),text}); }
|
||||
}
|
||||
Ok(ClientMessage::SetColor{color})=>{ let users=state.update_room_color(&room_key,connection_id,clean_color(color)).await; let _=channel.send(RoomEvent::Presence(users)); },
|
||||
Ok(ClientMessage::Authenticate{..})=>{}, Err(error)=>warn!(%error,"invalid websocket message"),
|
||||
},
|
||||
Some(Ok(Message::Close(_)))|None=>break, Some(Ok(_))=>{}, Some(Err(error))=>{debug!(%error,"websocket receive error");break;}
|
||||
},
|
||||
update=updates.recv()=>match update {
|
||||
Ok(update)=>if send_split(&mut sender,&ServerMessage::Document{content:update.content,revision_id:update.revision_id,updated_at:update.updated_at,author:update.author,owner_map:update.owner_map}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Document(update))=>if send_split(&mut sender,&ServerMessage::Document{content:update.content,revision_id:update.revision_id,updated_at:update.updated_at,author:update.author,owner_map:update.owner_map}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Presence(users))=>if send_split(&mut sender,&ServerMessage::Presence{users}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_split(&mut sender,&ServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_note(&state.db,workspace.id,¬e_slug).await { if send_split(&mut sender,&ServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
|
||||
}
|
||||
}}
|
||||
let users = state.leave_room(&room_key, connection_id).await;
|
||||
let _ = channel.send(RoomEvent::Presence(users));
|
||||
info!(workspace_id = workspace.id, note_id = note.id, "note websocket disconnected");
|
||||
}
|
||||
fn clean_nickname(value: Option<String>)->Option<String> {
|
||||
value.map(|v|v.trim().chars().take(40).collect::<String>()).filter(|v|!v.is_empty())
|
||||
}
|
||||
fn clean_color(value: Option<String>) -> Option<String> {
|
||||
value.map(|v| v.trim().to_ascii_lowercase()).filter(|v| v.len() == 7 && v.starts_with('#') && v[1..].chars().all(|c| c.is_ascii_hexdigit()))
|
||||
}
|
||||
fn clean_chat(value: String) -> String {
|
||||
value.chars().map(|c| if matches!(c, '\r' | '\n' | '\0') { ' ' } else { c }).collect::<String>().trim().chars().take(1000).collect()
|
||||
}
|
||||
async fn send_error(socket:&mut WebSocket,message:&str)->Result<(),axum::Error> {
|
||||
send(socket,&ServerMessage::Error {
|
||||
message:message.into()
|
||||
@@ -84,6 +112,9 @@ async fn send_split(sender:&mut futures_util::stream::SplitSink<WebSocket,Messag
|
||||
enum PadServerMessage {
|
||||
Authenticated { title: String, content: String, owner_map: String },
|
||||
Document { content: String, revision_id: i64, updated_at: String, author: Option<String>, owner_map: String },
|
||||
Presence { users: Vec<PresenceUser> },
|
||||
Chat { sender: String, text: String },
|
||||
Pong { nonce: u64 },
|
||||
Error { message: String },
|
||||
}
|
||||
pub async fn upgrade_pad(ws:WebSocketUpgrade,Path(slug):Path<String>,State(state):State<SharedState>)->Response{
|
||||
@@ -92,9 +123,9 @@ pub async fn upgrade_pad(ws:WebSocketUpgrade,Path(slug):Path<String>,State(state
|
||||
async fn handle_pad_socket(mut socket:WebSocket,state:SharedState,slug:String){
|
||||
info!(%slug, "pad websocket connected");
|
||||
let Some(pad)=db::find_pad(&state.db,&slug).await.ok().flatten() else {warn!(%slug, "pad websocket rejected: pad not found");let _=send_pad(&mut socket,&PadServerMessage::Error{message:"Note not found".into()}).await;return;};
|
||||
let (password,access_token,nickname,session_token)=match socket.recv().await{
|
||||
let (password,access_token,nickname,session_token,color)=match socket.recv().await{
|
||||
Some(Ok(Message::Text(text)))=>match serde_json::from_str::<ClientMessage>(&text){
|
||||
Ok(ClientMessage::Authenticate{password,access_token,nickname,session_token})=>(password,access_token,clean_nickname(nickname),session_token),
|
||||
Ok(ClientMessage::Authenticate{password,access_token,nickname,session_token,color})=>(password,access_token,clean_nickname(nickname),session_token,clean_color(color)),
|
||||
_=>{let _=send_pad(&mut socket,&PadServerMessage::Error{message:"Wymagane uwierzytelnienie".into()}).await;return;}
|
||||
},_=>return
|
||||
};
|
||||
@@ -102,8 +133,13 @@ async fn handle_pad_socket(mut socket:WebSocket,state:SharedState,slug:String){
|
||||
if pad.password_hash.is_some() && !db::verify_pad_password(&pad,password.as_deref()) && !crate::api::verify_resource_access_token(&state,"pad",&slug,access_token.as_deref()).await.unwrap_or(false){warn!(pad_id = pad.id, "pad websocket rejected: invalid password");let _=send_pad(&mut socket,&PadServerMessage::Error{message:"Invalid password".into()}).await;return;}
|
||||
info!(pad_id = pad.id, nickname = ?nickname, "pad websocket authenticated");
|
||||
if send_pad(&mut socket,&PadServerMessage::Authenticated{title:pad.title.clone(),content:pad.content.clone(),owner_map:pad.owner_map.clone()}).await.is_err(){return;}
|
||||
let room_key = AppState::pad_room_key(&slug);
|
||||
let channel=state.pad_channel(&slug).await;
|
||||
let mut updates=channel.subscribe();
|
||||
let display_name = nickname.clone().unwrap_or_else(|| "Guest".into());
|
||||
let (connection_id, users) = state.join_room(&room_key, display_name.clone(), color).await;
|
||||
let _ = channel.send(RoomEvent::Presence(users));
|
||||
let mut last_chat = Instant::now() - Duration::from_secs(1);
|
||||
let(mut sender,mut receiver)=socket.split();
|
||||
loop{tokio::select!{
|
||||
incoming=receiver.next()=>match incoming{
|
||||
@@ -112,9 +148,15 @@ async fn handle_pad_socket(mut socket:WebSocket,state:SharedState,slug:String){
|
||||
if content.len()>2_000_000 { let _=send_pad_split(&mut sender,&PadServerMessage::Error{message:"The document is too large".into()}).await; continue; }
|
||||
let owner_map=owner_map.unwrap_or_else(||"[]".into());
|
||||
if let Ok((revision_id,updated_at))=db::save_pad_revision(&state.db,pad.id,&content,nickname.as_deref(),&owner_map).await{
|
||||
let _=channel.send(NoteUpdate{content,revision_id,updated_at,author:nickname.clone(),owner_map});
|
||||
let _=channel.send(RoomEvent::Document(NoteUpdate{content,revision_id,updated_at,author:nickname.clone(),owner_map}));
|
||||
}
|
||||
}
|
||||
Ok(ClientMessage::Ping{nonce})=>{ let _=send_pad_split(&mut sender,&PadServerMessage::Pong{nonce}).await; },
|
||||
Ok(ClientMessage::Chat{text})=>{
|
||||
let text=clean_chat(text);
|
||||
if !text.is_empty() && last_chat.elapsed() >= Duration::from_millis(500) { last_chat=Instant::now(); let _=channel.send(RoomEvent::Chat{sender:display_name.clone(),text}); }
|
||||
}
|
||||
Ok(ClientMessage::SetColor{color})=>{ let users=state.update_room_color(&room_key,connection_id,clean_color(color)).await; let _=channel.send(RoomEvent::Presence(users)); },
|
||||
Ok(ClientMessage::Authenticate{..})=>{},
|
||||
Err(error)=>warn!(%error,"invalid pad websocket message"),
|
||||
},
|
||||
@@ -123,11 +165,15 @@ async fn handle_pad_socket(mut socket:WebSocket,state:SharedState,slug:String){
|
||||
Some(Err(error))=>{debug!(%error,"pad websocket receive error");break;}
|
||||
},
|
||||
update=updates.recv()=>match update{
|
||||
Ok(u)=>if send_pad_split(&mut sender,&PadServerMessage::Document{content:u.content,revision_id:u.revision_id,updated_at:u.updated_at,author:u.author,owner_map:u.owner_map}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Document(u))=>if send_pad_split(&mut sender,&PadServerMessage::Document{content:u.content,revision_id:u.revision_id,updated_at:u.updated_at,author:u.author,owner_map:u.owner_map}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Presence(users))=>if send_pad_split(&mut sender,&PadServerMessage::Presence{users}).await.is_err(){break;},
|
||||
Ok(RoomEvent::Chat{sender:chat_sender,text})=>if send_pad_split(&mut sender,&PadServerMessage::Chat{sender:chat_sender,text}).await.is_err(){break;},
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_))=>if let Ok(Some(current))=db::find_pad(&state.db,&slug).await { if send_pad_split(&mut sender,&PadServerMessage::Document{content:current.content,revision_id:0,updated_at:current.updated_at,author:None,owner_map:current.owner_map}).await.is_err(){break;} },
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed)=>break,
|
||||
}
|
||||
}}
|
||||
let users = state.leave_room(&room_key, connection_id).await;
|
||||
let _ = channel.send(RoomEvent::Presence(users));
|
||||
info!(pad_id = pad.id, "pad websocket disconnected");
|
||||
}
|
||||
async fn send_pad(socket:&mut WebSocket,message:&PadServerMessage)->Result<(),axum::Error> {
|
||||
|
||||
Reference in New Issue
Block a user