Compare commits

..

4 Commits

Author SHA1 Message Date
izzy
3dc7c32723 chore: ignore WS protocol errors 2025-06-28 18:55:15 +01:00
izzy
083229c7f5 refactor: one channel per consumer 2025-06-28 18:26:51 +01:00
izzy
9d576d2430 refactor: move lapin code into revolt-broker crate 2025-06-28 17:23:44 +01:00
izzy
85989a2138 feat: switch to RabbitMQ streams from Redis Pub/Sub
refactor: redo the bonfire client structure
2025-06-28 13:35:18 +01:00
240 changed files with 2934 additions and 5714 deletions

View File

@@ -49,6 +49,13 @@ jobs:
uses: docker/setup-buildx-action@v2
# Authenticate with Docker Hub and GHCR
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v2
with:
@@ -65,13 +72,14 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
# stoatchat/api
# revoltchat/server
- name: Docker meta
id: meta-delta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/api
docker.io/revoltchat/server
ghcr.io/revoltchat/server
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -84,13 +92,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-delta.outputs.labels }}
# stoatchat/events
# revoltchat/bonfire
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/events
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -103,13 +112,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-bonfire.outputs.labels }}
# stoatchat/file-server
# revoltchat/autumn
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/file-server
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -122,13 +132,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-autumn.outputs.labels }}
# stoatchat/proxy
# revoltchat/january
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/proxy
docker.io/revoltchat/january
ghcr.io/revoltchat/january
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -141,32 +152,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }}
# stoatchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/gifbox/Dockerfile
tags: ${{ steps.meta-gifbox.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-gifbox.outputs.labels }}
# stoatchat/crond
# revoltchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/crond
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -179,13 +172,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-crond.outputs.labels }}
# stoatchat/pushd
# revoltchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/pushd
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:

View File

@@ -73,7 +73,7 @@ jobs:
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
with:
repository: stoatchat/api
repository: revoltchat/api
path: api
token: ${{ secrets.PAT }}

View File

@@ -14,7 +14,7 @@ jobs:
run: |
gh api graphql -f query='
query {
organization(login: "stoatchat"){
organization(login: "revoltchat"){
projectV2(number: 3) {
id
fields(first:20) {

View File

@@ -14,7 +14,7 @@ jobs:
run: |
gh api graphql -f query='
query {
organization(login: "stoatchat"){
organization(login: "revoltchat"){
projectV2(number: 5) {
id
fields(first:20) {

2239
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,8 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps

View File

@@ -23,11 +23,8 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps

View File

@@ -21,11 +21,9 @@ The services and libraries that power the Revolt service.<br/>
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service | ![Crates.io Version](https://img.shields.io/crates/v/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/size/revolt-coalesced) ![Crates.io License](https://img.shields.io/crates/l/revolt-coalesced) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
@@ -68,7 +66,6 @@ As a heads-up, the development environment uses the following ports:
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 |
| `crates/services/gifbox` | 14706 |
Now you can clone and build the project:
@@ -144,8 +141,6 @@ cargo run --bin revolt-bonfire
cargo run --bin revolt-autumn
# run the proxy server
cargo run --bin revolt-january
# run the tenor proxy
cargo run --bin revolt-gifbox
# run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd

View File

@@ -40,9 +40,6 @@ port = 14025
use_tls = false
use_starttls = false
[api.security]
token_secret = "trolt"
[files.s3]
# S3 protocol endpoint
endpoint = "http://127.0.0.1:14009"

View File

@@ -34,7 +34,7 @@ services:
- minio
entrypoint: >
/bin/sh -c "while ! /usr/bin/mc ready minio; do
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
echo 'Waiting minio...' && sleep 1;
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.8.9"
version = "0.8.8"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -9,6 +9,7 @@ edition = "2021"
[dependencies]
# util
log = "*"
rand = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
@@ -19,7 +20,6 @@ async-channel = "2.3.1"
# parsing
querystring = "1.1.0"
regex = "1.11.1"
# serde
bincode = "1.3.3"
@@ -39,11 +39,15 @@ async-std = { version = "1.8.0", features = [
# core
authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result" }
revolt-broker = { path = "../core/broker" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
revolt-permissions = { version = "0.8.8", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
# rabbit
lapin = { version = "3.0.0" }

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage

View File

@@ -0,0 +1,129 @@
use async_std::{net::TcpStream, sync::Mutex};
use async_tungstenite::WebSocketStream;
use futures::{join, SinkExt, StreamExt, TryStreamExt};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
iso8601_timestamp::Timestamp,
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
use revolt_result::create_error;
use crate::{
client::{
subscriber::client_subscriber,
worker::{client_worker, WorkerRef},
},
config::ProtocolConfiguration,
events::state::State,
};
/// Core event loop of gateway clients
pub async fn client_core(
db: &'static Database,
ws: WebSocketStream<TcpStream>,
mut config: ProtocolConfiguration,
) {
// 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(Some(message)) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
config.set_session_token(token);
break;
}
}
}
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
write
.send(config.encode(&EventV1::Error {
data: create_error!(InvalidSession),
}))
.await
.ok();
return;
};
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user,
Err(err) => {
write
.send(config.encode(&EventV1::Error { data: err }))
.await
.ok();
return;
}
};
info!(
"Authenticated user {}#{}",
user.username, user.discriminator
);
db.update_session_last_seen(&session_id, Timestamp::now_utc())
.await
.ok();
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
return;
}
// Download required data to local cache and send Ready payload.
let ready_payload = match report_internal_error!(
state
.generate_ready_payload(db, config.get_ready_payload_fields())
.await
) {
Ok(ready_payload) => ready_payload,
Err(_) => return,
};
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
return;
}
// 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 worker_ref = WorkerRef::from(&state);
let write = Mutex::new(write);
let (reload, reloaded) = async_channel::bounded(1);
let (cancel_1, cancelled_1) = async_channel::bounded(1);
let (cancel_2, cancelled_2) = async_channel::bounded(1);
join!(
async {
client_subscriber(&write, cancelled_1, reloaded, &config, db, &mut state).await;
cancel_2.send(()).await.ok();
},
async {
client_worker(read, &write, cancelled_2, reload, &config, worker_ref).await;
cancel_1.send(()).await.ok();
}
);
}
// 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;
}
}

View File

@@ -0,0 +1,3 @@
pub mod core;
pub mod subscriber;
pub mod worker;

View File

@@ -0,0 +1,112 @@
use async_channel::Receiver;
use async_std::{net::TcpStream, sync::Mutex};
use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use futures::{pin_mut, select, stream::SplitSink, FutureExt, SinkExt};
use revolt_broker::event_stream;
use revolt_database::{events::client::EventV1, Database};
use sentry::Level;
use crate::{
config::ProtocolConfiguration,
events::state::{State, SubscriptionStateChange},
};
/// Event subscriber loop
pub async fn client_subscriber(
write: &Mutex<SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>>,
cancelled: Receiver<()>,
reloaded: Receiver<()>,
protocol_config: &ProtocolConfiguration,
db: &'static Database,
state: &mut State,
) {
let mut consumer = event_stream::Consumer::new().await;
consumer.set_topics(state.subscribed.read().await.clone());
let mut cancel = false;
loop {
// Reload consumer if subscriptions change
if !matches!(state.apply_state().await, SubscriptionStateChange::None) {
consumer.set_topics(state.subscribed.read().await.clone());
}
// Read incoming events
loop {
let reloaded = reloaded.recv().fuse();
let cancelled = cancelled.recv().fuse();
let delivery = consumer.next().fuse();
pin_mut!(delivery, reloaded, cancelled);
select! {
_ = reloaded => {
break;
}
_ = cancelled => {
cancel = true;
break;
}
event = delivery => {
if let Some(mut event) = event {
// Handle the event
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 {
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
}
let result = write.lock().await.send(protocol_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: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
cancel = true;
break;
}
if let EventV1::Logout = event {
info!("User {} received log out event!", state.user_id);
cancel = true;
break;
}
break;
} else {
cancel = true;
break;
}
}
}
}
// Break out if cancelled
if cancel {
break;
}
}
consumer.dispose_channel().await;
}

View File

@@ -0,0 +1,124 @@
use std::{collections::HashSet, sync::Arc};
use async_channel::{Receiver, Sender};
use async_std::{
net::TcpStream,
sync::{Mutex, RwLock},
};
use async_tungstenite::WebSocketStream;
use futures::{
pin_mut, select,
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, TryStreamExt,
};
use revolt_database::events::{client::EventV1, server::ClientMessage};
use sentry::Level;
use crate::{config::ProtocolConfiguration, events::state::State};
pub struct WorkerRef {
user_id: String,
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
subscribed: Arc<RwLock<HashSet<String>>>,
}
impl WorkerRef {
pub fn from(state: &State) -> WorkerRef {
WorkerRef {
user_id: state.user_id.clone(),
active_servers: state.active_servers.clone(),
subscribed: state.subscribed.clone(),
}
}
}
/// Incoming message handling
pub async fn client_worker(
mut read: SplitStream<WebSocketStream<TcpStream>>,
write: &Mutex<SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>>,
cancelled: Receiver<()>,
reload: Sender<()>,
config: &ProtocolConfiguration,
state: WorkerRef,
) {
loop {
let read = read.try_next().fuse();
let cancelled = cancelled.recv().fuse();
pin_mut!(read, cancelled);
select! {
_ = cancelled => { return; },
msg = read => {
let msg = match msg {
Ok(Some(msg)) => msg,
Ok(None) => {
warn!("Received a None message!");
return;
}
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed | Error::Protocol(_)) {
let err = format!("Error while reading an event: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
return;
}
};
let Ok(payload) = config.decode(&msg) else {
continue;
};
match payload {
ClientMessage::BeginTyping { channel } => {
if !state.subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStartTyping {
id: channel.clone(),
user: state.user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
if !state.subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStopTyping {
id: channel.clone(),
user: state.user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Subscribe { server_id } => {
let mut servers = state.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
reload.send(()).await.ok();
}
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(&EventV1::Pong { data }))
.await
.ok();
}
}
_ => {}
}
}
}
}
}

View File

@@ -1,15 +1,9 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
/// Enumeration of supported protocol formats
#[derive(Debug)]
pub enum ProtocolFormat {
@@ -23,7 +17,6 @@ pub struct ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
}
impl ProtocolConfiguration {
@@ -32,13 +25,11 @@ impl ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
) -> Self {
Self {
protocol_version,
format,
session_token,
ready_payload_fields,
}
}
@@ -95,8 +86,14 @@ impl ProtocolConfiguration {
}
/// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
&self.ready_payload_fields
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
vec![
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
}
}
@@ -127,22 +124,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
let mut protocol_version = 1;
let mut format = ProtocolFormat::Json;
let mut session_token = None;
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
// If they pass the ready field, set all fields to false
ReadyPayloadFields {
users: false,
servers: false,
channels: false,
members: false,
emojis: false,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: false,
}
} else {
ReadyPayloadFields::default()
};
// Parse and map parameters from key-value to known variables.
for (key, value) in params {
@@ -158,30 +139,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
_ => {}
},
"token" => session_token = Some(value.into()),
"ready" => {
// Re-enable all the fields the client specifies
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
if let Some(field) = captures.get(0) {
match field.as_str() {
"users" => ready_payload_fields.users = true,
"servers" => ready_payload_fields.servers = true,
"channels" => ready_payload_fields.channels = true,
"members" => ready_payload_fields.members = true,
"emojis" => ready_payload_fields.emojis = true,
"channel_unreads" => ready_payload_fields.channel_unreads = true,
"user_settings" => {
if let Some(subkey) = captures.get(1) {
ready_payload_fields
.user_settings
.push(subkey.as_str().to_string());
}
}
"policy_changes" => ready_payload_fields.policy_changes = true,
_ => {}
}
}
}
}
_ => {}
}
}
@@ -194,7 +151,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
protocol_version,
format,
session_token,
ready_payload_fields,
})
.is_ok()
{

View File

@@ -95,23 +95,21 @@ impl State {
pub async fn generate_ready_payload(
&mut self,
db: &Database,
fields: &ReadyPayloadFields,
fields: Vec<ReadyPayloadFields>,
) -> Result<EventV1> {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes.
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
None
let policy_changes = if user.bot.is_some() {
vec![]
} else {
Some(
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect(),
)
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect()
};
// Find all relationships to the user.
@@ -170,7 +168,7 @@ impl State {
.await?;
// Fetch customisations.
let emojis = if fields.emojis {
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
Some(
db.fetch_emoji_by_parent_ids(
&servers
@@ -178,34 +176,25 @@ impl State {
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?
.into_iter()
.map(|emoji| emoji.into())
.collect(),
.await?,
)
} else {
None
};
// Fetch user settings
let user_settings = if !fields.user_settings.is_empty() {
Some(
db.fetch_user_settings(&user.id, &fields.user_settings)
.await?,
)
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
.iter()
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
{
Some(db.fetch_user_settings(&user.id, keys).await?)
} else {
None
};
// Fetch channel unreads
let channel_unreads = if fields.channel_unreads {
Some(
db.fetch_unreads(&user.id)
.await?
.into_iter()
.map(|unread| unread.into())
.collect(),
)
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
Some(db.fetch_unreads(&user.id).await?)
} else {
None
};
@@ -252,25 +241,30 @@ impl State {
}
Ok(EventV1::Ready {
users: if fields.users { Some(users) } else { None },
servers: if fields.servers {
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
Some(servers.into_iter().map(Into::into).collect())
} else {
None
},
channels: if fields.channels {
channels: if fields.contains(&ReadyPayloadFields::Channels) {
Some(channels.into_iter().map(Into::into).collect())
} else {
None
},
members: if fields.members {
members: if fields.contains(&ReadyPayloadFields::Members) {
Some(members.into_iter().map(Into::into).collect())
} else {
None
},
emojis,
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
user_settings,
channel_unreads,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
policy_changes,
})

View File

@@ -66,6 +66,7 @@ impl Default for Cache {
pub struct State {
pub cache: Cache,
pub user_id: String,
pub session_id: String,
pub private_topic: String,
pub state: SubscriptionStateChange,
@@ -87,6 +88,7 @@ impl State {
..Default::default()
};
let user_id = user.id.clone();
cache.users.insert(user.id.clone(), user);
State {
@@ -96,6 +98,7 @@ impl State {
Duration::from_secs(900),
5,
))),
user_id,
session_id,
private_topic,
state: SubscriptionStateChange::Reset,

View File

@@ -6,10 +6,11 @@ use revolt_presence::clear_region;
#[macro_use]
extern crate log;
pub mod config;
pub mod events;
pub mod client;
mod config;
mod database;
mod events;
mod websocket;
#[async_std::main]

View File

@@ -1,42 +1,11 @@
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use std::net::SocketAddr;
use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use fred::{
error::RedisErrorKind,
interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig,
};
use futures::{
channel::oneshot,
join, pin_mut, select,
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
util::oauth2,
iso8601_timestamp::Timestamp,
Database, User, UserHint,
};
use revolt_models::v0;
use revolt_presence::{create_session, delete_session};
use async_std::net::TcpStream;
use futures::channel::oneshot;
use revolt_database::Database;
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};
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
use crate::client::core::client_core;
use crate::config::WebsocketHandshakeCallback;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
@@ -56,7 +25,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
};
// Verify we've received a valid config, otherwise we should just drop the connection.
let Ok(mut config) = receiver.await else {
let Ok(config) = receiver.await else {
return;
};
@@ -66,474 +35,5 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
config.get_protocol_format()
);
// 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(Some(message)) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
config.set_session_token(token);
break;
}
}
}
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
write
.send(config.encode(&EventV1::Error {
data: create_error!(InvalidSession),
}))
.await
.ok();
return;
};
// Presume the token is a proper token first
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
Ok((user, session_id)) => {
db.update_session_last_seen(&session_id, Timestamp::now_utc())
.await
.ok();
(user, session_id)
},
Err(err) => {
let revolt_config = revolt_config::config().await;
// If it fails to find the user from the token see if its an OAuth2 token
let res = match oauth2::decode_token(&revolt_config.api.security.token_secret, token) {
// Check if the OAuth2 token is allowed to establish an events websocket
Ok(claims) => if !claims.scopes.contains(&v0::OAuth2Scope::Events) {
// TODO: maybe a last_seen system for OAuth2 as well
db.fetch_user(&claims.sub).await.map(|user| (user, claims.jti))
} else {
Err(create_error!(MissingScope { scope: v0::OAuth2Scope::Events.to_string() }))
},
// If its expired return an error
Err(e) => if e.into_kind() == oauth2::JWTErrorKind::ExpiredSignature {
Err(create_error!(ExpiredToken))
} else {
// Finally re-return the error from User::from_token if everything else fails to avoid a confusing error
Err(err)
}
};
match res {
Ok(user) => user,
Err(err) => {
write
.send(config.encode(&EventV1::Error { data: err }))
.await
.ok();
return;
}
}
}
};
info!("User {addr:?} authenticated as @{}", user.username);
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
return;
}
// Download required data to local cache and send Ready payload.
let ready_payload = match report_internal_error!(
state
.generate_ready_payload(db, config.get_ready_payload_fields())
.await
) {
Ok(ready_payload) => ready_payload,
Err(_) => return,
};
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
return;
}
// 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;
}
{
// 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_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;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
#[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 subscriber = match report_internal_error!(
fred::types::Builder::from_config(redis_config).build_subscriber_client()
) {
Ok(subscriber) => subscriber,
Err(_) => return,
};
if report_internal_error!(subscriber.init().await).is_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();
'out: loop {
// Check for state changes for subscriptions.
match state.apply_state().await {
SubscriptionStateChange::Reset => {
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
break 'out;
}
let subscribed = state.subscribed.read().await;
for id in subscribed.iter() {
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out;
}
}
#[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}");
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
break 'out;
}
}
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out;
}
}
}
SubscriptionStateChange::None => {}
}
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();
pin_mut!(t1, t2, t3, t4);
select! {
_ = t4 => {
break 'out;
},
_ = t3 => {
break 'out;
},
_ = t2 => {},
message = t1 => {
// Handle incoming events.
let message = match report_internal_error!(message) {
Ok(message) => message,
Err(_) => break 'out
};
let event = match *REDIS_PAYLOAD_TYPE {
PayloadType::Json => message
.value
.as_str()
.and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
};
let Some(mut event) = event else {
let err = format!(
"Failed to deserialise event for {}: `{:?}`",
message.channel,
message
.value
);
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 {
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;
}
}
}
}
report_internal_error!(subscriber.quit().await).ok();
}
#[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 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);
}
return;
}
};
let Ok(payload) = config.decode(&msg) else {
continue;
};
match payload {
ClientMessage::BeginTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
continue;
}
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();
}
}
_ => {}
}
}
}
}
client_core(db, ws, config).await;
}

