forked from jmug/stoatchat
Compare commits
21 Commits
20240611-2
...
20240625-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 |
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -138,11 +138,23 @@ version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
"concurrent-queue 1.2.2",
|
||||
"event-listener 2.5.2",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue 2.5.0",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.4.1"
|
||||
@@ -150,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"concurrent-queue 1.2.2",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
@@ -163,7 +175,7 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
@@ -180,7 +192,7 @@ version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"concurrent-queue 1.2.2",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -199,7 +211,7 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -211,7 +223,7 @@ dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -237,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
|
||||
dependencies = [
|
||||
"async-attributes",
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
@@ -531,7 +543,7 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-task",
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
@@ -729,6 +741,15 @@ dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.13.3"
|
||||
@@ -833,13 +854,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.8"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
]
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
@@ -1295,6 +1312,27 @@ version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
||||
dependencies = [
|
||||
"concurrent-queue 2.5.0",
|
||||
"parking",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener 5.3.1",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fake-simd"
|
||||
version = "0.1.2"
|
||||
@@ -1976,13 +2014,13 @@ version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"castaway",
|
||||
"crossbeam-utils",
|
||||
"curl",
|
||||
"curl-sys",
|
||||
"encoding_rs",
|
||||
"event-listener",
|
||||
"event-listener 2.5.2",
|
||||
"futures-lite",
|
||||
"http",
|
||||
"log",
|
||||
@@ -2266,6 +2304,12 @@ dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru_time_cache"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@@ -3408,8 +3452,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-std",
|
||||
"async-tungstenite",
|
||||
"authifier",
|
||||
@@ -3418,6 +3463,7 @@ dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"lru 0.7.6",
|
||||
"lru_time_cache",
|
||||
"once_cell",
|
||||
"querystring",
|
||||
"redis-kiss",
|
||||
@@ -3436,7 +3482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-config"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"cached",
|
||||
@@ -3452,7 +3498,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
@@ -3498,9 +3544,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"async-std",
|
||||
"authifier",
|
||||
"bitfield",
|
||||
@@ -3544,7 +3590,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
@@ -3561,7 +3607,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3576,7 +3622,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"log",
|
||||
@@ -3587,7 +3633,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
dependencies = [
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
@@ -4362,7 +4408,7 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-channel 1.6.1",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -14,6 +14,8 @@ lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
@@ -39,7 +41,7 @@ revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.7.4", path = "../core/permissions" }
|
||||
revolt-permissions = { version = "0.7.10", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
|
||||
@@ -92,6 +92,7 @@ impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -191,6 +192,10 @@ impl State {
|
||||
|
||||
for server in &servers {
|
||||
self.insert_subscription(server.id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", server.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
@@ -397,6 +402,11 @@ impl State {
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", id)).await;
|
||||
}
|
||||
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::RwLock;
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
@@ -34,6 +36,7 @@ pub enum SubscriptionStateChange {
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
@@ -47,6 +50,7 @@ impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
@@ -65,7 +69,9 @@ pub struct State {
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
pub state: SubscriptionStateChange,
|
||||
|
||||
pub subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -86,6 +92,10 @@ impl State {
|
||||
State {
|
||||
cache,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(900),
|
||||
5,
|
||||
))),
|
||||
session_id,
|
||||
private_topic,
|
||||
state: SubscriptionStateChange::Reset,
|
||||
@@ -94,6 +104,39 @@ impl State {
|
||||
|
||||
/// Apply currently queued state
|
||||
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
// Check if we need to change subscriptions to member event topics
|
||||
if !self.cache.is_bot {
|
||||
enum Server {
|
||||
Subscribe(String),
|
||||
Unsubscribe(String),
|
||||
}
|
||||
|
||||
let active_server_changes: Vec<Server> = {
|
||||
let mut active_servers = self.active_servers.lock().await;
|
||||
active_servers
|
||||
.notify_iter()
|
||||
.map(|e| match e {
|
||||
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
|
||||
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
|
||||
})
|
||||
.collect()
|
||||
// It is bad practice to open more than one Mutex at once and could
|
||||
// lead to a deadlock, so instead we choose to collect the changes.
|
||||
};
|
||||
|
||||
for entry in active_server_changes {
|
||||
match entry {
|
||||
Server::Subscribe(k) => {
|
||||
self.insert_subscription(k).await;
|
||||
}
|
||||
Server::Unsubscribe(k) => {
|
||||
self.remove_subscription(&k).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes to subscriptions
|
||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, RedisErrorKind},
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
pin_mut, select,
|
||||
join, pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
@@ -22,8 +23,10 @@ use revolt_presence::{create_session, delete_session};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
@@ -52,6 +55,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
@@ -63,10 +67,8 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// 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())
|
||||
{
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
@@ -75,7 +77,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write.send(config.encode(&create_error!(InvalidSession))).await.ok();
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -94,20 +99,24 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if write
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
let ready_payload = match state.generate_ready_payload(db).await {
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,22 +129,44 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(addr, subscribed, 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 => {}
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
@@ -146,31 +177,88 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if subscriber.init().await.is_err() {
|
||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
{
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(err) => {
|
||||
error!("Failed to build a subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = subscriber.init().await {
|
||||
error!("Failed to init subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
loop {
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
subscriber.unsubscribe_all().await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
||||
error!("Unsubscribe all failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -181,149 +269,255 @@ async fn listener(
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
subscriber.unsubscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
||||
error!("Unsubscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
warn!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let t1 = message_rx.recv().fuse();
|
||||
let t2 = topic_signal_r.recv().fuse();
|
||||
let t3 = kill_signal_r.recv().fuse();
|
||||
let t4 = clean_up_r.recv().fuse();
|
||||
|
||||
let event = 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()),
|
||||
};
|
||||
pin_mut!(t1, t2, t3, t4);
|
||||
|
||||
let Some(mut event) = event else {
|
||||
warn!("Failed to deserialise an event for {}!", message.channel);
|
||||
return;
|
||||
};
|
||||
select! {
|
||||
_ = t4 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t3 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t2 => {},
|
||||
message = t1 => {
|
||||
// Handle incoming events.
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
error!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break 'out;
|
||||
}
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
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;
|
||||
let event = 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) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise an event for {}! Introspection: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
.as_string()
|
||||
.map(|x| x.chars().take(32).collect::<String>())
|
||||
);
|
||||
|
||||
error!("{}", err);
|
||||
sentry::capture_message(&err, Level::Error);
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
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 {
|
||||
event = EventV1::Logout;
|
||||
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;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while sending an event to {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
break 'out;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
info!("User {addr:?} received log out event!");
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while sending an event to {addr:?}: {e:?}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = subscriber.quit().await {
|
||||
error!("{}", err);
|
||||
sentry::capture_error(&err);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker_with_kill_signal(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
worker(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id,
|
||||
config,
|
||||
topic_signal_s,
|
||||
kill_signal_r,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
loop {
|
||||
let result = read.try_next().await;
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while reading an event from {addr:?}: {e:?}");
|
||||
}
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t2 => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
},
|
||||
result = t1 => {
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => {
|
||||
warn!("Received a None message!");
|
||||
sentry::capture_message("Received a None message!", Level::Warning);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while reading an event from {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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();
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
servers.insert(server_id, ());
|
||||
|
||||
if !has_item {
|
||||
// Poke the listener to adjust subscriptions
|
||||
topic_signal_s.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -46,19 +46,40 @@ webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.default]
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_attachments = 5
|
||||
message_reactions = 20
|
||||
servers = 100
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
new_user_days = 3
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
|
||||
@@ -105,19 +105,27 @@ pub struct Api {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub struct GlobalLimits {
|
||||
pub group_size: usize,
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_attachments: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub servers: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
pub server_channels: usize,
|
||||
|
||||
pub new_user_days: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub attachment_size: usize,
|
||||
pub avatar_size: usize,
|
||||
pub background_size: usize,
|
||||
@@ -128,6 +136,9 @@ pub struct FeaturesLimits {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.4", path = "../config" }
|
||||
revolt-result = { version = "0.7.4", path = "../result" }
|
||||
revolt-models = { version = "0.7.4", path = "../models", features = [
|
||||
revolt-config = { version = "0.7.10", path = "../config" }
|
||||
revolt-result = { version = "0.7.10", path = "../result" }
|
||||
revolt-models = { version = "0.7.10", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.4", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.4", path = "../permissions", features = [
|
||||
revolt-presence = { version = "0.7.10", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.10", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
|
||||
@@ -249,7 +249,7 @@ impl EventV1 {
|
||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,6 +259,11 @@ impl EventV1 {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish server member event
|
||||
pub async fn server(self, id: String) {
|
||||
self.p(format!("{id}u")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
|
||||
@@ -8,5 +8,6 @@ pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
Subscribe { server_id: String },
|
||||
Ping { data: Ping, responded: Option<()> },
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ impl Bot {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
|
||||
return Err(create_error!(ReachedMaximumBots));
|
||||
}
|
||||
|
||||
|
||||
@@ -201,9 +201,9 @@ impl Channel {
|
||||
update_server: bool,
|
||||
) -> Result<Channel> {
|
||||
let config = config().await;
|
||||
if server.channels.len() > config.features.limits.default.server_channels {
|
||||
if server.channels.len() > config.features.limits.global.server_channels {
|
||||
return Err(create_error!(TooManyChannels {
|
||||
max: config.features.limits.default.server_channels,
|
||||
max: config.features.limits.global.server_channels,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -263,9 +263,9 @@ impl Channel {
|
||||
data.users.insert(owner_id.to_string());
|
||||
|
||||
let config = config().await;
|
||||
if data.users.len() > config.features.limits.default.group_size {
|
||||
if data.users.len() > config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size,
|
||||
max: config.features.limits.global.group_size,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -346,9 +346,9 @@ impl Channel {
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.default.group_size {
|
||||
if recipients.len() >= config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size
|
||||
max: config.features.limits.global.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
|
||||
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
@@ -207,11 +207,13 @@ impl Default for Message {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Message {
|
||||
/// Create message from API data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_from_api(
|
||||
db: &Database,
|
||||
channel: Channel,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
limits: FeaturesLimits,
|
||||
mut idempotency: IdempotencyKey,
|
||||
generate_embeds: bool,
|
||||
allow_mentions: bool,
|
||||
@@ -221,7 +223,7 @@ impl Message {
|
||||
Message::validate_sum(
|
||||
&data.content,
|
||||
data.embeds.as_deref().unwrap_or_default(),
|
||||
config.features.limits.default.message_length,
|
||||
limits.message_length,
|
||||
)?;
|
||||
|
||||
idempotency
|
||||
@@ -288,9 +290,9 @@ impl Message {
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > config.features.limits.default.message_replies {
|
||||
if entries.len() > config.features.limits.global.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
max: config.features.limits.default.message_replies,
|
||||
max: config.features.limits.global.message_replies,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -320,20 +322,20 @@ impl Message {
|
||||
if data
|
||||
.attachments
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
|
||||
.is_some_and(|v| v.len() > limits.message_attachments)
|
||||
{
|
||||
return Err(create_error!(TooManyAttachments {
|
||||
max: config.features.limits.default.message_attachments,
|
||||
max: limits.message_attachments,
|
||||
}));
|
||||
}
|
||||
|
||||
if data
|
||||
.embeds
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
|
||||
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
|
||||
{
|
||||
return Err(create_error!(TooManyEmbeds {
|
||||
max: config.features.limits.default.message_embeds,
|
||||
max: config.features.limits.global.message_embeds,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -504,7 +506,35 @@ impl Message {
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.map(|m| m.author.clone())
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
v0::SystemMessage::ChannelDescriptionChanged { by } => {
|
||||
users.push(by.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
v0::SystemMessage::UserAdded { by, id, .. }
|
||||
| v0::SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::UserBanned { id, .. }
|
||||
| v0::SystemMessage::UserKicked { id, .. }
|
||||
| v0::SystemMessage::UserJoined { id, .. }
|
||||
| v0::SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::Text { .. } => {}
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
@@ -588,7 +618,7 @@ impl Message {
|
||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||
// Check how many reactions are already on the message
|
||||
let config = config().await;
|
||||
if self.reactions.len() >= config.features.limits.default.message_reactions
|
||||
if self.reactions.len() >= config.features.limits.global.message_reactions
|
||||
&& !self.reactions.contains_key(emoji)
|
||||
{
|
||||
return Err(create_error!(InvalidOperation));
|
||||
@@ -753,7 +783,7 @@ impl Interactions {
|
||||
if let Some(reactions) = &self.reactions {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||
|
||||
if reactions.len() > config.features.limits.default.message_reactions {
|
||||
if reactions.len() > config.features.limits.global.message_reactions {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
|
||||
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_config::config;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0;
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
@@ -197,6 +197,22 @@ impl User {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Get limits for this user
|
||||
pub async fn limits(&self) -> FeaturesLimits {
|
||||
let config = config().await;
|
||||
if ulid::Ulid::from_str(&self.id)
|
||||
.expect("`ulid`")
|
||||
.datetime()
|
||||
.elapsed()
|
||||
.expect("time went backwards")
|
||||
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
|
||||
{
|
||||
config.features.limits.new_user
|
||||
} else {
|
||||
config.features.limits.default
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the relationship with another user
|
||||
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
|
||||
if self.id == user_b {
|
||||
@@ -235,12 +251,11 @@ impl User {
|
||||
|
||||
/// Check if this user can acquire another server
|
||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
||||
let config = config().await;
|
||||
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
|
||||
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(TooManyServers {
|
||||
max: config.features.limits.default.servers
|
||||
max: self.limits().await.servers
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -477,6 +492,7 @@ impl User {
|
||||
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
||||
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
||||
RelationshipStatus::Incoming => {
|
||||
// Accept incoming friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
@@ -486,6 +502,26 @@ impl User {
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::None => {
|
||||
// Get this user's current count of outgoing friend requests
|
||||
let count = self
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// If we're over the limit, don't allow creating more requests
|
||||
if count >= self.limits().await.outgoing_friend_requests {
|
||||
return Err(create_error!(TooManyPendingFriendRequests {
|
||||
max: self.limits().await.outgoing_friend_requests
|
||||
}));
|
||||
}
|
||||
|
||||
// Send the friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn worker(db: Database) {
|
||||
let embeds = generate(
|
||||
task.content,
|
||||
&config.hosts.january,
|
||||
config.features.limits.default.message_embeds,
|
||||
config.features.limits.global.message_embeds,
|
||||
semaphore,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1009,11 +1009,6 @@ impl crate::User {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
@@ -1086,11 +1081,6 @@ impl crate::User {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
@@ -1124,7 +1114,6 @@ impl crate::User {
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
profile: self.profile.map(|profile| profile.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
@@ -1152,7 +1141,7 @@ impl From<User> for crate::User {
|
||||
relations: None,
|
||||
badges: Some(value.badges as i32),
|
||||
status: value.status.map(Into::into),
|
||||
profile: value.profile.map(Into::into),
|
||||
profile: None,
|
||||
flags: Some(value.flags as i32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(Into::into),
|
||||
@@ -1175,7 +1164,6 @@ impl From<crate::PartialUser> for PartialUser {
|
||||
}),
|
||||
badges: value.badges.map(|badges| badges as u32),
|
||||
status: value.status.map(|status| status.into()),
|
||||
profile: value.profile.map(|profile| profile.into()),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(|bot| bot.into()),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
@@ -44,9 +46,27 @@ impl Reference {
|
||||
db.fetch_channel(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
db.fetch_invite(&self.id).await
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
let server = self.as_server(db).await?;
|
||||
if !server.discoverable {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(Invite::Server {
|
||||
code: self.id.to_string(),
|
||||
server: server.id,
|
||||
creator: server.owner,
|
||||
channel: server
|
||||
.channels
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(create_error!(NotFound))?,
|
||||
})
|
||||
} else {
|
||||
db.fetch_invite(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch message from Ref
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.4", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.4", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.10", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.10", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
|
||||
@@ -50,9 +50,6 @@ auto_derived_partial!(
|
||||
/// User's current status
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub status: Option<UserStatus>,
|
||||
/// User's profile page
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub profile: Option<UserProfile>,
|
||||
|
||||
/// Enum of user flags
|
||||
#[cfg_attr(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.7.4", path = "../result" }
|
||||
revolt-result = { version = "0.7.10", path = "../result" }
|
||||
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
@@ -28,6 +30,14 @@ pub struct Error {
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?} occurred in {}", self.error_type, self.location)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
/// Possible error types
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(tag = "type"))]
|
||||
@@ -50,6 +60,9 @@ pub enum ErrorType {
|
||||
Blocked,
|
||||
BlockedByOther,
|
||||
NotFriends,
|
||||
TooManyPendingFriendRequests {
|
||||
max: usize,
|
||||
},
|
||||
|
||||
// ? Channel related errors
|
||||
UnknownChannel,
|
||||
|
||||
@@ -25,6 +25,7 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::Blocked => Status::Conflict,
|
||||
ErrorType::BlockedByOther => Status::Forbidden,
|
||||
ErrorType::NotFriends => Status::Forbidden,
|
||||
ErrorType::TooManyPendingFriendRequests { .. } => Status::BadRequest,
|
||||
|
||||
ErrorType::UnknownChannel => Status::NotFound,
|
||||
ErrorType::UnknownMessage => Status::NotFound,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.7.4"
|
||||
version = "0.7.10"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -47,8 +47,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: Default::default(),
|
||||
// config: authifier_config().await,
|
||||
config: authifier_config().await,
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
|
||||
@@ -30,11 +30,10 @@ pub async fn edit(
|
||||
})
|
||||
})?;
|
||||
|
||||
let config = config().await;
|
||||
Message::validate_sum(
|
||||
&edit.content,
|
||||
edit.embeds.as_deref().unwrap_or_default(),
|
||||
config.features.limits.default.message_length,
|
||||
user.limits().await.message_length,
|
||||
)?;
|
||||
|
||||
// Ensure we have permissions to send a message
|
||||
|
||||
@@ -81,6 +81,7 @@ pub async fn message_send(
|
||||
channel,
|
||||
data,
|
||||
v0::MessageAuthor::User(&author),
|
||||
user.limits().await,
|
||||
idempotency,
|
||||
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
|
||||
allow_mentions,
|
||||
|
||||
@@ -45,9 +45,9 @@ pub async fn create_emoji(
|
||||
|
||||
// Check that we haven't hit the emoji limit
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
if emojis.len() >= config.features.limits.default.server_emoji {
|
||||
if emojis.len() >= config.features.limits.global.server_emoji {
|
||||
return Err(create_error!(TooManyEmoji {
|
||||
max: config.features.limits.default.server_emoji,
|
||||
max: config.features.limits.global.server_emoji,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ pub async fn edit(
|
||||
})
|
||||
})?;
|
||||
|
||||
// Fetch server, target member and current permissions
|
||||
// Fetch server and target member
|
||||
let mut server = server.as_server(db).await?;
|
||||
let mut member = target.as_member(db, &server.id).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user)
|
||||
.server(&server)
|
||||
.member(&member);
|
||||
|
||||
// Fetch our currrent permissions
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||
let permissions = calculate_server_permissions(&mut query).await;
|
||||
|
||||
// Check permissions in server
|
||||
|
||||
@@ -34,9 +34,9 @@ pub async fn create(
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
|
||||
|
||||
let config = config().await;
|
||||
if server.roles.len() >= config.features.limits.default.server_roles {
|
||||
if server.roles.len() >= config.features.limits.global.server_roles {
|
||||
return Err(create_error!(TooManyRoles {
|
||||
max: config.features.limits.default.server_roles,
|
||||
max: config.features.limits.global.server_roles,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use revolt_config::config;
|
||||
use revolt_database::{
|
||||
util::{idempotency::IdempotencyKey, reference::Reference},
|
||||
Database, Message,
|
||||
@@ -58,6 +59,7 @@ pub async fn webhook_execute(
|
||||
channel,
|
||||
data,
|
||||
v0::MessageAuthor::Webhook(&webhook.into()),
|
||||
config().await.features.limits.default,
|
||||
idempotency,
|
||||
true,
|
||||
true,
|
||||
|
||||
Reference in New Issue
Block a user