Initial commit: EifelDC - Discord-like Matrix chat platform
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Rust Tests (push) Has been cancelled
CI / Frontend Check (push) Has been cancelled
CI / Build Tauri (macOS) (push) Has been cancelled
CI / Build Tauri (macOS Intel) (push) Has been cancelled
CI / Build Tauri (Linux) (push) Has been cancelled
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Rust Tests (push) Has been cancelled
CI / Frontend Check (push) Has been cancelled
CI / Build Tauri (macOS) (push) Has been cancelled
CI / Build Tauri (macOS Intel) (push) Has been cancelled
CI / Build Tauri (Linux) (push) Has been cancelled
Includes server (Rust/Axum API proxy with voice management), Tauri desktop client with Svelte UI, bot-sdk, Docker infra (Synapse, PostgreSQL, Coturn, Nginx), and CI/CD pipeline.
This commit is contained in:
22
server/Cargo.toml
Normal file
22
server/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "eifeldc-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
matrix-sdk = { workspace = true }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
url = { workspace = true }
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
27
server/Dockerfile
Normal file
27
server/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM rust:1.82-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY server/Cargo.toml server/Cargo.toml
|
||||
RUN mkdir -p server/src && echo "fn main() {}" > server/src/main.rs
|
||||
RUN cargo build --release -p eifeldc-server && rm -rf server/src
|
||||
|
||||
COPY server/src server/src
|
||||
RUN touch server/src/main.rs && cargo build --release -p eifeldc-server
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd -m -d /home/eifeldc eifeldc
|
||||
USER eifeldc
|
||||
|
||||
COPY --from=builder /app/target/release/eifeldc-server /usr/local/bin/eifeldc-server
|
||||
|
||||
ENV EIFELDC_STATIC_DIR=/usr/share/eifeldc/client
|
||||
ENV RUST_LOG=eifeldc_server=info,tower_http=info
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["eifeldc-server"]
|
||||
4
server/src/lib.rs
Normal file
4
server/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod routes;
|
||||
pub mod state;
|
||||
|
||||
pub use state::ServerState;
|
||||
25
server/src/main.rs
Normal file
25
server/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use eifeldc_server::routes::api_router;
|
||||
use eifeldc_server::ServerState;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let state = ServerState::new();
|
||||
let api = api_router(state);
|
||||
|
||||
let static_dir = std::env::var("EIFELDC_STATIC_DIR")
|
||||
.unwrap_or_else(|_| "client/src-ui/dist".to_string());
|
||||
|
||||
let app = api
|
||||
.fallback_service(ServeDir::new(&static_dir));
|
||||
|
||||
let addr: SocketAddr = ([0, 0, 0, 0], 3000).into();
|
||||
tracing::info!("EifelDC Web Server listening on http://{}", addr);
|
||||
tracing::info!("Serving static files from {}", static_dir);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
160
server/src/routes/auth.rs
Normal file
160
server/src/routes/auth.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
Json,
|
||||
};
|
||||
use matrix_sdk::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::state::VoiceManager;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub homeserver: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LoginResult {
|
||||
pub success: bool,
|
||||
pub user_id: String,
|
||||
pub token: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
pub homeserver: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
State(state): State<crate::state::ServerState>,
|
||||
Json(req): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResult>, StatusCode> {
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&req.homeserver)
|
||||
.build()
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(&req.username, &req.password)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let user_id = client
|
||||
.user_id()
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let token = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let mut s = state.write().await;
|
||||
s.sessions.insert(token.clone(), crate::state::Session {
|
||||
client,
|
||||
user_id: user_id.clone(),
|
||||
voice_manager: VoiceManager::new(),
|
||||
});
|
||||
|
||||
Ok(Json(LoginResult {
|
||||
success: true,
|
||||
user_id,
|
||||
token: Some(token),
|
||||
error: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
State(state): State<crate::state::ServerState>,
|
||||
Json(req): Json<RegisterRequest>,
|
||||
) -> Result<Json<LoginResult>, StatusCode> {
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&req.homeserver)
|
||||
.build()
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let mut request = matrix_sdk::ruma::api::client::account::register::v3::Request::new();
|
||||
request.username = Some(req.username);
|
||||
request.password = Some(req.password);
|
||||
|
||||
client
|
||||
.matrix_auth()
|
||||
.register(request)
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let user_id = client
|
||||
.user_id()
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let token = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let mut s = state.write().await;
|
||||
s.sessions.insert(token.clone(), crate::state::Session {
|
||||
client,
|
||||
user_id: user_id.clone(),
|
||||
voice_manager: VoiceManager::new(),
|
||||
});
|
||||
|
||||
Ok(Json(LoginResult {
|
||||
success: true,
|
||||
user_id,
|
||||
token: Some(token),
|
||||
error: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn logout(
|
||||
State(state): State<crate::state::ServerState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<bool>, StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let mut s = state.write().await;
|
||||
if let Some(session) = s.sessions.remove(&token) {
|
||||
let _ = session.client.matrix_auth().logout().await;
|
||||
}
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
pub async fn get_current_user(
|
||||
State(state): State<crate::state::ServerState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Option<String>>, StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let s = state.read().await;
|
||||
let user_id = s.sessions.get(&token).map(|s| s.user_id.clone());
|
||||
Ok(Json(user_id))
|
||||
}
|
||||
|
||||
pub fn extract_token(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(
|
||||
headers: HeaderMap,
|
||||
State(state): State<crate::state::ServerState>,
|
||||
request: axum::extract::Request,
|
||||
next: middleware::Next,
|
||||
) -> Result<axum::response::Response, StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let s = state.read().await;
|
||||
if !s.sessions.contains_key(&token) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Ok(next.run(request).await)
|
||||
}
|
||||
86
server/src/routes/emoji.rs
Normal file
86
server/src/routes/emoji.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CustomEmoji {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub category: String,
|
||||
pub animated: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StickerPack {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub stickers: Vec<Sticker>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Sticker {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
pub async fn get_custom_emoji(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(_room_id): Path<String>,
|
||||
) -> Result<Json<Vec<CustomEmoji>>, axum::http::StatusCode> {
|
||||
let _token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
Ok(Json(Vec::new()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UploadEmojiRequest {
|
||||
pub name: String,
|
||||
pub image_path: String,
|
||||
}
|
||||
|
||||
pub async fn upload_emoji(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
Json(req): Json<UploadEmojiRequest>,
|
||||
) -> Result<Json<CustomEmoji>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let _room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let path = std::path::Path::new(&req.image_path);
|
||||
if !path.exists() {
|
||||
return Err(axum::http::StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
let mime_type = match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("png") => "image/png",
|
||||
Some("jpg") | Some("jpeg") => "image/jpeg",
|
||||
Some("gif") => "image/gif",
|
||||
Some("webp") => "image/webp",
|
||||
_ => "image/png",
|
||||
};
|
||||
|
||||
let data = std::fs::read(path).map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let content_type = mime_type.parse::<matrix_sdk::ruma::mime::Mime>().map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let response = session.client.media().upload(&content_type, data).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(CustomEmoji {
|
||||
id: format!("emoji_{}", chrono::Utc::now().timestamp()),
|
||||
name: req.name,
|
||||
url: response.content_uri.to_string(),
|
||||
category: "custom".to_string(),
|
||||
animated: mime_type == "image/gif",
|
||||
}))
|
||||
}
|
||||
84
server/src/routes/messages.rs
Normal file
84
server/src/routes/messages.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use axum::{
|
||||
extract::{Path, State, Query},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MessageInfo {
|
||||
pub event_id: String,
|
||||
pub sender: String,
|
||||
pub body: String,
|
||||
pub timestamp: u64,
|
||||
pub reply_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MessagesQuery {
|
||||
pub limit: Option<u32>,
|
||||
pub from: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_room_messages(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
Query(query): Query<MessagesQuery>,
|
||||
) -> Result<Json<Vec<MessageInfo>>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let limit = query.limit.unwrap_or(50);
|
||||
let mut options = matrix_sdk::ruma::api::client::message::get_message_events::v3::Request::new();
|
||||
options.limit = limit.into();
|
||||
options.from = query.from.map(|t| t.into());
|
||||
|
||||
let messages = room.messages(options).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for msg in messages.chunk {
|
||||
if let matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(ev) = msg {
|
||||
result.push(MessageInfo {
|
||||
event_id: ev.event_id().to_string(),
|
||||
sender: ev.sender().to_string(),
|
||||
body: ev.content().body().to_string(),
|
||||
timestamp: ev.origin_server_ts().0,
|
||||
reply_to: ev.content().in_reply_to().map(|r| r.event_id.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SendMessageRequest {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub async fn send_message(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
Json(req): Json<SendMessageRequest>,
|
||||
) -> Result<Json<String>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(&req.message);
|
||||
let txn_id = matrix_sdk::ruma::TransactionId::new();
|
||||
let response = room.send(content, Some(&txn_id)).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(response.event_id.to_string()))
|
||||
}
|
||||
63
server/src/routes/mod.rs
Normal file
63
server/src/routes/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
pub mod auth;
|
||||
pub mod rooms;
|
||||
pub mod messages;
|
||||
pub mod presence;
|
||||
pub mod voice;
|
||||
pub mod emoji;
|
||||
pub mod roles;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
middleware,
|
||||
http::HeaderMap,
|
||||
};
|
||||
use tower_http::cors::{CorsLayer, Any};
|
||||
use crate::ServerState;
|
||||
use crate::routes::auth::auth_middleware;
|
||||
|
||||
pub fn api_router(state: ServerState) -> Router {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
Router::new()
|
||||
.nest("/api", api_routes(state))
|
||||
.layer(cors)
|
||||
}
|
||||
|
||||
fn api_routes(state: ServerState) -> Router {
|
||||
let public = Router::new()
|
||||
.route("/login", post(auth::login))
|
||||
.route("/register", post(auth::register))
|
||||
.route("/current-user", get(auth::get_current_user));
|
||||
|
||||
let protected = Router::new()
|
||||
.route("/logout", post(auth::logout))
|
||||
.route("/rooms", get(rooms::get_joined_rooms))
|
||||
.route("/rooms/create", post(rooms::create_room))
|
||||
.route("/rooms/join", post(rooms::join_room))
|
||||
.route("/rooms/{room_id}/leave", post(rooms::leave_room))
|
||||
.route("/rooms/{room_id}/members", get(rooms::get_room_members))
|
||||
.route("/rooms/{room_id}/messages", get(messages::get_room_messages))
|
||||
.route("/rooms/{room_id}/send", post(messages::send_message))
|
||||
.route("/presence/set", post(presence::set_presence))
|
||||
.route("/presence/{user_id}", get(presence::get_presence))
|
||||
.route("/voice/join", post(voice::join_voice_channel))
|
||||
.route("/voice/leave", post(voice::leave_voice_channel))
|
||||
.route("/voice/toggle-mute", post(voice::toggle_mute))
|
||||
.route("/voice/toggle-deafen", post(voice::toggle_deafen))
|
||||
.route("/rooms/{room_id}/roles", get(roles::get_roles))
|
||||
.route("/rooms/{room_id}/roles/assign", post(roles::assign_role))
|
||||
.route("/rooms/{room_id}/roles/remove", post(roles::remove_role))
|
||||
.route("/rooms/{room_id}/permissions/{user_id}", get(roles::get_permissions))
|
||||
.route("/rooms/{room_id}/emoji", get(emoji::get_custom_emoji))
|
||||
.route("/rooms/{room_id}/emoji/upload", post(emoji::upload_emoji))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), auth_middleware));
|
||||
|
||||
Router::new()
|
||||
.merge(public)
|
||||
.merge(protected)
|
||||
.with_state(state)
|
||||
}
|
||||
77
server/src/routes/presence.rs
Normal file
77
server/src/routes/presence.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetPresenceRequest {
|
||||
pub status: String,
|
||||
pub status_msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PresenceInfo {
|
||||
pub user_id: String,
|
||||
pub status: String,
|
||||
pub status_msg: Option<String>,
|
||||
pub last_active: Option<u64>,
|
||||
}
|
||||
|
||||
pub async fn set_presence(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<SetPresenceRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let presence_state = match req.status.as_str() {
|
||||
"online" => matrix_sdk::ruma::presence::PresenceState::Online,
|
||||
"away" => matrix_sdk::ruma::presence::PresenceState::Away,
|
||||
"unavailable" => matrix_sdk::ruma::presence::PresenceState::Unavailable,
|
||||
_ => matrix_sdk::ruma::presence::PresenceState::Online,
|
||||
};
|
||||
|
||||
let user_id = session.client.user_id().ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let mut request = matrix_sdk::ruma::api::client::presence::set_presence::v3::Request::new(user_id.to_owned());
|
||||
request.presence = presence_state;
|
||||
request.status_msg = req.status_msg;
|
||||
|
||||
session.client.send(request, None).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
pub async fn get_presence(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(user_id): Path<String>,
|
||||
) -> Result<Json<PresenceInfo>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let uid: matrix_sdk::ruma::OwnedUserId = user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let request = matrix_sdk::ruma::api::client::presence::get_presence::v3::Request::new(uid.to_owned());
|
||||
|
||||
let response = session.client.send(request, None).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let status_str = match response.presence {
|
||||
matrix_sdk::ruma::presence::PresenceState::Online => "online",
|
||||
matrix_sdk::ruma::presence::PresenceState::Away => "away",
|
||||
matrix_sdk::ruma::presence::PresenceState::Unavailable => "unavailable",
|
||||
_ => "offline",
|
||||
};
|
||||
|
||||
Ok(Json(PresenceInfo {
|
||||
user_id,
|
||||
status: status_str.to_string(),
|
||||
status_msg: response.status_msg,
|
||||
last_active: response.last_active_ago.map(|d| d.as_secs()),
|
||||
}))
|
||||
}
|
||||
159
server/src/routes/roles.rs
Normal file
159
server/src/routes/roles.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Role {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
pub permissions: Vec<String>,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Permissions {
|
||||
pub can_send_messages: bool,
|
||||
pub can_delete_messages: bool,
|
||||
pub can_manage_channels: bool,
|
||||
pub can_manage_roles: bool,
|
||||
pub can_kick: bool,
|
||||
pub can_ban: bool,
|
||||
pub can_manage_emoji: bool,
|
||||
pub can_manage_threads: bool,
|
||||
pub can_voice_connect: bool,
|
||||
pub can_voice_stream: bool,
|
||||
}
|
||||
|
||||
fn power_level_to_permissions(power_level: i64) -> Permissions {
|
||||
Permissions {
|
||||
can_send_messages: power_level >= 0,
|
||||
can_delete_messages: power_level >= 50,
|
||||
can_manage_channels: power_level >= 50,
|
||||
can_manage_roles: power_level >= 75,
|
||||
can_kick: power_level >= 50,
|
||||
can_ban: power_level >= 75,
|
||||
can_manage_emoji: power_level >= 50,
|
||||
can_manage_threads: power_level >= 0,
|
||||
can_voice_connect: power_level >= 0,
|
||||
can_voice_stream: power_level >= 25,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_roles(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
) -> Result<Json<Vec<Role>>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let mut roles = Vec::new();
|
||||
if let Ok(power_levels) = room.power_levels().await {
|
||||
for (uid, power_level) in &power_levels.users {
|
||||
roles.push(Role {
|
||||
id: format!("role_{}", uid),
|
||||
name: uid.to_string(),
|
||||
color: if *power_level >= 100 { "#ed4245".to_string() } else if *power_level >= 50 { "#fee75c".to_string() } else { "#5865f2".to_string() },
|
||||
permissions: vec![],
|
||||
position: *power_level as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(roles))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AssignRoleRequest {
|
||||
pub user_id: String,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
pub async fn assign_role(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
Json(req): Json<AssignRoleRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
let uid: matrix_sdk::ruma::OwnedUserId = req.user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let power_level: i64 = match req.role_id.as_str() {
|
||||
"admin" => 100,
|
||||
"moderator" => 50,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let mut content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::new();
|
||||
content.users.insert(uid.to_owned(), power_level.into());
|
||||
|
||||
room.send_state_event(content).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RemoveRoleRequest {
|
||||
pub user_id: String,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
pub async fn remove_role(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
Json(req): Json<RemoveRoleRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
let uid: matrix_sdk::ruma::OwnedUserId = req.user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
if let Ok(mut power_levels) = room.power_levels().await {
|
||||
power_levels.users.remove(&uid);
|
||||
let content = matrix_sdk::ruma::events::room::power_levels::RoomPowerLevelsEventContent::from(power_levels);
|
||||
room.send_state_event(content).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
pub async fn get_permissions(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path((room_id, user_id)): Path<(String, String)>,
|
||||
) -> Result<Json<Permissions>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
let uid: matrix_sdk::ruma::OwnedUserId = user_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let user_power = if let Ok(power_levels) = room.power_levels().await {
|
||||
power_levels.users.get(&uid).copied().map(|p| p.into()).unwrap_or(power_levels.users_default as i64)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(Json(power_level_to_permissions(user_power)))
|
||||
}
|
||||
134
server/src/routes/rooms.rs
Normal file
134
server/src/routes/rooms.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RoomInfo {
|
||||
pub room_id: String,
|
||||
pub name: String,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_encrypted: bool,
|
||||
pub member_count: u64,
|
||||
pub topic: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_joined_rooms(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<Vec<RoomInfo>>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rooms = session.client.joined_rooms();
|
||||
let mut result = Vec::new();
|
||||
for room in rooms {
|
||||
let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default();
|
||||
let avatar_url = room.avatar_url().map(|u| u.to_string());
|
||||
let member_count = room.joined_members().len() as u64;
|
||||
let topic = room.topic().map(|t| t.to_string());
|
||||
let is_encrypted = room.is_encrypted().await.unwrap_or(false);
|
||||
result.push(RoomInfo {
|
||||
room_id: room.room_id().to_string(),
|
||||
name,
|
||||
avatar_url,
|
||||
is_encrypted,
|
||||
member_count,
|
||||
topic,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateRoomRequest {
|
||||
pub name: String,
|
||||
pub topic: Option<String>,
|
||||
pub visibility: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_room(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<CreateRoomRequest>,
|
||||
) -> Result<Json<String>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let vis = match req.visibility.as_deref() {
|
||||
Some("public") => matrix_sdk::ruma::Space::Public,
|
||||
_ => matrix_sdk::ruma::Space::Private,
|
||||
};
|
||||
|
||||
let mut request = matrix_sdk::ruma::api::client::room::create_room::v3::Request::new();
|
||||
request.name = Some(req.name);
|
||||
request.topic = req.topic;
|
||||
request.visibility = vis;
|
||||
|
||||
let response = session.client.create_room(request).await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(response.room_id.to_string()))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct JoinRoomRequest {
|
||||
pub room_id_or_alias: String,
|
||||
}
|
||||
|
||||
pub async fn join_room(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<JoinRoomRequest>,
|
||||
) -> Result<Json<String>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let room_id = session.client
|
||||
.join_room_by_id_or_alias(
|
||||
&req.room_id_or_alias.try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(room_id.to_string()))
|
||||
}
|
||||
|
||||
pub async fn leave_room(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
room.leave().await.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(true))
|
||||
}
|
||||
|
||||
pub async fn get_room_members(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Path(room_id): Path<String>,
|
||||
) -> Result<Json<Vec<String>>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let s = state.read().await;
|
||||
let session = s.sessions.get(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let rid: matrix_sdk::ruma::OwnedRoomId = room_id.as_str().try_into().map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
let room = session.client.get_room(&rid).ok_or(axum::http::StatusCode::NOT_FOUND)?;
|
||||
|
||||
let members = room.joined_members();
|
||||
Ok(Json(members.iter().map(|m| m.user_id().to_string()).collect()))
|
||||
}
|
||||
80
server/src/routes/voice.rs
Normal file
80
server/src/routes/voice.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::HeaderMap,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::ServerState;
|
||||
use super::auth::extract_token;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VoiceRequest {
|
||||
pub room_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VoiceStateInfo {
|
||||
pub room_id: String,
|
||||
pub muted: bool,
|
||||
pub deafened: bool,
|
||||
pub streaming: bool,
|
||||
}
|
||||
|
||||
pub async fn join_voice_channel(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<VoiceRequest>,
|
||||
) -> Result<Json<VoiceStateInfo>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let mut s = state.write().await;
|
||||
let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
session.voice_manager.join_channel(req.room_id.clone(), session.user_id.clone());
|
||||
|
||||
Ok(Json(VoiceStateInfo {
|
||||
room_id: req.room_id,
|
||||
muted: false,
|
||||
deafened: false,
|
||||
streaming: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn leave_voice_channel(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<VoiceRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let mut s = state.write().await;
|
||||
let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
Ok(Json(session.voice_manager.leave_channel(&req.room_id, &session.user_id)))
|
||||
}
|
||||
|
||||
pub async fn toggle_mute(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<VoiceRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let mut s = state.write().await;
|
||||
let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
session.voice_manager.toggle_mute(&req.room_id, &session.user_id)
|
||||
.ok_or(axum::http::StatusCode::BAD_REQUEST)
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
pub async fn toggle_deafen(
|
||||
State(state): State<ServerState>,
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<VoiceRequest>,
|
||||
) -> Result<Json<bool>, axum::http::StatusCode> {
|
||||
let token = extract_token(&headers).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
let mut s = state.write().await;
|
||||
let session = s.sessions.get_mut(&token).ok_or(axum::http::StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
session.voice_manager.toggle_deafen(&req.room_id, &session.user_id)
|
||||
.ok_or(axum::http::StatusCode::BAD_REQUEST)
|
||||
.map(Json)
|
||||
}
|
||||
117
server/src/state.rs
Normal file
117
server/src/state.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use matrix_sdk::Client;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct Session {
|
||||
pub client: Client,
|
||||
pub user_id: String,
|
||||
pub voice_manager: VoiceManager,
|
||||
}
|
||||
|
||||
pub struct VoiceManager {
|
||||
channels: HashMap<String, VoiceChannel>,
|
||||
active_channel: Option<String>,
|
||||
}
|
||||
|
||||
pub struct VoiceChannel {
|
||||
pub room_id: String,
|
||||
pub participants: Vec<VoiceParticipant>,
|
||||
}
|
||||
|
||||
pub struct VoiceParticipant {
|
||||
pub user_id: String,
|
||||
pub muted: bool,
|
||||
pub deafened: bool,
|
||||
pub streaming: bool,
|
||||
}
|
||||
|
||||
impl VoiceManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channels: HashMap::new(),
|
||||
active_channel: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_channel(&mut self, room_id: String, user_id: String) -> bool {
|
||||
if let Some(ref old_channel) = self.active_channel {
|
||||
self.leave_channel_internal(old_channel, &user_id);
|
||||
}
|
||||
let channel = self.channels.entry(room_id.clone()).or_insert_with(|| VoiceChannel {
|
||||
room_id: room_id.clone(),
|
||||
participants: Vec::new(),
|
||||
});
|
||||
if channel.participants.iter().any(|p| p.user_id == user_id) {
|
||||
return false;
|
||||
}
|
||||
channel.participants.push(VoiceParticipant {
|
||||
user_id,
|
||||
muted: false,
|
||||
deafened: false,
|
||||
streaming: false,
|
||||
});
|
||||
self.active_channel = Some(room_id);
|
||||
true
|
||||
}
|
||||
|
||||
fn leave_channel_internal(&mut self, room_id: &str, user_id: &str) {
|
||||
if let Some(channel) = self.channels.get_mut(room_id) {
|
||||
channel.participants.retain(|p| p.user_id != user_id);
|
||||
if channel.participants.is_empty() {
|
||||
self.channels.remove(room_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leave_channel(&mut self, room_id: &str, user_id: &str) -> bool {
|
||||
self.leave_channel_internal(room_id, user_id);
|
||||
if self.active_channel.as_deref() == Some(room_id) {
|
||||
self.active_channel = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, room_id: &str, user_id: &str) -> Option<bool> {
|
||||
if let Some(channel) = self.channels.get_mut(room_id) {
|
||||
if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) {
|
||||
participant.muted = !participant.muted;
|
||||
return Some(participant.muted);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(&mut self, room_id: &str, user_id: &str) -> Option<bool> {
|
||||
if let Some(channel) = self.channels.get_mut(room_id) {
|
||||
if let Some(participant) = channel.participants.iter_mut().find(|p| p.user_id == user_id) {
|
||||
participant.deafened = !participant.deafened;
|
||||
if participant.deafened {
|
||||
participant.muted = true;
|
||||
}
|
||||
return Some(participant.deafened);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerStateInner {
|
||||
pub sessions: HashMap<String, Session>,
|
||||
}
|
||||
|
||||
impl ServerStateInner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sessions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServerState = Arc<RwLock<ServerStateInner>>;
|
||||
|
||||
impl ServerState {
|
||||
pub fn new() -> Self {
|
||||
Arc::new(RwLock::new(ServerStateInner::new()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user