View File

@@ -0,0 +1,25 @@
[package]
name = "revolt-broker"
version = "0.8.8"
edition = "2024"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Event Broker"
[dependencies]
# Utility
log = "0.4"
rand = "0.9.1"
# RabbitMQ/AMQP client
lapin = "3.0.0"
# Async runtime
async-std = { version = "1.8.0" }
# Serialisation
serde = "1"
rmp-serde = "1.3.0"
# Core
revolt-config = { version = "0.8.8", path = "../config" }

View File

@@ -0,0 +1,180 @@
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use async_std::stream::StreamExt;
use lapin::{
Channel, Connection,
options::BasicAckOptions,
types::{AMQPValue, FieldArray, FieldTable, LongLongInt},
};
use log::info;
use rand::Rng;
use revolt_config::{capture_internal_error, config};
use serde::de::DeserializeOwned;
use crate::event_stream::{create_channel, get_connection};
pub struct Consumer {
#[allow(dead_code)]
conn: Arc<Connection>,
channel: Channel,
tag: String,
topics: HashSet<String>,
topics_changed: bool,
consumer: Option<lapin::Consumer>,
offset: Option<LongLongInt>,
}
impl Consumer {
/// Create a new event stream consumer
pub async fn new() -> Consumer {
let config = config().await;
let conn = get_connection().await;
let channel = create_channel(&conn, config.rabbit.event_stream).await;
Consumer {
conn,
channel,
tag: rand::rng()
.sample_iter::<char, _>(&rand::distr::StandardUniform)
.take(32)
.collect(),
topics: HashSet::new(),
topics_changed: false,
consumer: None,
offset: None,
}
}
/// Update the set of topics
pub fn set_topics(&mut self, topics: HashSet<String>) {
self.topics = topics;
self.topics_changed = true;
}
/// Get the current consumer
pub async fn ensure_consumer(&mut self) {
if self.topics_changed {
info!("Topics changed, disposing the consumer.");
self.dispose_consumer().await;
self.topics_changed = false;
}
if self.consumer.is_none() {
info!("Creating a new consumer, tag={}", self.tag);
let config = config().await;
// Build arguments for consumer
let mut args: FieldTable = Default::default();
// Configure stream filter to select topics we are listening for
{
let mut filter: FieldArray = Default::default();
for topic in &self.topics {
filter.push(AMQPValue::LongString(topic.as_str().into()));
}
args.insert("x-stream-filter".into(), AMQPValue::FieldArray(filter));
}
// Set stream offset if applicable
if let Some(offset) = self.offset {
args.insert("x-stream-offset".into(), AMQPValue::LongLongInt(offset));
}
// Create the consumer
self.consumer = Some(
self.channel
.basic_consume(
&config.rabbit.event_stream.queue,
&self.tag,
Default::default(),
args,
)
.await
.unwrap(),
);
}
}
/// Close the active consumer if one exists
pub async fn dispose_consumer(&mut self) {
if let Some(consumer) = self.consumer.as_ref() {
if consumer.state().is_active() {
if let Err(err) = self
.channel
.basic_cancel(&self.tag, Default::default())
.await
{
eprintln!("Failed to close consumer! {:?}", err);
}
// is this necessary?
// else {
// Read the consumer to the end
// while let Some(delivery) = consumer.next().await {
// let delivery = delivery.expect("error in consumer");
// delivery.ack(BasicAckOptions::default()).await.expect("ack");
// }
// }
}
self.consumer = None;
}
}
/// Close the active channel
pub async fn dispose_channel(&mut self) {
// Close the channel -- don't do this actually
capture_internal_error!(self.channel.close(0, "closing channel").await);
}
/// Get the next item
pub async fn next<T: DeserializeOwned>(&mut self) -> Option<T> {
self.ensure_consumer().await;
let consumer = self.consumer.as_mut().unwrap();
while let Some(Ok(delivery)) = consumer.next().await {
// Acknowledgement is required
delivery.ack(BasicAckOptions::default()).await.expect("ack");
// Parse the delivery headers
let headers: HashMap<String, AMQPValue> = delivery
.properties
.headers()
.as_ref()
.map(|table| {
table
.into_iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
})
.unwrap_or_default();
// Keep track of the current offset
let stream_offset = headers
.get("x-stream-offset")
.expect("`x-stream-offset` not present in message!");
self.offset = Some(stream_offset.as_long_long_int().unwrap() + 1);
// Client-side topic filtering (broker uses Bloom filter so may have false-positives)
let filter_value = headers
.get("x-stream-filter-value")
.expect("`x-stream-filter-value` not present in message!")
.as_long_string()
.expect("`string`")
.to_string();
if self.topics.contains(&filter_value) {
// Deserialise the data
return Some(rmp_serde::from_slice(&delivery.data).expect("`data`"));
}
}
None
}
}

