feat(bonfire): add disconnection mechanism

feat(bonfire): handle session deletion and logout events
feat(core): trigger logout on bot token reset
This commit is contained in:
Paul Makles
2024-06-11 12:38:32 +01:00
parent 92e948aabc
commit 2cb20618da
10 changed files with 68 additions and 25 deletions

1
Cargo.lock generated
View File

@@ -3412,6 +3412,7 @@ version = "0.7.3"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
"authifier",
"bincode", "bincode",
"fred", "fred",
"futures", "futures",

View File

@@ -34,6 +34,7 @@ async-std = { version = "1.8.0", features = [
] } ] }
# core # core
authifier = { version = "1.0.8" }
revolt-result = { path = "../core/result" } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }

View File

@@ -58,6 +58,7 @@ impl Default for Cache {
pub struct State { pub struct State {
pub cache: Cache, pub cache: Cache,
pub session_id: String,
pub private_topic: String, pub private_topic: String,
subscribed: HashSet<String>, subscribed: HashSet<String>,
state: SubscriptionStateChange, state: SubscriptionStateChange,
@@ -65,7 +66,7 @@ pub struct State {
impl State { impl State {
/// Create state from User /// Create state from User
pub fn from(user: User) -> State { pub fn from(user: User, session_id: String) -> State {
let mut subscribed = HashSet::new(); let mut subscribed = HashSet::new();
let private_topic = format!("{}!", user.id); let private_topic = format!("{}!", user.id);
subscribed.insert(private_topic.clone()); subscribed.insert(private_topic.clone());
@@ -81,6 +82,7 @@ impl State {
State { State {
cache, cache,
subscribed, subscribed,
session_id,
private_topic, private_topic,
state: SubscriptionStateChange::Reset, state: SubscriptionStateChange::Reset,
} }

View File

@@ -1,6 +1,7 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use async_tungstenite::WebSocketStream; use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use fred::{ use fred::{
interfaces::{ClientLike, EventInterface, PubsubInterface}, interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig, types::RedisConfig,
@@ -73,17 +74,19 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
write.send(config.encode(&create_error!(InvalidSession))).await.ok(); write.send(config.encode(&create_error!(InvalidSession))).await.ok();
return; return;
}; };
let user = match User::from_token(db, token, UserHint::Any).await {
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user, Ok(user) => user,
Err(err) => { Err(err) => {
write.send(config.encode(&err)).await.ok(); write.send(config.encode(&err)).await.ok();
return; return;
} }
}; };
info!("User {addr:?} authenticated as @{}", user.username); info!("User {addr:?} authenticated as @{}", user.username);
// Create local state. // Create local state.
let mut state = State::from(user); let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone(); let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated. // Notify socket we have authenticated.
@@ -189,6 +192,7 @@ async fn listener(
}) else { }) else {
return; return;
}; };
let event = match *REDIS_PAYLOAD_TYPE { let event = match *REDIS_PAYLOAD_TYPE {
PayloadType::Json => message PayloadType::Json => message
.value .value
@@ -203,13 +207,34 @@ async fn listener(
.as_bytes() .as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()), .and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
}; };
let Some(mut event) = event else { let Some(mut event) = event else {
warn!("Failed to deserialise an event for {}!", message.channel); warn!("Failed to deserialise an event for {}!", message.channel);
return; return;
}; };
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send { if let EventV1::Auth(auth) = &event {
continue; if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
if &state.session_id == session_id {
event = EventV1::Logout;
}
} else if let AuthifierEvent::DeleteAllSessions {
exclude_session_id, ..
} = auth
{
if let Some(excluded) = exclude_session_id {
if &state.session_id != excluded {
event = EventV1::Logout;
}
} else {
event = EventV1::Logout;
}
}
} else {
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
} }
let result = write.lock().await.send(config.encode(&event)).await; let result = write.lock().await.send(config.encode(&event)).await;
@@ -218,6 +243,11 @@ async fn listener(
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) { if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while sending an event to {addr:?}: {e:?}"); warn!("Error while sending an event to {addr:?}: {e:?}");
} }
return;
}
if let EventV1::Logout = event {
return; return;
} }
} }

View File

@@ -48,6 +48,8 @@ pub enum EventV1 {
/// Successfully authenticated /// Successfully authenticated
Authenticated, Authenticated,
/// Logged out
Logout,
/// Basic data to cache /// Basic data to cache
Ready { Ready {
users: Vec<User>, users: Vec<User>,

View File

@@ -2,7 +2,7 @@ use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{BotInformation, Database, PartialUser, User}; use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
auto_derived_partial!( auto_derived_partial!(
/// Bot /// Bot
@@ -142,6 +142,10 @@ impl Bot {
db.update_bot(&self.id, &partial, remove).await?; db.update_bot(&self.id, &partial, remove).await?;
if partial.token.is_some() {
EventV1::Logout.private(self.id.clone()).await;
}
self.apply_options(partial); self.apply_options(partial);
Ok(()) Ok(())
} }

View File

@@ -276,23 +276,27 @@ impl User {
Ok(username) Ok(username)
} }
/// Find a user from a given token and hint /// Find a user and session ID from a given token and hint
#[async_recursion] #[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> { pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
match hint { match hint {
UserHint::Bot => { UserHint::Bot => Ok((
db.fetch_user( db.fetch_user(
&db.fetch_bot_by_token(token) &db.fetch_bot_by_token(token)
.await .await
.map_err(|_| create_error!(InvalidSession))? .map_err(|_| create_error!(InvalidSession))?
.id, .id,
) )
.await .await?,
String::new(),
)),
UserHint::User => {
let session = db.fetch_session_by_token(token).await?;
Ok((db.fetch_user(&session.user_id).await?, session.id))
} }
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => { UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await { if let Ok(result) = User::from_token(db, token, UserHint::User).await {
Ok(user) Ok(result)
} else { } else {
User::from_token(db, token, UserHint::Bot).await User::from_token(db, token, UserHint::Bot).await
} }

View File

@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -16,8 +17,8 @@ pub trait AbstractUsers: Sync + Send {
/// Fetch a user from the database by their username /// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>; async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>; async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>; async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;

View File

@@ -46,10 +46,9 @@ impl AbstractUsers for MongoDb {
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, token: &str) -> Result<User> { async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
let session = self self.col::<Session>("sessions")
.col::<Session>("sessions")
.find_one( .find_one(
doc! { doc! {
"token": token "token": token
@@ -58,9 +57,7 @@ impl AbstractUsers for MongoDb {
) )
.await .await
.map_err(|_| create_database_error!("find_one", "sessions"))? .map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?; .ok_or_else(|| create_error!(InvalidSession))
self.fetch_user(&session.user_id).await
} }
/// Fetch multiple users by their ids /// Fetch multiple users by their ids

View File

@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -40,8 +41,8 @@ impl AbstractUsers for ReferenceDb {
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> { async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
todo!() todo!()
} }