mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 13:36:59 +00:00
@@ -18,6 +18,7 @@ querystring = "1.1.0"
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
@@ -34,3 +35,6 @@ async-std = { version = "1.8.0", features = [
|
||||
# core
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
sentry = "0.31.5"
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
@@ -29,6 +29,10 @@ async fn main() {
|
||||
|
||||
// Start accepting new connections and spawn a client for each connection.
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
websocket::spawn_client(database::get_db(), stream, addr);
|
||||
async_std::task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
websocket::client(database::get_db(), stream, addr).await;
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use fred::interfaces::{ClientLike, EventInterface, PubsubInterface};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
@@ -9,249 +16,253 @@ use revolt_quark::{
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
redis_kiss, Database,
|
||||
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE},
|
||||
Database,
|
||||
};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex, task};
|
||||
use async_std::{net::TcpStream, sync::Mutex};
|
||||
|
||||
use crate::config::WebsocketHandshakeCallback;
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
|
||||
/// Spawn a new WebSocket client worker given access to the database,
|
||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||
|
||||
/// Start a new WebSocket client worker given access to the database,
|
||||
/// the relevant TCP stream and the remote address of the client.
|
||||
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Spawn a new Async task to work on.
|
||||
task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (mut write, mut read) = ws.split();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
return;
|
||||
};
|
||||
let user = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if write
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
{
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
if let Ok(mut config) = receiver.await {
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
};
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
'outer: while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
{
|
||||
let write = Mutex::new(write);
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(user_id.clone(), &config, read, &write).fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let Ok(subscriber) = fred::prelude::Builder::default_centralized().build_subscriber_client()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if subscriber.init().await.is_err() {
|
||||
return;
|
||||
};
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
subscriber.unsubscribe_all().await.unwrap();
|
||||
for id in state.iter_subscriptions() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
if let Some(token) = config.get_session_token().as_ref() {
|
||||
match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => {
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
subscriber.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&ready_payload))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = async {
|
||||
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
|
||||
{
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
for id in state.iter_subscriptions() {
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
conn.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// * Debug logging of current subscriptions.
|
||||
/*#[cfg(debug_assertions)]
|
||||
info!(
|
||||
"User {addr:?} is subscribed to {:?}",
|
||||
state
|
||||
.iter_subscriptions()
|
||||
.collect::<Vec<&String>>()
|
||||
);*/
|
||||
|
||||
// Handle incoming events.
|
||||
match conn.on_message().next().await.map(|res| {
|
||||
res.map(|item|(
|
||||
item.get_channel_name().to_string(),
|
||||
redis_kiss::decode_payload::<EventV1>(&item),
|
||||
))
|
||||
}) {
|
||||
Some(Ok((channel, item))) => {
|
||||
if let Ok(mut event) = item {
|
||||
if state
|
||||
.handle_incoming_event_v1(
|
||||
db, &mut event,
|
||||
)
|
||||
.await
|
||||
&& write.lock().await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to deserialise an event for {channel}!");
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
info!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break
|
||||
}
|
||||
// No more data, assume we disconnected or otherwise
|
||||
// something bad occurred, so disconnect user.
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker =
|
||||
async {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
if let Ok(payload) = config.decode(&msg) {
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(
|
||||
&EventV1::Pong { data },
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
);
|
||||
|
||||
// * Combine the streams back once we are ready to disconnect.
|
||||
/* ws = read.reunite(write).unwrap(); */
|
||||
}
|
||||
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
write.lock().await.send(config.encode(&err)).await.ok();
|
||||
}
|
||||
}
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// * Disconnect the WebSocket if it isn't already.
|
||||
/*ws.close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: std::borrow::Cow::from(""),
|
||||
}))
|
||||
.await
|
||||
.unwrap();*/
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
info!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok())
|
||||
else {
|
||||
warn!("Failed to deserialise an event for {}!", message.channel);
|
||||
return;
|
||||
};
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
|
||||
if should_send
|
||||
&& write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user