View File

@@ -0,0 +1,7 @@
mod consumer;
mod pool;
mod publish;
pub use consumer::Consumer;
pub use pool::{create_channel, get_connection};
pub use publish::publish_event;

View File

@@ -0,0 +1,99 @@
use async_std::sync::Mutex;
use lapin::{
Connection,
options::QueueDeclareOptions,
types::{AMQPValue, FieldTable},
};
use log::{debug, warn};
use revolt_config::{RabbitEventStream, config};
use std::sync::Arc;
use crate::create_client;
/// Get a handle to the event stream
pub async fn get_connection() -> Arc<Connection> {
let config = config().await;
static CONNECTIONS: Mutex<Vec<Arc<Connection>>> = Mutex::new(Vec::new());
let mut connections = CONNECTIONS.lock().await;
connections.retain(|item| {
if item.status().connected() {
true
} else {
warn!(
"Dropping connection with status {:?}",
item.status().state()
);
false
}
});
debug!(
"Connections: {}, Clients: {:?}",
connections.len(),
connections
.iter()
.map(Arc::strong_count)
.collect::<Vec<usize>>()
);
for conn in connections.iter() {
if Arc::strong_count(conn) < config.rabbit.event_stream.channels_per_conn {
return conn.clone();
}
}
let conn = Arc::new(create_client().await);
connections.push(conn.clone());
conn
}
/// Create a channel
pub async fn create_channel(
conn: &lapin::Connection,
event_stream: RabbitEventStream,
) -> lapin::Channel {
let channel = conn.create_channel().await.unwrap();
let mut args: FieldTable = Default::default();
args.insert(
// set queue type to stream
"x-queue-type".into(),
AMQPValue::LongString("stream".into()),
);
args.insert(
// max. size of the stream
"x-max-length-bytes".into(),
AMQPValue::LongLongInt(event_stream.stream_max_length_bytes),
);
args.insert(
// size of the Bloom filter
"x-stream-filter-size-bytes".into(),
AMQPValue::LongLongInt(event_stream.filter_size_bytes),
);
channel
.queue_declare(
&event_stream.queue,
QueueDeclareOptions {
durable: true,
..Default::default()
},
args,
)
.await
.unwrap();
channel
.basic_qos(event_stream.qos_prefetch, Default::default())
.await
.unwrap();
channel
}

View File

@@ -0,0 +1,37 @@
use lapin::{
Error,
protocol::basic::AMQPProperties,
publisher_confirm::PublisherConfirm,
types::{AMQPValue, FieldTable},
};
use revolt_config::config;
use serde::Serialize;
use crate::event_stream::{create_channel, get_connection};
/// Publish an event to the message broker
pub async fn publish_event<T: Serialize>(
channel: &str,
data: &T,
) -> Result<PublisherConfirm, Error> {
let config = config().await;
let mut headers: FieldTable = Default::default();
headers.insert(
"x-stream-filter-value".into(),
AMQPValue::LongString(channel.into()),
);
let conn = get_connection().await;
create_channel(&conn, config.rabbit.event_stream.clone())
.await
.basic_publish(
&config.rabbit.event_stream.exchange,
&config.rabbit.event_stream.queue,
Default::default(),
&rmp_serde::to_vec_named(data).unwrap(),
AMQPProperties::default().with_headers(headers),
)
.await
}

View File

@@ -0,0 +1,18 @@
use revolt_config::config;
pub mod event_stream;
/// Create a lapin client
pub async fn create_client() -> lapin::Connection {
let config = config().await;
lapin::Connection::connect(
&format!(
"amqp://{}:{}@{}:{}/%2f",
config.rabbit.username, config.rabbit.password, config.rabbit.host, config.rabbit.port
),
Default::default(),
)
.await
.unwrap()
}

View File

@@ -1,22 +0,0 @@
[package]
name = "revolt-coalesced"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
description = "Revolt Backend: Coalescion service"
[features]
tokio = ["dep:tokio"]
queue = ["dep:indexmap"]
cache = ["dep:lru"]
default = ["tokio"]
[dependencies]
tokio = { version = "1.47.0", features = ["sync"], optional = true }
indexmap = { version = "*", optional = true }
lru = { version = "*", optional = true }
[dev-dependencies]
tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros", "time"] }

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,24 +0,0 @@
#[derive(Clone, PartialEq, Eq, Debug)]
/// Config values for [`CoalescionService`].
pub struct CoalescionServiceConfig {
/// How many tasks are running at once
pub max_concurrent: Option<usize>,
/// Whether to queue tasks once `max_concurrent` is reached
#[cfg(feature = "queue")]
pub queue_requests: bool,
/// Max amount of tasks in the buffer queue
#[cfg(feature = "queue")]
pub max_queue: Option<usize>,
}
impl Default for CoalescionServiceConfig {
fn default() -> Self {
Self {
max_concurrent: Some(100),
#[cfg(feature = "queue")]
queue_requests: true,
#[cfg(feature = "queue")]
max_queue: Some(100)
}
}
}

View File

@@ -1,27 +0,0 @@
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
/// Coalescion service error.
pub enum Error {
/// Failed to receive the actions return from the channel for unknown reason
RecvError,
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
MaxConcurrent,
/// Reached the `max_queue` amount of actions in the queue
MaxQueue,
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
DowncastError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::RecvError => write!(f, "Unable to receive data from the channel"),
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
}
}
}
impl std::error::Error for Error {}

View File

@@ -1,39 +0,0 @@
//! # Coalesced
//!
//! Coalescion service to group, caching and queue duplicate actions.
//! useful for deduplicating web requests, database lookups and other similar resource
//! intensive or rate-limited actions.
//!
//! ## Features
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
//!
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
//! use in your codebase.
//!
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
//!
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
//! and a type is wrong the library will return an error, values returned from the service are also
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
//!
//! ## Example:
//! ```rs
//! use revolt_coalesced::CoalescionService;
//!
//! let service = CoalescionService::new();
//!
//! let user_id = "my_user_id";
//! let user = service.execute(user_id, || async move {
//! database.fetch_user(user_id).await.unwrap()
//! }).await;
//! ```
mod config;
mod error;
mod service;
pub use config::CoalescionServiceConfig;
pub use error::Error;
pub use service::CoalescionService;

View File

@@ -1,208 +0,0 @@
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
use tokio::sync::{
watch::{channel as watch_channel, Receiver},
RwLock,
};
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "queue")]
use indexmap::IndexMap;
use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
config: Arc<CoalescionServiceConfig>,
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "queue")]
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "cache")]
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
}
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
pub fn new() -> Self {
Default::default()
}
pub fn from_config(config: CoalescionServiceConfig) -> Self {
Self {
config: Arc::new(config),
watchers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "queue")]
queue: Arc::new(RwLock::new(IndexMap::new())),
#[cfg(feature = "cache")]
cache: None,
}
}
#[cfg(feature = "cache")]
pub fn from_cache(
config: CoalescionServiceConfig,
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
) -> Self {
Self {
cache: Some(Arc::new(Mutex::new(cache))),
..Self::from_config(config)
}
}
async fn wait_for<Value: Any + Send + Sync>(
&self,
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
) -> Result<Arc<Value>, Error> {
receiver
.wait_for(|v| v.is_some())
.await
.map_err(|_| Error::RecvError)
.and_then(|r| r.clone().unwrap())
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
}
async fn insert_and_execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
let (send, recv) = watch_channel(None);
self.watchers.write().await.insert(id.clone(), recv);
let value = Ok(Arc::new(func().await));
send.send_modify(|opt| {
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
});
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Ok(value) = &value {
cache.lock().await.push(id.clone(), value.clone());
}
};
self.watchers.write().await.remove(&id);
value
}
/// Coalesces an function, the actual function may not run if one with the same id is already running,
/// queued to be ran, or cached, the id should be globally unique for this specific action.
pub async fn execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Some(value) = cache.lock().await.get(&id) {
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
}
};
let (receiver, length) = {
let watchers = self.watchers.read().await;
let length = watchers.len();
(watchers.get(&id).cloned(), length)
};
if let Some(receiver) = receiver {
self.wait_for(receiver).await
} else {
match self.config.max_concurrent {
Some(max_concurrent) if length >= max_concurrent => {
#[cfg(feature = "queue")]
if self.config.queue_requests {
let (receiver, length) = {
let queue = self.queue.read().await;
(queue.get(&id).cloned(), queue.len())
};
if let Some(receiver) = receiver {
return self.wait_for(receiver).await;
} else {
if self
.config
.max_queue
.is_some_and(|max_queue| max_queue >= length)
{
return Err(Error::MaxQueue);
};
let (send, recv) = watch_channel(None);
self.queue.write().await.insert(id.clone(), recv);
loop {
let length = self.watchers.read().await.len();
if length < max_concurrent {
let first_key = {
let queue = self.queue.read().await;
queue.first().map(|v| v.0).cloned()
};
if first_key == Some(id.clone()) {
self.queue.write().await.shift_remove(&id);
let response = self.insert_and_execute(id, func).await;
send.send_modify(|opt| {
opt.replace(
response
.clone()
.map(|v| v as Arc<dyn Any + Send + Sync>),
);
});
return response;
}
}
}
}
} else {
Err(Error::MaxConcurrent)
}
#[cfg(not(feature = "queue"))]
Err(Error::MaxConcurrent)
}
_ => self.insert_and_execute(id, func).await,
}
}
}
/// Fetches the amount of currently running tasks
pub async fn current_task_count(&self) -> usize {
self.watchers.read().await.len()
}
#[cfg(feature = "queue")]
/// Fetches the current length of the queue
pub async fn current_queue_len(&self) -> usize {
self.queue.read().await.len()
}
}
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
fn default() -> Self {
Self::from_config(CoalescionServiceConfig::default())
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -11,9 +11,8 @@ description = "Revolt Backend: Configuration"
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
sentry = ["dep:sentry"]
test = ["async-std"]
default = ["test", "sentry"]
default = ["test", "anyhow"]
[dependencies]
# Utility
@@ -33,8 +32,8 @@ log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = { version = "0.31.5", optional = true }
sentry = "0.31.5"
sentry-anyhow = { version = "0.38.1", optional = true }
# Core
revolt-result = { version = "0.8.9", path = "../result", optional = true }
revolt-result = { version = "0.8.8", path = "../result", optional = true }

View File

@@ -28,6 +28,20 @@ port = 5672
username = "rabbituser"
password = "rabbitpass"
[rabbit.event_stream]
# Configuration for event brokerage
# Using default/direct exchange
exchange = ""
queue = "revolt.events"
# Number of channels that can be opened per single TCP connection
channels_per_conn = 128
# Maximum size of the stream
stream_max_length_bytes = 5_000_000_000
# Size of the Bloom filter
filter_size_bytes = 26
# Number of messages to prefetch
qos_prefetch = 100
[api]
[api.registration]
@@ -56,10 +70,6 @@ voso_legacy_token = ""
trust_cloudflare = false
# easypwned endpoint
easypwned = ""
# Secret used to encode and decode tokens
token_secret = ""
# Tenor API Key
tenor_key = ""
[api.security.captcha]
# hCaptcha configuration
@@ -281,4 +291,3 @@ files = ""
proxy = ""
pushd = ""
crond = ""
gifbox = ""

View File

@@ -6,12 +6,10 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "anyhow")]
pub use sentry_anyhow::capture_anyhow;
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
@@ -26,7 +24,7 @@ macro_rules! report_error {
};
}
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
@@ -37,7 +35,7 @@ macro_rules! capture_internal_error {
};
}
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
@@ -110,12 +108,24 @@ pub struct Database {
pub redis: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct RabbitEventStream {
pub exchange: String,
pub queue: String,
pub channels_per_conn: usize,
pub stream_max_length_bytes: i64,
pub filter_size_bytes: i64,
pub qos_prefetch: u16,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Rabbit {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub event_stream: RabbitEventStream,
}
#[derive(Deserialize, Debug, Clone)]
@@ -190,8 +200,6 @@ pub struct ApiSecurity {
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
pub easypwned: String,
pub token_secret: String,
pub tenor_key: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -367,7 +375,6 @@ pub struct Sentry {
pub proxy: String,
pub pushd: String,
pub crond: String,
pub gifbox: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -424,7 +431,6 @@ pub async fn config() -> Settings {
}
/// Configure logging and common Rust variables
#[cfg(feature = "sentry")]
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -450,7 +456,6 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
}
}
#[cfg(feature = "sentry")]
#[macro_export]
macro_rules! configure {
($application: ident) => {

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
[features]
# Databases
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
mongodb = ["dep:mongodb", "bson"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
@@ -24,19 +24,20 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.9", path = "../config", features = [
revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.9", path = "../result" }
revolt-models = { version = "0.8.9", path = "../models", features = [
revolt-result = { version = "0.8.8", path = "../result" }
revolt-models = { version = "0.8.8", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.9", path = "../presence" }
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
revolt-presence = { version = "0.8.8", path = "../presence" }
revolt-permissions = { version = "0.8.8", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.8.9", path = "../parser" }
revolt-parser = { version = "0.8.8", path = "../parser" }
revolt-broker = { version = "0.8.8", path = "../broker" }
# Utility
log = "0.4"
@@ -53,11 +54,10 @@ linkify = { optional = true, version = "0.8.1" }
url-escape = { optional = true, version = "0.1.1" }
validator = { version = "0.16", features = ["derive"] }
isahc = { optional = true, version = "1.7", features = ["json"] }
jsonwebtoken = "9.3.1"
chrono = "0.4"
# Serialisation
serde_json = "1"
rmp-serde = "1.0.0"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
@@ -93,8 +93,14 @@ rocket = { version = "0.5.1", default-features = false, features = [
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.15" }
authifier = { version = "1.0.15", features = ["rocket_impl"] }
# RabbitMQ
amqprs = { version = "1.7.0" }
lapin = { version = "3.0.0" }

View File

@@ -1,4 +1,3 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -14,7 +13,6 @@ use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
#[cfg(feature = "mongodb")]
pub use self::mongodb::*;
pub use self::reference::*;
@@ -27,10 +25,8 @@ pub enum DatabaseInfo {
/// Use the mock database
Reference,
/// Connect to MongoDB
#[cfg(feature = "mongodb")]
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
#[cfg(feature = "mongodb")]
MongoDbFromClient(::mongodb::Client, String),
}
@@ -40,7 +36,6 @@ pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
#[cfg(feature = "mongodb")]
MongoDb(MongoDb),
}
@@ -50,7 +45,7 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
match self {
Ok(match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
@@ -58,20 +53,16 @@ impl DatabaseInfo {
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await
.await?
} else if !config.database.mongodb.is_empty() {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(),
}
.connect()
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
.await?
} else {
DatabaseInfo::Reference.connect().await
DatabaseInfo::Reference.connect().await?
}
}
DatabaseInfo::Test(database_name) => {
@@ -79,36 +70,30 @@ impl DatabaseInfo {
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await,
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"MONGODB" => {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
.await?
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
}
}
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
#[cfg(feature = "mongodb")]
DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::MongoDb { uri, database_name } => {
let client = ::mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Ok(Database::MongoDb(MongoDb(client, database_name)))
Database::MongoDb(MongoDb(client, database_name))
}
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Ok(Database::MongoDb(MongoDb(client, database_name)))
Database::MongoDb(MongoDb(client, database_name))
}
}
})
}
}
@@ -234,16 +219,12 @@ impl Database {
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
#[cfg(feature = "mongodb")]
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
#[cfg(feature = "tasks")]
event_channel: Some(crate::tasks::authifier_relay::sender()),
#[cfg(not(feature = "tasks"))]
event_channel: None,
}
}
}

View File

@@ -10,6 +10,7 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
pub struct MongoDb(pub ::mongodb::Client, pub String);
);

View File

@@ -5,14 +5,13 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook, AuthorizedBotId, AuthorizedBot,
User, UserSettings, Webhook,
};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
pub authorized_bots: Arc<Mutex<HashMap<AuthorizedBotId, AuthorizedBot>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,

View File

@@ -1,4 +1,5 @@
use authifier::AuthifierEvent;
use revolt_broker::event_stream;
use revolt_result::Error;
use serde::{Deserialize, Serialize};
@@ -20,31 +21,16 @@ pub enum Ping {
}
/// Fields provided in Ready payload
#[derive(PartialEq, Debug, Clone, Deserialize)]
pub struct ReadyPayloadFields {
pub users: bool,
pub servers: bool,
pub channels: bool,
pub members: bool,
pub emojis: bool,
pub user_settings: Vec<String>,
pub channel_unreads: bool,
pub policy_changes: bool,
}
#[derive(PartialEq)]
pub enum ReadyPayloadFields {
Users,
Servers,
Channels,
Members,
Emoji,
impl Default for ReadyPayloadFields {
fn default() -> Self {
Self {
users: true,
servers: true,
channels: true,
members: true,
emojis: true,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: true,
}
}
UserSettings(Vec<String>),
ChannelUnreads,
}
/// Protocol Events
@@ -78,8 +64,7 @@ pub enum EventV1 {
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
#[serde(skip_serializing_if = "Option::is_none")]
policy_changes: Option<Vec<PolicyChange>>,
policy_changes: Vec<PolicyChange>,
},
/// Ping response
@@ -160,13 +145,7 @@ pub enum EventV1 {
},
/// User joins server
ServerMemberJoin {
id: String,
// Deprecated: use member.id.user
#[deprecated = "Use member.id.user instead"]
user: String,
member: Member,
},
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave {
@@ -275,14 +254,16 @@ pub enum EventV1 {
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
let result = event_stream::publish_event(&channel, &self).await;
#[cfg(not(debug_assertions))]
result.ok();
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
result.unwrap();
}
/// Publish user event

View File

@@ -1,8 +1,8 @@
use serde::{Serialize, Deserialize};
use serde::Deserialize;
use super::client::Ping;
#[derive(Serialize, Deserialize, Debug)]
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ClientMessage {
Authenticate { token: String },

View File

@@ -25,9 +25,6 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
@@ -106,7 +103,6 @@ pub mod util;
pub use models::*;
pub mod events;
#[cfg(feature = "tasks")]
pub mod tasks;
mod amqp;

View File

@@ -1,4 +1,3 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,5 +0,0 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -1,30 +0,0 @@
use iso8601_timestamp::Timestamp;
use crate::OAuth2Scope;
auto_derived! (
/// Unique id of the user and bot
#[derive(Hash)]
pub struct AuthorizedBotId {
/// User id
pub user: String,
/// Bot Id
pub bot: String,
}
pub struct AuthorizedBot {
/// Unique Id
#[serde(rename = "_id")]
pub id: AuthorizedBotId,
/// When the authorized oauth2 bot connection was created at
pub created_at: Timestamp,
/// If and when the authorized oauth2 bot connection was revoked at
pub deauthorized_at: Option<Timestamp>,
/// Scopes the bot has access to
pub scope: Vec<OAuth2Scope>,
}
);

View File

@@ -1,27 +0,0 @@
use revolt_result::Result;
use crate::{AuthorizedBot, AuthorizedBotId};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAuthorizedBots: Sync + Send {
/// Insert emoji into database.
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()>;
/// Fetch an authorized bot by its id
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot>;
/// Fetch a users authorized bot by its id
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>>;
/// Deletes an authorized bot
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()>;
/// Deauthorizes an authorized bot
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot>;
// Fetches all authorized bots which have been deauthorized
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>>;
}

View File

@@ -1,71 +0,0 @@
use bson::to_bson;
use revolt_result::Result;
use iso8601_timestamp::Timestamp;
use crate::{MongoDb, AuthorizedBot, AuthorizedBotId};
use super::AbstractAuthorizedBots;
static COL: &str = "authorized_bots";
#[async_trait]
impl AbstractAuthorizedBots for MongoDb {
/// Insert an authorized bot into database.
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()> {
query!(self, insert_one, COL, &authorized_bot).map(|_| ())
}
/// Fetch an authorized bot by its id
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
query!(
self,
find_one,
COL,
doc! {
"_id.user": &id.user,
"_id.bot": &id.bot
}
)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a users authorized bot by its id
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>> {
query!(self, find, COL, doc! { "_id.user": &user_id })
}
/// Deletes an authorized bot
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()> {
query!(self, delete_one, COL, doc! { "_id.user": &id.user, "_id.bot": &id.bot }).map(|_| ())
}
/// Deauthorizes an authorized bot
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
self.col::<AuthorizedBot>(COL)
.find_one_and_update(
doc! {
"_id.user": &id.user,
"_id.bot": &id.bot
},
doc! {
"$set": {
"deauthorized_at": to_bson(&Timestamp::now_utc()).unwrap()
}
}
)
.await
.map_err(|_| create_database_error!("find_one_and_update", COL))
.and_then(|opt| opt.ok_or_else(|| create_database_error!("find_one_and_update", COL)))
}
// Fetches all authorized bots which have been deauthorized
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>> {
query!(
self,
find,
COL,
doc! {
"deauthorized_at": { "$exists": true }
}
)
}
}

View File

@@ -1,76 +0,0 @@
use revolt_result::Result;
use iso8601_timestamp::Timestamp;
use crate::{ReferenceDb, AuthorizedBot, AuthorizedBotId};
use super::AbstractAuthorizedBots;
#[async_trait]
impl AbstractAuthorizedBots for ReferenceDb {
/// Insert an authorized bot into database.
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()> {
let mut authorized_bots = self.authorized_bots.lock().await;
if authorized_bots.contains_key(&authorized_bot.id) {
Err(create_database_error!("insert", "authorized_bots"))
} else {
authorized_bots.insert(authorized_bot.id.clone(), authorized_bot.clone());
Ok(())
}
}
/// Fetch an authorized bot by its id
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
let authorized_bots = self.authorized_bots.lock().await;
authorized_bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
}
/// Fetch a users authorized bot by its id
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>> {
let authorized_bots = self.authorized_bots.lock().await;
Ok(authorized_bots
.values()
.filter(|authorized_bot| authorized_bot.id.user == user_id)
.cloned()
.collect()
)
}
/// Deletes an authorized bot
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()> {
let mut authorized_bots = self.authorized_bots.lock().await;
if authorized_bots.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Deauthorizes an authorized bot
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
let mut authorized_bots = self.authorized_bots.lock().await;
if let Some(authorized_bot) = authorized_bots.get_mut(id) {
authorized_bot.deauthorized_at = Some(Timestamp::now_utc());
Ok(authorized_bot.clone())
} else {
Err(create_error!(NotFound))
}
}
// Fetches all authorized bots which have been deauthorized
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>> {
let authorized_bots = self.authorized_bots.lock().await;
Ok(authorized_bots
.values()
.filter(|authorized_bot| authorized_bot.deauthorized_at.is_some())
.cloned()
.collect()
)
}
}

View File

@@ -1,6 +1,5 @@
use revolt_result::Result;
use ulid::Ulid;
use std::collections::HashMap;
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
@@ -36,10 +35,6 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "String::is_empty", default)]
pub privacy_policy_url: String,
/// Oauth2 bot settings
#[serde(skip_serializing_if = "Option::is_none", default)]
pub oauth2: Option<BotOauth2>,
/// Enum of bot flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
@@ -47,52 +42,11 @@ auto_derived_partial!(
"PartialBot"
);
auto_derived!(
#[derive(Copy, Hash)]
pub enum OAuth2Scope {
#[serde(rename = "read:identify")]
ReadIdentify,
#[serde(rename = "read:servers")]
ReadServers,
#[serde(rename = "write:files")]
WriteFiles,
#[serde(rename = "events")]
Events,
#[serde(rename = "full")]
Full,
}
pub struct OAuth2ScopeReasoning {
pub allow: String,
pub deny: String
}
);
auto_derived_partial!(
pub struct BotOauth2 {
/// Whether the oauth2 client is public and should not receive a secret key
#[serde(default)]
pub public: bool,
/// Secret key used for authorisation, not provided if the client is public
#[serde(default)]
pub secret: Option<String>,
/// Allowed redirects for the authorisation
#[serde(default)]
pub redirects: Vec<String>,
/// Mapping of allowed scopes and the reasonings
#[serde(default)]
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
},
"PartialBotOauth2"
);
auto_derived!(
/// Optional fields on bot object
pub enum FieldsBot {
Token,
InteractionsURL,
Oauth2,
Oauth2Secret,
}
);
@@ -109,24 +63,11 @@ impl Default for Bot {
interactions_url: Default::default(),
terms_of_service_url: Default::default(),
privacy_policy_url: Default::default(),
oauth2: Default::default(),
flags: Default::default(),
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for BotOauth2 {
fn default() -> Self {
Self {
public: false,
secret: Some(nanoid::nanoid!(64)),
redirects: Vec::new(),
allowed_scopes: HashMap::new(),
}
}
}
#[allow(clippy::disallowed_methods)]
impl Bot {
/// Create a new bot
@@ -183,14 +124,6 @@ impl Bot {
FieldsBot::Token => self.token = nanoid::nanoid!(64),
FieldsBot::InteractionsURL => {
self.interactions_url = String::new();
},
FieldsBot::Oauth2 => self.oauth2 = None,
FieldsBot::Oauth2Secret => {
if let Some(oauth2) = &mut self.oauth2 {
if !oauth2.public {
oauth2.secret = Some(nanoid::nanoid!(64))
}
}
}
}
}

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -87,8 +87,6 @@ impl IntoDocumentPath for FieldsBot {
match self {
FieldsBot::InteractionsURL => Some("interactions_url"),
FieldsBot::Token => None,
FieldsBot::Oauth2 => Some("oauth2"),
FieldsBot::Oauth2Secret => None,
}
}
}

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::Invite;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::ChannelUnread;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -8,13 +8,10 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{
events::client::EventV1, Database, File, PartialServer,
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User, AMQP,
};
#[cfg(feature = "mongodb")]
use crate::IntoDocumentPath;
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
@@ -649,11 +646,10 @@ impl Channel {
.private(user.to_string())
.await;
#[cfg(feature = "tasks")]
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
crate::tasks::ack::AckEvent::AckMessage {
AckEvent::AckMessage {
id: message.to_string(),
},
)
@@ -770,7 +766,6 @@ impl Channel {
}
}
#[cfg(feature = "mongodb")]
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {

View File

@@ -1,7 +1,5 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::Emoji;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::FileHash;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -4,7 +4,6 @@ use crate::File;
use super::FileUsedFor;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -14,6 +14,7 @@ use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
@@ -21,9 +22,6 @@ use crate::{
Channel, Database, Emoji, File, User, AMQP,
};
#[cfg(feature = "tasks")]
use crate::tasks::{self, ack::AckEvent};
auto_derived_partial!(
/// Message
pub struct Message {
@@ -440,8 +438,7 @@ impl Message {
}
// Verify replies are valid.
let mut replies = Vec::new();
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies {
@@ -449,8 +446,6 @@ impl Message {
}));
}
replies.reserve(entries.len());
for ReplyIntent {
id,
mention,
@@ -464,12 +459,7 @@ impl Message {
user_mentions.insert(message.author.to_owned());
}
// This is O(n^2), but this is faster than a HashSet
// when n < 20; as long as the message_replies limit
// is reasonable, this will be fast.
if !replies.contains(&message.id) {
replies.push(message.id);
}
replies.insert(message.id);
}
// If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply.
@@ -542,7 +532,9 @@ impl Message {
}
if !replies.is_empty() {
message.replies.replace(replies);
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Calculate final message flags
@@ -624,11 +616,9 @@ impl Message {
.await;
// Update last_message_id
#[cfg(feature = "tasks")]
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
#[cfg(feature = "tasks")]
if !mentions_elsewhere {
if let Some(mentions) = &self.mentions {
tasks::ack::queue_message(
@@ -647,7 +637,6 @@ impl Message {
}
// Generate embeds
#[cfg(feature = "tasks")]
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
@@ -684,12 +673,10 @@ impl Message {
)
.await?;
if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
#[cfg(feature = "tasks")]
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,5 +1,4 @@
mod admin_migrations;
mod authorized_bots;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -20,7 +19,6 @@ mod user_settings;
mod users;
pub use admin_migrations::*;
pub use authorized_bots::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -40,15 +38,11 @@ pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, ReferenceDb};
#[cfg(feature = "mongodb")]
use crate::MongoDb;
use crate::{Database, MongoDb, ReferenceDb};
pub trait AbstractDatabase:
Sync
+ Send
+ authorized_bots::AbstractAuthorizedBots
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ channels::AbstractChannels
@@ -72,8 +66,6 @@ pub trait AbstractDatabase:
}
impl AbstractDatabase for ReferenceDb {}
#[cfg(feature = "mongodb")]
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
@@ -82,7 +74,6 @@ impl std::ops::Deref for Database {
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
#[cfg(feature = "mongodb")]
Database::MongoDb(mongo) => mongo,
}
}

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::PolicyChange;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,8 +1,6 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::Report;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::Snapshot;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -30,9 +30,6 @@ auto_derived_partial!(
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
// This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp>
},
"PartialMember"
);
@@ -53,7 +50,6 @@ auto_derived!(
Avatar,
Roles,
Timeout,
JoinedAt,
}
/// Member removal intention
@@ -94,7 +90,7 @@ impl Member {
return Err(create_error!(AlreadyInServer));
}
let mut member = Member {
let member = Member {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user.id.to_string(),
@@ -102,9 +98,7 @@ impl Member {
..Default::default()
};
if let Some(updated) = db.insert_or_merge_member(&member).await? {
member = updated;
}
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
@@ -130,7 +124,6 @@ impl Member {
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
member: member.clone().into(),
}
.p(server.id.clone())
.await;
@@ -193,7 +186,6 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
FieldsMember::JoinedAt => (),
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
@@ -232,7 +224,7 @@ impl Member {
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.soft_delete_member(&self.id).await?;
db.delete_member(&self.id).await?;
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
@@ -268,74 +260,3 @@ impl Member {
Ok(())
}
}
#[cfg(test)]
mod tests {
use iso8601_timestamp::{Duration, Timestamp};
use revolt_models::v0::DataCreateServer;
use crate::{Member, PartialMember, RemovalIntention, Server, User};
#[async_std::test]
async fn muted_member_rejoin() {
database_test!(|db| async move {
match db {
crate::Database::Reference(_) => return,
crate::Database::MongoDb(_) => (),
}
let owner = User::create(&db, "Server Owner".to_string(), None, None)
.await
.unwrap();
let kickable_user = User::create(&db, "Member".to_string(), None, None)
.await
.unwrap();
let server = Server::create(
&db,
DataCreateServer {
name: "Server".to_string(),
description: None,
nsfw: None,
},
&owner,
false,
)
.await
.unwrap()
.0;
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
kickable_member
.update(
&db,
PartialMember {
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
..Default::default()
},
vec![],
)
.await
.unwrap();
assert!(kickable_member.in_timeout());
kickable_member
.remove(&db, &server, RemovalIntention::Kick, false)
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
assert!(kickable_member.in_timeout())
});
}
}

View File

@@ -1,20 +1,16 @@
#[cfg(feature = "mongodb")]
use ::mongodb::{ClientSession, SessionCursor};
use ::mongodb::SessionCursor;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ClientSession,
session: ::mongodb::ClientSession,
cursor: Option<SessionCursor<Member>>,
},
@@ -26,7 +22,7 @@ pub enum ChunkedServerMembersGenerator {
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
@@ -73,13 +69,13 @@ impl ChunkedServerMembersGenerator {
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
async fn insert_member(&self, member: &Member) -> Result<()>;
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Fetch all members in a server
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
@@ -100,10 +96,10 @@ pub trait AbstractServerMembers: Sync + Send {
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
/// Fetch multiple members by their ids
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
@@ -119,14 +115,6 @@ pub trait AbstractServerMembers: Sync + Send {
remove: Vec<FieldsMember>,
) -> Result<()>;
/// Marks a user as no longer a member of a server, while retaining the database value.
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Forcibly delete a server member by their id.
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Fetch all members who have been marked for deletion.
async fn remove_dangling_members(&self) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}

View File

@@ -1,6 +1,4 @@
use bson::Document;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern;
use revolt_result::Result;
@@ -13,42 +11,9 @@ static COL: &str = "server_members";
#[async_trait]
impl AbstractServerMembers for MongoDb {
/// Insert a new server member (or use the existing member if one is found)
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
let existing: Result<Option<Document>> = query!(
self,
find_one,
COL,
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
"pending_deletion_at": {"$exists": true}
}
);
// Update the existing record if it exist, otherwise make a new record
if existing.is_ok_and(|x| x.is_some()) {
self.col::<Member>(COL)
.find_one_and_update(
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
},
doc! {
"$set": {
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
},
"$unset": {
"pending_deletion_at": ""
}
},
)
.return_document(mongodb::options::ReturnDocument::After)
.await
.map_err(|_| create_database_error!("update_one", COL))
} else {
query!(self, insert_one, COL, &member).map(|_| ())?;
Ok(None)
}
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
query!(self, insert_one, COL, &member).map(|_| ())
}
/// Fetch a server member by their id
@@ -59,20 +24,18 @@ impl AbstractServerMembers for MongoDb {
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all members in a server
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
"_id.server": server_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -176,12 +139,11 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
"_id.user": user_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -197,12 +159,11 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch multiple members by their ids
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"pending_deletion_at": {"$exists": false},
"_id.user": {
"$in": ids
}
@@ -224,8 +185,7 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
"_id.server": server_id
})
.await
.map(|c| c as usize)
@@ -236,8 +196,7 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
"_id.user": user_id
})
.await
.map(|c| c as usize)
@@ -266,42 +225,8 @@ impl AbstractServerMembers for MongoDb {
.map(|_| ())
}
/// Marks a member for deletion.
/// This will remove the record if the user has no pending actions (eg. timeout),
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let member = self.fetch_member(&id.server, &id.user).await;
if let Ok(member) = member {
if member.in_timeout() {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.server": &id.server,
"_id.user": &id.user,
},
doc! {
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
"$unset": {
"joined_at": "",
"avatar": "",
"nickname": "",
"roles": ""
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
} else {
self.force_delete_member(id).await
}
} else {
Err(create_database_error!("fetch_member", COL))
}
}
/// Delete a server member by their id
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
@@ -313,25 +238,11 @@ impl AbstractServerMembers for MongoDb {
)
.map(|_| ())
}
async fn remove_dangling_members(&self) -> Result<()> {
let now = Timestamp::now_utc();
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
self.col::<Document>(COL)
.delete_many(doc! {
"pending_deletion_at": {"$lt": date}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("count_documents", COL))
}
}
impl IntoDocumentPath for FieldsMember {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsMember::JoinedAt => "joined_at",
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",

View File

@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
/// Insert a new server member into the database
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
async fn insert_member(&self, member: &Member) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.contains_key(&member.id) {
Err(create_database_error!("insert", "member"))
} else {
server_members.insert(member.id.clone(), member.clone());
Ok(None)
Ok(())
}
}
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all members in a server
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch multiple members by their ids
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(ids
.iter()
@@ -169,26 +169,8 @@ impl AbstractServerMembers for ReferenceDb {
}
}
/// Soft delete a member
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
let member = server_members.get_mut(id);
if let Some(member) = member {
if member.in_timeout() {
panic!("Soft deletion is not implemented.")
} else if server_members.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server member by their id
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.remove(id).is_some() {
Ok(())
@@ -196,8 +178,4 @@ impl AbstractServerMembers for ReferenceDb {
Err(create_error!(NotFound))
}
}
async fn remove_dangling_members(&self) -> Result<()> {
todo!()
}
}

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,7 +2,6 @@ use revolt_result::Result;
use crate::UserSettings;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,22 +1,14 @@
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
use axum::{extract::FromRequestParts, http::request::Parts};
use revolt_config::config;
use revolt_models::v0;
use revolt_result::{create_error, Error, Result};
use crate::{util::oauth2, Database, OAuth2Scope, User};
use crate::{Database, User};
#[async_trait::async_trait]
impl<S> FromRequestParts<S> for User
where
Database: FromRef<S>,
S: Send + Sync
{
impl FromRequestParts<Database> for User {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
let db = Database::from_ref(state);
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await
@@ -25,24 +17,6 @@ where
{
let session = db.fetch_session_by_token(session_token).await?;
db.fetch_user(&session.user_id).await
} else if let Some(Ok(header_oauth_token)) = parts.headers.get("x-oauth2-token").map(|v| v.to_str()) {
let config = config().await;
let claims = oauth2::decode_token(
&config.api.security.token_secret,
header_oauth_token,
).map_err(|_| create_error!(NotAuthenticated))?;
let required_scope: v0::OAuth2Scope = parts.extensions.get::<OAuth2Scope>()
.copied()
.ok_or_else(|| create_error!(NotAuthenticated))?
.into();
if claims.scopes.contains(&v0::OAuth2Scope::Full) || claims.scopes.contains(&required_scope) {
db.fetch_user(&claims.sub).await
} else {
Err(create_error!(MissingScope { scope: required_scope.to_string() }))
}
} else {
Err(create_error!(NotAuthenticated))
}

View File

@@ -4,7 +4,6 @@ use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -212,34 +212,16 @@ impl AbstractUsers for MongoDb {
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
// stupid-ass workaround to fix mongo conflicting the same item
let _: Result<()> = query!(
self,
update_one_by_id,
COL,
id,
PartialUser {
..Default::default()
},
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ());
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
} else {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Set relationship with another user

View File

@@ -1,10 +1,7 @@
use authifier::models::Session;
use revolt_config::config;
use revolt_models::v0;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
use crate::util::oauth2;
use crate::{Database, User};
#[rocket::async_trait]
@@ -22,38 +19,12 @@ impl<'r> FromRequest<'r> for User {
.next()
.map(|x| x.to_string());
let header_oauth_token = request
.headers()
.get("x-oauth2-token")
.next()
.map(|x| x.to_string());
if let Some(bot_token) = header_bot_token {
if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
if let Ok(user) = db.fetch_user(&bot.id).await {
return Some(user);
}
}
} else if let Some(header_oauth_token) = header_oauth_token {
let config = config().await;
let claims = oauth2::decode_token(
&config.api.security.token_secret,
&header_oauth_token,
)
.ok()?;
// access the scope required for the route stored in the request local cache set via `OAuth2Scoped`
let required_scope: v0::OAuth2Scope = request.local_cache(|| None::<crate::OAuth2Scope>)
.as_ref()
.copied()?
.into();
if claims.scopes.contains(&v0::OAuth2Scope::Full) || claims.scopes.contains(&required_scope) {
if let Ok(user) = db.fetch_user(&claims.sub).await {
return Some(user);
}
}
} else if let Outcome::Success(session) = request.guard::<Session>().await {
if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user);

View File

@@ -33,7 +33,6 @@ impl From<crate::Bot> for Bot {
interactions_url: value.interactions_url,
terms_of_service_url: value.terms_of_service_url,
privacy_policy_url: value.privacy_policy_url,
oauth2: value.oauth2.map(|oauth2| oauth2.into()),
flags: value.flags.unwrap_or_default() as u32,
}
}
@@ -44,8 +43,6 @@ impl From<FieldsBot> for crate::FieldsBot {
match value {
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
FieldsBot::Token => crate::FieldsBot::Token,
FieldsBot::Oauth2 => crate::FieldsBot::Oauth2,
FieldsBot::Oauth2Secret => crate::FieldsBot::Oauth2Secret,
}
}
}
@@ -55,78 +52,6 @@ impl From<crate::FieldsBot> for FieldsBot {
match value {
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
crate::FieldsBot::Token => FieldsBot::Token,
crate::FieldsBot::Oauth2 => FieldsBot::Oauth2,
crate::FieldsBot::Oauth2Secret => FieldsBot::Oauth2Secret,
}
}
}
impl From<BotOauth2> for crate::BotOauth2 {
fn from(value: BotOauth2) -> Self {
crate::BotOauth2 {
public: value.public,
secret: value.secret,
redirects: value.redirects,
allowed_scopes: value.allowed_scopes
.into_iter()
.map(|(scope, value)| (scope.into(), value.into()))
.collect(),
}
}
}
impl From<crate::BotOauth2> for BotOauth2 {
fn from(value: crate::BotOauth2) -> Self {
BotOauth2 {
public: value.public,
secret: value.secret,
redirects: value.redirects,
allowed_scopes: value.allowed_scopes
.into_iter()
.map(|(scope, value)| (scope.into(), value.into()))
.collect(),
}
}
}
impl From<crate::OAuth2Scope> for OAuth2Scope {
fn from(value: crate::OAuth2Scope) -> Self {
match value {
crate::OAuth2Scope::ReadIdentify => OAuth2Scope::ReadIdentify,
crate::OAuth2Scope::ReadServers => OAuth2Scope::ReadServers,
crate::OAuth2Scope::WriteFiles => OAuth2Scope::WriteFiles,
crate::OAuth2Scope::Events => OAuth2Scope::Events,
crate::OAuth2Scope::Full => OAuth2Scope::Full,
}
}
}
impl From<OAuth2Scope> for crate::OAuth2Scope {
fn from(value: OAuth2Scope) -> Self {
match value {
OAuth2Scope::ReadIdentify => crate::OAuth2Scope::ReadIdentify,
OAuth2Scope::ReadServers => crate::OAuth2Scope::ReadServers,
OAuth2Scope::WriteFiles => crate::OAuth2Scope::WriteFiles,
OAuth2Scope::Events => crate::OAuth2Scope::Events,
OAuth2Scope::Full => crate::OAuth2Scope::Full,
}
}
}
impl From<crate::OAuth2ScopeReasoning> for OAuth2ScopeReasoning {
fn from(value: crate::OAuth2ScopeReasoning) -> Self {
OAuth2ScopeReasoning {
allow: value.allow,
deny: value.deny,
}
}
}
impl From<OAuth2ScopeReasoning> for crate::OAuth2ScopeReasoning {
fn from(value: OAuth2ScopeReasoning) -> Self {
crate::OAuth2ScopeReasoning {
allow: value.allow,
deny: value.deny,
}
}
}
@@ -782,7 +707,6 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
}
}
}
@@ -794,7 +718,6 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
}
}
}
@@ -1462,43 +1385,3 @@ impl From<FieldsMessage> for crate::FieldsMessage {
}
}
}
impl From<AuthorizedBotId> for crate::AuthorizedBotId {
fn from(value: AuthorizedBotId) -> Self {
crate::AuthorizedBotId {
user: value.user,
bot: value.bot
}
}
}
impl From<crate::AuthorizedBotId> for AuthorizedBotId {
fn from(value: crate::AuthorizedBotId) -> Self {
AuthorizedBotId {
user: value.user,
bot: value.bot
}
}
}
impl From<AuthorizedBot> for crate::AuthorizedBot {
fn from(value: AuthorizedBot) -> Self {
crate::AuthorizedBot {
id: value.id.into(),
created_at: value.created_at,
deauthorized_at: value.deauthorized_at,
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
}
}
}
impl From<crate::AuthorizedBot> for AuthorizedBot {
fn from(value: crate::AuthorizedBot) -> Self {
AuthorizedBot {
id: value.id.into(),
created_at: value.created_at,
deauthorized_at: value.deauthorized_at,
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
}
}
}

View File

@@ -4,4 +4,3 @@ pub mod idempotency;
pub mod permissions;
pub mod reference;
pub mod test_fixtures;
pub mod oauth2;

View File

@@ -1,17 +0,0 @@
use std::marker::PhantomData;
use axum::{extract::FromRequestParts, http::request::Parts};
use revolt_result::{Error, Result};
use super::{OAuth2Scoped, scopes::OAuth2Scope};
#[rocket::async_trait]
impl<S: Send + Sync, Scope: OAuth2Scope> FromRequestParts<S> for OAuth2Scoped<Scope> {
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self> {
parts.extensions.insert(Scope::SCOPE);
Ok(OAuth2Scoped { _scope: PhantomData })
}
}

View File

@@ -1,120 +0,0 @@
use chrono::{TimeDelta, Utc};
use jsonwebtoken::{decode, encode, errors::Error, DecodingKey, EncodingKey, Header, Validation};
use redis_kiss::AsyncCommands;
use revolt_models::v0;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
pub mod scopes;
pub use scopes::OAuth2Scoped;
#[cfg(feature = "rocket")]
pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;
pub use jsonwebtoken::errors::{Error as JWTError, ErrorKind as JWTErrorKind};
use ulid::Ulid;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum TokenType {
Auth,
Access,
Refresh,
}
impl TokenType {
pub fn lifetime(self) -> TimeDelta {
match self {
TokenType::Access => TimeDelta::weeks(1),
TokenType::Auth => TimeDelta::minutes(5),
TokenType::Refresh => TimeDelta::weeks(4),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub jti: String,
pub sub: String,
pub exp: i64,
pub r#type: TokenType,
pub client_id: String,
pub scopes: Vec<v0::OAuth2Scope>,
pub redirect_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
}
#[allow(clippy::too_many_arguments)]
pub fn encode_token(
token_secret: &str,
token_type: TokenType,
user_id: String,
client_id: String,
redirect_uri: String,
scopes: Vec<v0::OAuth2Scope>,
code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
) -> Result<String, Error> {
let now = Utc::now();
let exp = now.checked_add_signed(token_type.lifetime()).unwrap();
println!("{now:?} {exp:}");
let claims = Claims {
jti: Ulid::new().to_string(),
sub: user_id,
exp: exp.timestamp(),
r#type: token_type,
client_id,
scopes,
redirect_uri,
code_challange_method,
};
let encoding_key = EncodingKey::from_secret(token_secret.as_bytes());
encode(&Header::default(), &claims, &encoding_key)
}
pub fn decode_token(token_secret: &str, code: &str) -> Result<Claims, Error> {
let decoding_key = DecodingKey::from_secret(token_secret.as_bytes());
let data = decode(
code,
&decoding_key,
&Validation::new(jsonwebtoken::Algorithm::HS256),
)?;
Ok(data.claims)
}
pub async fn add_code_challange(token: &str, code_challenge: &str) -> Result<()> {
let mut conn = redis_kiss::get_connection()
.await
.map_err(|_| create_error!(InternalError))?;
conn.pset_ex::<_, _, ()>(
format!("oauth2:{token}:code_challenge"),
code_challenge,
TokenType::Access.lifetime().num_milliseconds() as usize,
)
.await
.map_err(|_| create_error!(InternalError))?;
Ok(())
}
pub async fn get_code_challange(token: &str) -> Result<Option<String>> {
let mut conn = redis_kiss::get_connection()
.await
.map_err(|_| create_error!(InternalError))?;
conn.get(format!("oauth2:{token}:code_challenge"))
.await
.map_err(|_| create_error!(InternalError))
}

View File

@@ -1,53 +0,0 @@
use std::{convert::Infallible, marker::PhantomData};
use revolt_okapi::openapi3::{OAuthFlows, SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use rocket::{request::{self, FromRequest}, Request};
use super::{OAuth2Scoped, scopes::OAuth2Scope};
#[rocket::async_trait]
impl<'r, Scope: OAuth2Scope> FromRequest<'r> for OAuth2Scoped<Scope> {
type Error = Infallible;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
request.local_cache(|| Some(Scope::SCOPE));
request::Outcome::Success(OAuth2Scoped { _scope: PhantomData })
}
}
impl<'r, Scope: OAuth2Scope> OpenApiFromRequest<'r> for OAuth2Scoped<Scope> {
fn from_request_input(
_gen: &mut revolt_rocket_okapi::r#gen::OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<revolt_rocket_okapi::request::RequestHeaderInput> {
Ok(RequestHeaderInput::Security(
"OAuth2".to_owned(),
SecurityScheme {
description: None,
extensions: Default::default(),
data: SecuritySchemeData::OAuth2 {
flows: OAuthFlows::AuthorizationCode {
authorization_url: "todo".to_string(),
token_url: "todo".to_string(),
refresh_url: Some("todo".to_string()),
scopes: revolt_okapi::map! {
"read:identify".to_string() => "".to_string(),
"read:servers".to_string() => "".to_string(),
"write:files".to_string() => "".to_string(),
"events".to_string() => "".to_string(),
"full".to_string() => "".to_string(),
},
extensions: Default::default()
}
}
},
revolt_okapi::map! {
"OAuth2".to_owned() => vec![Scope::MODEL.to_string()]
},
))
}
}

View File

@@ -1,29 +0,0 @@
use std::marker::PhantomData;
pub struct OAuth2Scoped<Scope> {
pub(crate) _scope: PhantomData<Scope>
}
pub trait OAuth2Scope {
const SCOPE: crate::OAuth2Scope;
const MODEL: revolt_models::v0::OAuth2Scope;
}
macro_rules! define_oauth2_scope {
($struct_name:ident) => {
pub struct $struct_name;
impl OAuth2Scope for $struct_name {
const SCOPE: crate::OAuth2Scope = crate::OAuth2Scope::$struct_name;
const MODEL: revolt_models::v0::OAuth2Scope = revolt_models::v0::OAuth2Scope::$struct_name;
}
};
}
// This must match the OAuth2Scope enum
// TODO: automatically sync this
define_oauth2_scope!(ReadIdentify);
define_oauth2_scope!(ReadServers);
define_oauth2_scope!(WriteFiles);
define_oauth2_scope!(Events);
define_oauth2_scope!(Full);

View File

@@ -14,40 +14,41 @@ use crate::{
};
/// Reference to some object in the database
pub struct Reference<'a> {
#[derive(Serialize, Deserialize)]
pub struct Reference {
/// Id of object
pub id: &'a str,
pub id: String,
}
impl<'a> Reference<'a> {
impl Reference {
/// Create a Ref from an unchecked string
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
pub fn from_unchecked(id: String) -> Reference {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, self.id).await
db.fetch_ban(server, &self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(self.id).await
db.fetch_bot(&self.id).await
}
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(self.id).await
db.fetch_emoji(&self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(self.id).await
db.fetch_channel(&self.id).await
}
/// Fetch invite from Ref or create invite to server if discoverable
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
if ulid::Ulid::from_str(self.id).is_ok() {
if ulid::Ulid::from_str(&self.id).is_ok() {
let server = self.as_server(db).await?;
if !server.discoverable {
return Err(create_error!(NotFound));
@@ -64,18 +65,18 @@ impl<'a> Reference<'a> {
.ok_or(create_error!(NotFound))?,
})
} else {
db.fetch_invite(self.id).await
db.fetch_invite(&self.id).await
}
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(self.id).await
db.fetch_message(&self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(self.id).await?;
let msg = db.fetch_message(&self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
@@ -85,36 +86,36 @@ impl<'a> Reference<'a> {
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, self.id).await
db.fetch_member(server, &self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(self.id).await
db.fetch_server(&self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(self.id).await
db.fetch_user(&self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(self.id).await
db.fetch_webhook(&self.id).await
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromParam<'r> for Reference<'r> {
impl<'r> FromParam<'r> for Reference {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
Ok(Reference::from_unchecked(param))
Ok(Reference::from_unchecked(param.into()))
}
}
#[cfg(feature = "rocket-impl")]
impl<'a> JsonSchema for Reference<'a> {
impl JsonSchema for Reference {
fn schema_name() -> String {
"Id".to_string()
}

View File

@@ -39,9 +39,7 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => {
db.insert_or_merge_member(&member).await.unwrap();
}
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-files"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.9", path = "../config", features = [
revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.9", path = "../result" }
revolt-result = { version = "0.8.8", path = "../result" }
# image processing
jxl-oxide = "0.8.1"

View File

@@ -80,9 +80,6 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
.decrypt_in_place(nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
// Remove the authentication tag bytes that were added during encryption
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
Ok(buf)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.8.9", path = "../config" }
revolt-permissions = { version = "0.8.9", path = "../permissions" }
revolt-config = { version = "0.8.8", path = "../config" }
revolt-permissions = { version = "0.8.8", path = "../permissions" }
# Utility
regex = "1.11"

View File

@@ -1,34 +0,0 @@
use iso8601_timestamp::Timestamp;
use crate::v0::{PublicBot, OAuth2Scope};
auto_derived!(
/// Unique id of the user and bot
pub struct AuthorizedBotId {
/// User id
pub user: String,
/// Bot Id
pub bot: String,
}
pub struct AuthorizedBot {
/// Unique Id
#[serde(rename = "_id")]
pub id: AuthorizedBotId,
/// When the authorized oauth2 bot connection was created at
pub created_at: Timestamp,
/// If and when the authorized oauth2 bot connection was revoked at
pub deauthorized_at: Option<Timestamp>,
/// Scopes the bot has access to
pub scope: Vec<OAuth2Scope>,
}
pub struct AuthorizedBotsResponse {
pub public_bot: PublicBot,
pub authorized_bot: AuthorizedBot,
}
);

View File

@@ -1,8 +1,4 @@
use super::{User, OAuth2Scope, OAuth2ScopeReasoning};
use std::collections::HashMap;
#[cfg(feature = "validator")]
use validator::Validate;
use super::User;
auto_derived!(
/// Bot
@@ -52,13 +48,6 @@ auto_derived!(
)]
pub privacy_policy_url: String,
/// Oauth2 bot settings
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Option::is_none", default)
)]
pub oauth2: Option<BotOauth2>,
/// Enum of bot flags
#[cfg_attr(
feature = "serde",
@@ -71,8 +60,6 @@ auto_derived!(
pub enum FieldsBot {
Token,
InteractionsURL,
Oauth2,
Oauth2Secret,
}
/// Flags that may be attributed to a bot
@@ -114,7 +101,7 @@ auto_derived!(
/// Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateBot {
/// Bot username
#[cfg_attr(
@@ -126,7 +113,7 @@ auto_derived!(
/// New Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataEditBot {
/// Bot username
#[cfg_attr(
@@ -144,22 +131,9 @@ auto_derived!(
/// Interactions URL
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
pub interactions_url: Option<String>,
#[cfg_attr(feature = "validator", validate)]
pub oauth2: Option<DataEditBotOauth2>,
/// Fields to remove from bot object
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsBot>,
}
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditBotOauth2 {
pub public: Option<bool>,
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 10)))]
pub redirects: Option<Vec<String>>,
pub allowed_scopes: Option<HashMap<OAuth2Scope, OAuth2ScopeReasoning>>
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsBot>>,
}
/// Where we are inviting a bot to
@@ -196,22 +170,3 @@ auto_derived!(
pub user: User,
}
);
auto_derived_partial!(
/// Bot Oauth2 information
pub struct BotOauth2 {
/// Whether the oauth client is public and should not receive a secret key
#[serde(default)]
pub public: bool,
/// Secret key used for authorisation, not provided if the client is public
#[serde(default)]
pub secret: Option<String>,
/// Allowed redirects for the authorisation
#[serde(default)]
pub redirects: Vec<String>,
/// Mapping of allowed scopes and the reasonings
#[serde(default)]
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
},
"PartialBotOauth2"
);

View File

@@ -207,7 +207,7 @@ auto_derived!(
/// Fields to remove from channel
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsChannel>,
pub remove: Option<Vec<FieldsChannel>>,
}
/// Create new group

View File

@@ -215,7 +215,7 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon_url: Option<String>,
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>,

View File

@@ -1,4 +1,3 @@
mod authorized_bots;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -8,7 +7,6 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod oauth2;
mod policy_changes;
mod safety_reports;
mod server_bans;
@@ -17,7 +15,6 @@ mod servers;
mod user_settings;
mod users;
pub use authorized_bots::*;
pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
@@ -27,7 +24,6 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use oauth2::*;
pub use policy_changes::*;
pub use safety_reports::*;
pub use server_bans::*;

View File

@@ -1,140 +0,0 @@
use crate::v0::{PublicBot, User};
use std::{collections::HashMap, fmt::Display};
auto_derived!(
pub struct OAuth2AuthorizeAuthResponse {
/// Redirect URI which will contain the access token
pub redirect_uri: String,
}
pub struct OAuth2ScopeReasoning {
pub allow: String,
pub deny: String
}
pub struct OAuth2AuthorizeInfoResponse {
pub bot: PublicBot,
pub user: User,
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>
}
#[derive(Copy)]
#[cfg_attr(feature = "serde", serde(rename = "lowercase"))]
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
pub enum OAuth2ResponseType {
#[cfg_attr(feature = "rocket", field(value = "code"))]
Code,
#[cfg_attr(feature = "rocket", field(value = "token"))]
Token,
}
#[derive(Copy)]
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
pub enum OAuth2GrantType {
#[cfg_attr(feature = "rocket", field(value = "authorization_code"))]
#[cfg_attr(feature = "serde", serde(rename = "authorization_code"))]
AuthorizationCode,
#[cfg_attr(feature = "rocket", field(value = "implicit"))]
#[cfg_attr(feature = "serde", serde(rename = "implicit"))]
Implicit,
#[cfg_attr(feature = "rocket", field(value = "refresh_token"))]
#[cfg_attr(feature = "serde", serde(rename = "refresh_token"))]
RefreshToken
}
#[derive(Copy)]
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
pub enum OAuth2CodeChallangeMethod {
#[cfg_attr(feature = "rocket", field(value = "plain"))]
#[cfg_attr(feature = "serde", serde(rename = "plain"))]
Plain,
S256
}
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
pub struct OAuth2AuthorizationForm {
pub client_id: String,
pub scope: String,
pub redirect_uri: String,
pub response_type: OAuth2ResponseType,
pub state: Option<String>,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<OAuth2CodeChallangeMethod>,
}
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
pub struct OAuth2TokenExchangeForm {
pub grant_type: OAuth2GrantType,
pub client_id: String,
pub client_secret: Option<String>,
/// Authorization code
pub code: Option<String>,
// Refresh token to generate new access token
pub refresh_token: Option<String>,
pub code_verifier: Option<String>,
}
pub struct OAuth2TokenExchangeResponse {
pub access_token: String,
pub refresh_token: Option<String>,
pub token_type: String,
pub scope: String,
}
#[derive(Copy, Hash)]
#[cfg_attr(feature = "serde", serde(rename = "snake_case"))]
pub enum OAuth2Scope {
#[cfg_attr(feature = "serde", serde(rename = "read:identify"))]
ReadIdentify,
#[cfg_attr(feature = "serde", serde(rename = "read:servers"))]
ReadServers,
#[cfg_attr(feature = "serde", serde(rename = "write:files"))]
WriteFiles,
#[cfg_attr(feature = "serde", serde(rename = "events"))]
Events,
#[cfg_attr(feature = "serde", serde(rename = "full"))]
Full,
}
);
impl Display for OAuth2Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
OAuth2Scope::ReadIdentify => "read:identify",
OAuth2Scope::ReadServers => "read:servers",
OAuth2Scope::WriteFiles => "write:files",
OAuth2Scope::Events => "events",
OAuth2Scope::Full => "full",
})
}
}
impl OAuth2Scope {
#[allow(clippy::should_implement_trait)]
pub fn from_str(str: &str) -> Option<OAuth2Scope> {
match str {
"read:identify" => Some(OAuth2Scope::ReadIdentify),
"read:servers" => Some(OAuth2Scope::ReadServers),
"events" => Some(OAuth2Scope::Events),
"full" => Some(OAuth2Scope::Full),
_ => None,
}
}
pub fn scopes_from_str(string: &str) -> Option<Vec<OAuth2Scope>> {
let mut scopes = Vec::new();
for scope in string.split(' ') {
if let Some(scope) = OAuth2Scope::from_str(scope) {
scopes.push(scope);
} else {
return None;
}
}
Some(scopes)
}
}

View File

@@ -77,7 +77,6 @@ auto_derived!(
Avatar,
Roles,
Timeout,
JoinedAt,
}
/// Member removal intention
@@ -125,7 +124,7 @@ auto_derived!(
/// Timestamp this member is timed out until
pub timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsMember>,
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
}
);

View File

@@ -175,8 +175,6 @@ auto_derived!(
/// Ranking position
///
/// Smaller values take priority.
///
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
}
@@ -249,8 +247,8 @@ auto_derived!(
pub analytics: Option<bool>,
/// Fields to remove from server object
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsServer>,
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsServer>>,
}
/// New role information
@@ -272,8 +270,8 @@ auto_derived!(
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
/// Fields to remove from role object
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsRole>,
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsRole>>,
}
/// New role permissions

View File

@@ -245,8 +245,8 @@ auto_derived!(
pub flags: Option<i32>,
/// Fields to remove from user object
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsUser>,
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsUser>>,
}
/// User flag reponse
@@ -255,14 +255,12 @@ auto_derived!(
pub flags: i32,
}
/// Mutual friends, servers, groups and DMs response
/// Mutual friends and servers response
pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with
pub users: Vec<String>,
/// Array of mutual server IDs that both users are in
pub servers: Vec<String>,
/// Array of mutual group and dm IDs that both users are in
pub channels: Vec<String>,
}
/// Bot information for if the user is a bot

View File

@@ -1,9 +1,8 @@
[package]
name = "revolt-parser"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
license = "AGPL-3.0-or-later"
description = "Revolt Backend: Message Parser"
[dependencies]

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More