Compare commits

..

7 Commits

Author SHA1 Message Date
Zomatree
1a60aecdb3 chore: switch to method chaining over macro 2025-08-15 02:07:26 +01:00
Zomatree
789dedd9f1 chore: cleanup errors 2025-08-15 02:07:26 +01:00
Zomatree
d7cf809424 fix: make sentry dep optional 2025-08-15 02:07:25 +01:00
Zomatree
249a4818fc feat: error docs 2025-08-15 02:07:25 +01:00
Zomatree
46e127ccd2 chore: send rest of the errors to the catchers 2025-08-15 02:07:25 +01:00
Zomatree
cf4fe859bf feat: move all json and validate to new utils 2025-08-15 02:07:25 +01:00
Zomatree
3d6f39a0eb feat: better error handling 2025-08-15 02:00:12 +01:00
210 changed files with 2206 additions and 5449 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) {

1779
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

@@ -26,3 +26,7 @@ disallowed-methods = [
# Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
]
disallowed-types = [
"rocket::serde::json::Json",
]

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"
@@ -19,7 +19,6 @@ async-channel = "2.3.1"
# parsing
querystring = "1.1.0"
regex = "1.11.1"
# serde
bincode = "1.3.3"
@@ -38,11 +37,11 @@ async-std = { version = "1.8.0", features = [
# core
authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result" }
revolt-result = { path = "../core/result", features = ["sentry"] }
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

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

@@ -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 revolt_result::{create_error, Result, ToRevoltError};
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,
}
}
@@ -47,16 +38,22 @@ impl ProtocolConfiguration {
match self.format {
ProtocolFormat::Json => {
if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
// Log the error in-case we make a breaking change to the payload
serde_json::from_str(text)
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else {
Err(create_error!(InternalError))
Err(create_error!(UnprocessableEntity))
}
}
ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
rmp_serde::from_slice(buf)
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else {
Err(create_error!(InternalError))
Err(create_error!(UnprocessableEntity))
}
}
}
@@ -95,8 +92,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 +130,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 +145,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 +157,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

@@ -14,14 +14,11 @@ use futures::{
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::{
@@ -29,7 +26,7 @@ use async_std::{
sync::{Mutex, RwLock},
task::spawn,
};
use revolt_result::create_error;
use revolt_result::{create_error, ToRevoltError};
use sentry::Level;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
@@ -90,71 +87,43 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
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())
Ok(user) => user,
Err(err) => {
write
.send(config.encode(&EventV1::Error { data: err }))
.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;
}
}
return;
}
};
info!("User {addr:?} authenticated as @{}", user.username);
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() {
if write.send(config.encode(&EventV1::Authenticated)).await.to_internal_error().is_err() {
return;
}
// Download required data to local cache and send Ready payload.
let ready_payload = match report_internal_error!(
state
let ready_payload = match state
.generate_ready_payload(db, config.get_ready_payload_fields())
.await
) {
.to_internal_error()
{
Ok(ready_payload) => ready_payload,
Err(_) => return,
};
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
if write.send(config.encode(&ready_payload)).await.to_internal_error().is_err() {
return;
}
@@ -249,14 +218,15 @@ async fn listener(
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()
) {
let subscriber = match fred::types::Builder::from_config(redis_config)
.build_subscriber_client()
.to_internal_error()
{
Ok(subscriber) => subscriber,
Err(_) => return,
};
if report_internal_error!(subscriber.init().await).is_err() {
if subscriber.init().await.to_internal_error().is_err() {
return;
}
@@ -279,13 +249,13 @@ async fn listener(
// Check for state changes for subscriptions.
match state.apply_state().await {
SubscriptionStateChange::Reset => {
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
if subscriber.unsubscribe_all().await.to_internal_error().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() {
if subscriber.subscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -298,7 +268,7 @@ async fn listener(
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
if subscriber.unsubscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -307,7 +277,7 @@ async fn listener(
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
if subscriber.subscribe(id).await.to_internal_error().is_err() {
break 'out;
}
}
@@ -332,7 +302,7 @@ async fn listener(
_ = t2 => {},
message = t1 => {
// Handle incoming events.
let message = match report_internal_error!(message) {
let message = match message.to_internal_error() {
Ok(message) => message,
Err(_) => break 'out
};
@@ -341,15 +311,15 @@ async fn listener(
PayloadType::Json => message
.value
.as_str()
.and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).to_internal_error().ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).to_internal_error().ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
.and_then(|b| bincode::deserialize::<EventV1>(b).to_internal_error().ok()),
};
let Some(mut event) = event else {
@@ -409,7 +379,7 @@ async fn listener(
}
}
report_internal_error!(subscriber.quit().await).ok();
subscriber.quit().await.to_internal_error().ok();
}
#[allow(clippy::too_many_arguments)]

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>"]
@@ -37,4 +37,4 @@ sentry = { version = "0.31.5", optional = true }
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

@@ -56,10 +56,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 +277,3 @@ files = ""
proxy = ""
pushd = ""
crond = ""
gifbox = ""

View File

@@ -190,8 +190,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 +365,6 @@ pub struct Sentry {
pub proxy: String,
pub pushd: String,
pub crond: String,
pub gifbox: String,
}
#[derive(Deserialize, Debug, Clone)]

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>"]
@@ -24,19 +24,19 @@ 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" }
# Utility
log = "0.4"
@@ -53,8 +53,6 @@ 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"

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

@@ -20,31 +20,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 +63,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 +144,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 {

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

@@ -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

@@ -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

@@ -1,7 +1,7 @@
use bson::to_document;
use bson::Document;
use revolt_config::report_internal_error;
use revolt_result::Result;
use revolt_result::ToRevoltError;
use crate::File;
use crate::FileUsedFor;
@@ -106,7 +106,7 @@ impl AbstractAttachments for MongoDb {
},
doc! {
"$set": {
"used_for": report_internal_error!(to_document(&used_for))?,
"used_for": to_document(&used_for).to_internal_error()?,
"uploader_id": uploader_id
}
},

View File

@@ -10,7 +10,6 @@ use revolt_models::v0::{
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result};
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
@@ -440,8 +439,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 +447,6 @@ impl Message {
}));
}
replies.reserve(entries.len());
for ReplyIntent {
id,
mention,
@@ -464,12 +460,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.
@@ -497,31 +488,28 @@ impl Message {
| Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members {
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
valid_members.iter().map(|m| &m.id.user),
);
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await?;
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
valid_members.iter().map(|m| &m.id.user),
);
if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
} else {
revolt_config::capture_error(&valid_members.unwrap_err());
return Err(create_error!(InternalError));
if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
}
Channel::SavedMessages { .. } => {
user_mentions.clear();
@@ -542,7 +530,9 @@ impl Message {
}
if !replies.is_empty() {
message.replies.replace(replies);
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Calculate final message flags
@@ -723,12 +713,6 @@ impl Message {
/// Create text embed from sendable embed
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media {
Some(File::use_attachment(db, &id, &self.id, &self.author).await?)
} else {

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::*;
@@ -48,7 +46,6 @@ use crate::MongoDb;
pub trait AbstractDatabase:
Sync
+ Send
+ authorized_bots::AbstractAuthorizedBots
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ channels::AbstractChannels

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

@@ -73,13 +73,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 +100,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 +119,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

@@ -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

@@ -705,6 +705,8 @@ impl User {
duration_days: Option<usize>,
reason: Option<Vec<String>>,
) -> Result<()> {
// TODO: authifier Error should implement Error
let authifier = db.clone().to_authifier().await;
let mut account = authifier
.database

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);
@@ -67,6 +38,7 @@ impl<'r> FromRequest<'r> for User {
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
request.local_cache(|| Some(create_error!(InvalidSession)));
Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession))
}
}

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

@@ -113,6 +113,8 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
let idempotency = IdempotencyKey { key };
let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.key).is_some() {
request.local_cache(|| Some(create_error!(DuplicateNonce)));
return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
}

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

@@ -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>"]
@@ -14,16 +14,16 @@ imagesize = "0.13.0"
tempfile = "3.12.0"
base64 = "0.22.1"
aes-gcm = "0.10.3"
aes-gcm = { version = "0.10.3", features = ["std"] }
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", features = ["sentry"] }
# image processing
jxl-oxide = "0.8.1"

View File

@@ -6,7 +6,7 @@ use aes_gcm::{
};
use image::{DynamicImage, ImageBuffer};
use revolt_config::{config, report_internal_error, FilesS3};
use revolt_result::{create_error, Result};
use revolt_result::{create_error, Result, ToRevoltError};
use aws_sdk_s3::{
config::{Credentials, Region},
@@ -55,13 +55,12 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Send a request for the file
let mut obj =
report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?;
client.get_object().bucket(bucket_id).key(path).send().await.to_internal_error()?;
// Read the file from remote
let mut buf = vec![];
while let Some(bytes) = obj.body.next().await {
let data = report_internal_error!(bytes)?;
report_internal_error!(buf.write_all(&data))?;
buf.write_all(&bytes.to_internal_error()?).to_internal_error()?;
// is there a more efficient way to do this?
// we just want the Vec<u8>
}
@@ -78,10 +77,7 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Decrypt the file
create_cipher(&config.files.encryption_key)
.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);
.to_internal_error()?;
Ok(buf)
}
@@ -100,18 +96,17 @@ pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result<Str
// Encrypt the file in place
create_cipher(&config.files.encryption_key)
.encrypt_in_place(&nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
.to_internal_error()?;
// Upload the file to remote
report_internal_error!(
client
.put_object()
.bucket(bucket_id)
.key(path)
.body(buf.into())
.send()
.await
)?;
client
.put_object()
.bucket(bucket_id)
.key(path)
.body(buf.into())
.send()
.await
.to_internal_error()?;
Ok(BASE64_STANDARD.encode(nonce))
}
@@ -121,14 +116,13 @@ pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> {
let config = config().await;
let client = create_client(config.files.s3);
report_internal_error!(
client
.delete_object()
.bucket(bucket_id)
.key(path)
.send()
.await
)?;
client
.delete_object()
.bucket(bucket_id)
.key(path)
.send()
.await
.to_internal_error()?;
Ok(())
}
@@ -148,8 +142,7 @@ pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> {
pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> {
match mime {
"image/svg+xml" => {
let tree =
report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?;
let tree = usvg::Tree::from_data(v, &Default::default()).to_internal_error().ok()?;
let size = tree.size();
Some((size.width() as usize, size.height() as usize))
@@ -224,9 +217,9 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
"image/svg+xml" => {
// usvg doesn't support Read trait so copy to buffer
let mut buf = Vec::new();
report_internal_error!(reader.read_to_end(&mut buf))?;
reader.read_to_end(&mut buf).to_internal_error()?;
let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?;
let tree = usvg::Tree::from_data(&buf, &Default::default()).to_internal_error()?;
let size = tree.size();
let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32)
.ok_or_else(|| create_error!(ImageProcessingFailed))?;
@@ -244,10 +237,11 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
))
}
// Check if we can read using image-rs crate
_ => report_internal_error!(report_internal_error!(
image::ImageReader::new(reader).with_guessed_format()
)?
.decode()),
_ => image::ImageReader::new(reader)
.with_guessed_format()
.to_internal_error()?
.decode()
.to_internal_error()
}
}

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,24 +131,11 @@ 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>>
}
/// Where we are inviting a bot to
#[serde(untagged)]
pub enum InviteBotDestination {
@@ -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

@@ -1,4 +1,3 @@
mod authorized_bots;
mod bots;
mod channel_invites;
mod channel_unreads;
@@ -8,7 +7,7 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod oauth2;
mod onboard;
mod policy_changes;
mod safety_reports;
mod server_bans;
@@ -17,7 +16,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 +25,7 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use oauth2::*;
pub use onboard::*;
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

@@ -0,0 +1,15 @@
auto_derived!(
/// # New User Data
#[derive(validator::Validate)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
}
/// # Onboarding Status
pub struct DataHello {
/// Whether onboarding is required
pub onboarding: bool,
}
);

View File

@@ -1,6 +1,17 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// # Report Data
#[derive(validator::Validate)]
pub struct DataReportContent {
/// Content being reported
pub content: ReportedContent,
/// Additional report description
#[validate(length(min = 0, max = 1000))]
#[serde(default)]
pub additional_context: String,
}
/// User-generated platform moderation report
pub struct Report {
/// Unique Id

View File

@@ -77,7 +77,6 @@ auto_derived!(
Avatar,
Roles,
Timeout,
JoinedAt,
}
/// Member removal intention

View File

@@ -1,4 +1,4 @@
use super::{Channel, File, RE_COLOUR};
use super::{Channel, File, Member, User, RE_COLOUR};
use revolt_permissions::{Override, OverrideField};
use std::collections::HashMap;
@@ -293,4 +293,22 @@ auto_derived!(
pub struct DataEditRoleRanks {
pub ranks: Vec<String>,
}
/// # Query Parameters
#[derive(FromForm)]
pub struct OptionsQueryMembers {
/// String to search for
pub query: String,
/// Discourage use of this API
pub experimental_api: bool,
}
/// # Query members by name
pub struct MemberQueryResponse {
/// List of members
pub members: Vec<Member>,
/// List of users
pub users: Vec<User>,
}
);

View File

@@ -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
@@ -277,6 +275,17 @@ auto_derived!(
/// Username and discriminator combo separated by #
pub username: String,
}
/// # Username Information
#[derive(Validate)]
pub struct DataChangeUsername {
/// New username
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
/// Current account password
#[validate(length(min = 8, max = 1024))]
pub password: String,
}
);
pub trait CheckRelationship {

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.

View File

@@ -4,18 +4,15 @@ use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
#[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
pub enum MessageToken<'a> {
pub enum MessageToken {
#[token("\\")]
Escape,
#[regex("```[^`\n]*", |_| 3)]
#[regex("``", |_| 2)]
#[regex("`", |_| 1)]
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
CodeblockMarker(usize),
#[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
UserMention(&'a str),
#[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
RoleMention(&'a str),
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
UserMention(String),
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
RoleMention(String),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
@@ -30,13 +27,13 @@ pub struct MessageResults {
pub mentions_online: bool
}
struct MessageParserIterator<'a, I> {
struct MessageParserIterator<I> {
inner: I,
temp: VecDeque<MessageToken<'a>>
temp: VecDeque<MessageToken>
}
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
type Item = MessageToken<'a>;
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
@@ -83,8 +80,8 @@ pub fn parse_message(text: &str) -> MessageResults {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
@@ -109,7 +106,7 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
@@ -117,7 +114,7 @@ mod tests {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
@@ -141,8 +138,8 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -152,8 +149,8 @@ mod tests {
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -173,14 +170,15 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_inline_codeblock_no_mentions() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
@@ -194,21 +192,12 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_with_language_no_mentions() {
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.8.9", path = "../result" }
revolt-result = { version = "0.8.8", path = "../result" }
# Utility
auto_ops = "0.3.0"

View File

@@ -8,18 +8,10 @@ pub use server::*;
pub use user::*;
/// Holds a permission value to manipulate.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug)]
pub struct PermissionValue(u64);
impl PermissionValue {
pub fn from_raw(value: u64) -> Self {
Self(value)
}
pub fn into_raw(self) -> u64 {
self.0
}
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.allow(v.allow);

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -16,7 +16,7 @@ redis-is-patched = []
async-std = { version = "1.8.0", features = ["attributes"] }
# Config for loading Redis URI
revolt-config = { version = "0.8.9", path = "../config" }
revolt-config = { version = "0.8.8", path = "../config" }
[dependencies]
# Utility

View File

@@ -1,26 +0,0 @@
[package]
name = "revolt-ratelimits"
version = "0.8.9"
edition = "2024"
[features]
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"]
[dependencies]
revolt-database = { version = "0.8.9", path = "../database"}
revolt-result = { version = "0.8.9", path = "../result" }
revolt-config = { version = "0.8.9", path = "../config" }
rocket = { version = "0.5.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
axum = { version = "0.7.5", optional = true, features = ["macros"] }
serde = { version = "1", features = ["derive"] }
authifier = { version = "1.0.15" }
dashmap = "5.2.0"
async-trait = "0.1.81"
log = "0.4"

View File

@@ -1,194 +0,0 @@
use std::net::SocketAddr;
use async_trait::async_trait;
use axum::{
Json, RequestPartsExt, Router,
body::Body,
extract::{ConnectInfo, FromRef, FromRequestParts, State},
http::{HeaderValue, Request, StatusCode, request::Parts},
middleware::Next,
response::{IntoResponse, Response},
routing::get,
};
use revolt_database::{Database, User};
use revolt_config::config;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind};
#[derive(Clone, Copy)]
pub struct AxumRequestKind;
impl RequestKind for AxumRequestKind {
type R<'a> = Parts;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<AxumRequestKind>;
fn to_ip(parts: &Parts) -> String {
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|info| info.ip().to_string())
.unwrap_or_default()
}
async fn to_real_ip(parts: &Parts) -> String {
if config().await.api.security.trust_cloudflare {
parts
.headers
.get("CF-Connecting-IP")
.map(|x| x.to_str().unwrap().to_string())
.unwrap_or_else(|| to_ip(parts))
} else {
to_ip(parts)
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<Ratelimiter>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let storage = RatelimitStorage::from_ref(state);
let identifier = if let Ok(user) = parts.extract_with_state::<User, _>(state).await {
user.id
} else {
to_real_ip(parts).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(parts);
let limit = storage.resolver.resolve_bucket_limit(bucket);
let ratelimiter =
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource));
parts.extensions.insert(ratelimiter.map_err(Json));
};
*parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap()
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<RatelimitInformation>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let ratelimiter = parts.extract_with_state::<Ratelimiter, S>(state).await;
parts.extensions.insert(ratelimiter);
};
let ratelimiter = *parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap();
match ratelimiter {
Ok(ratelimter) => Ok(RatelimitInformation::Success(ratelimter)),
Err(ratelimiter) => Err(Json(RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
})),
}
}
}
pub async fn ratelimit_middleware(
State(database): State<Database>,
State(ratelimit_storage): State<RatelimitStorage>,
request: Request<Body>,
next: Next,
) -> Response {
#[derive(axum::extract::FromRef)]
struct TempState {
database: Database,
ratelimit_storage: RatelimitStorage,
}
let state = TempState {
database,
ratelimit_storage,
};
let (mut parts, body) = request.into_parts();
let res = Ratelimiter::from_request_parts(&mut parts, &state).await;
let (Ok(ratelimiter) | Err(Json(ratelimiter))) = &res;
let mut response = if res.is_ok() {
let request = Request::from_parts(parts, body);
next.run(request).await
} else {
let ratelimit_info = RatelimitInformation::from_request_parts(&mut parts, &state).await;
ratelimit_info.map(Json).into_response()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
let headers = response.headers_mut();
headers.insert(
"X-RateLimit-Limit",
HeaderValue::from_str(&limit.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Bucket",
HeaderValue::from_str(&key.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Remaining",
HeaderValue::from_str(&remaining.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Reset-After",
HeaderValue::from_str(&reset.to_string()).unwrap(),
);
if res.is_err() {
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
};
response
}
async fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes<S: Clone + Send + Sync + 'static>() -> Router<S>
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
Router::new().route("/ratelimit", get(ratelimit_info))
}

View File

@@ -1,7 +0,0 @@
pub mod ratelimiter;
#[cfg(feature = "rocket")]
pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;

View File

@@ -1,145 +0,0 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::ops::Add;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
use dashmap::DashMap;
pub trait RequestKind {
type R<'a>;
}
pub trait RatelimitResolver<R>: Send + Sync {
fn resolve_bucket<'a>(&self, request: &'a R) -> (&'a str, Option<&'a str>);
fn resolve_bucket_limit(&self, bucket: &str) -> u32;
}
#[derive(Clone)]
pub struct RatelimitStorage<K: RequestKind> {
pub resolver: Arc<dyn for<'a> RatelimitResolver<K::R<'a>>>,
pub map: Arc<DashMap<u64, Entry>>,
}
impl<K: RequestKind> RatelimitStorage<K> {
pub fn new<R: for<'a> RatelimitResolver<K::R<'a>> + 'static>(resolver: R) -> Self {
Self {
resolver: Arc::new(resolver),
map: Arc::new(DashMap::new()),
}
}
}
/// Ratelimit Bucket
#[derive(Clone, Copy, Debug)]
pub struct Entry {
used: u32,
reset: u128,
}
/// Get the current time from Unix Epoch as a Duration
fn now() -> Duration {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards...")
}
impl Entry {
/// Find bucket by its key
pub fn from(map: &DashMap<u64, Entry>, key: u64) -> Entry {
map.get(&key).map(|x| *x).unwrap_or_else(|| Entry {
used: 0,
reset: now().add(Duration::from_secs(10)).as_millis(),
})
}
/// Deduct one unit from the bucket and save
pub fn deduct(&mut self) {
let current_time = now().as_millis();
if current_time > self.reset {
self.used = 1;
self.reset = now().add(Duration::from_secs(10)).as_millis();
} else {
self.used += 1;
}
}
/// Save information
pub fn save(self, map: &DashMap<u64, Entry>, key: u64) {
map.insert(key, self);
}
/// Get remaining units in the bucket
pub fn get_remaining(&self, limit: u32) -> u32 {
if now().as_millis() > self.reset {
limit
} else {
limit - self.used
}
}
/// Get how long bucket has until reset
pub fn left_until_reset(&self) -> u128 {
let current_time = now().as_millis();
self.reset.saturating_sub(current_time)
}
}
/// Ratelimit Guard
#[derive(Serialize, Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct Ratelimiter {
pub key: u64,
pub limit: u32,
pub remaining: u32,
pub reset: u128,
}
impl Ratelimiter {
/// Generate guard from identifier and target bucket
pub fn from(
map: &DashMap<u64, Entry>,
identifier: &str,
limit: u32,
(bucket, resource): (&str, Option<&str>),
) -> Result<Ratelimiter, Ratelimiter> {
let mut key = DefaultHasher::new();
key.write(identifier.as_bytes());
key.write(bucket.as_bytes());
if let Some(id) = resource {
key.write(id.as_bytes());
}
let key = key.finish();
let mut entry = Entry::from(map, key);
let remaining = entry.get_remaining(limit);
let reset = entry.left_until_reset();
let mut ratelimiter = Ratelimiter {
key,
limit,
remaining,
reset,
};
if remaining == 0 {
return Err(ratelimiter);
}
entry.deduct();
entry.save(map, key);
ratelimiter.remaining -= 1;
ratelimiter.reset = entry.left_until_reset();
Ok(ratelimiter)
}
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum RatelimitInformation {
Success(Ratelimiter),
Failure { retry_after: u128 },
}

View File

@@ -1,163 +0,0 @@
use async_trait::async_trait;
use log::info;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::uri::Origin;
use rocket::http::{Method, Status};
use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::{Data, Request, Response, State};
use revolt_config::config;
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use authifier::models::Session;
use crate::ratelimiter::RequestKind;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
#[derive(Clone, Copy)]
pub struct RocketRequestKind;
impl RequestKind for RocketRequestKind {
type R<'a> = Request<'a>;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
/// Find the remote IP of the client
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
request
.remote()
.map(|x| x.ip().to_string())
.unwrap_or_default()
}
/// Find the actual IP of the client
async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
if config().await.api.security.trust_cloudflare {
request
.headers()
.get_one("CF-Connecting-IP")
.map(|x| x.to_string())
.unwrap_or_else(|| to_ip(request))
} else {
to_ip(request)
}
}
#[async_trait]
impl<'r> FromRequest<'r> for Ratelimiter {
type Error = Ratelimiter;
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
let ratelimiter = request
.local_cache_async(async {
use rocket::outcome::Outcome;
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
{
session.id
} else {
to_real_ip(request).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(request);
let limit = storage.resolver.resolve_bucket_limit(bucket);
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
})
.await;
match ratelimiter {
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
}
}
}
impl OpenApiFromRequest<'_> for Ratelimiter {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}
/// Attach ratelimiter to the Rocket application
pub struct RatelimitFairing;
#[async_trait]
impl Fairing for RatelimitFairing {
fn info(&self) -> Info {
Info {
name: "Ratelimiter",
kind: Kind::Request | Kind::Response,
}
}
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
use rocket::outcome::Outcome;
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
info!(
"User rate-limited on route {}! (IP = {:?})",
request.uri(),
to_real_ip(request).await
);
request.set_method(Method::Get);
request.set_uri(Origin::parse("/ratelimit").unwrap())
}
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
let guard = request.guard::<Ratelimiter>().await;
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
unreachable!()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
if guard.is_error() {
response.set_status(Status::TooManyRequests);
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for RatelimitInformation {
type Error = u128;
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
let info = match request.guard::<Ratelimiter>().await {
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
},
_ => unreachable!(),
};
Outcome::Success(info)
}
}
#[rocket::get("/ratelimit")]
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes() -> Vec<rocket::Route> {
rocket::routes![ratelimit_info]
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -15,6 +15,7 @@ utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
sentry = ["dep:sentry"]
default = ["serde"]
@@ -34,3 +35,6 @@ revolt_okapi = { version = "0.9.1", optional = true }
# Axum
axum = { version = "0.7.5", optional = true }
# Sentry
sentry = { version = "0.31.5", optional = true }

View File

@@ -2,10 +2,9 @@ use axum::{http::StatusCode, response::IntoResponse, Json};
use crate::{Error, ErrorType};
/// HTTP response builder for Error enum
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let status = match self.error_type {
impl Error {
pub fn axum_status(&self) -> StatusCode {
match self.error_type {
ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN,
@@ -70,25 +69,31 @@ impl IntoResponse for Error {
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
ErrorType::NotAuthenticated => StatusCode::UNAUTHORIZED,
ErrorType::Conflict => StatusCode::CONFLICT,
ErrorType::DuplicateNonce => StatusCode::CONFLICT,
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::IOError => StatusCode::BAD_REQUEST,
ErrorType::UnprocessableEntity => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::DeserializationError { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::FailedValidation { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::MissingScope { .. } => StatusCode::UNAUTHORIZED,
ErrorType::ExpiredToken => StatusCode::UNAUTHORIZED,
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::FileTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST,
ErrorType::ImageProcessingFailed => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::NoEmbedData => StatusCode::BAD_REQUEST,
};
(status, Json(&self)).into_response()
}
}
}
/// HTTP response builder for Error enum
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
(self.axum_status(), Json(&self)).into_response()
}
}

View File

@@ -1,3 +1,4 @@
use std::panic::Location;
use std::fmt::Display;
#[cfg(feature = "serde")]
@@ -51,7 +52,7 @@ impl std::error::Error for Error {}
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
pub enum ErrorType {
/// This error was not labeled :(
LabelMe,
@@ -151,9 +152,15 @@ pub enum ErrorType {
InvalidSession,
InvalidFlagValue,
NotAuthenticated,
Conflict,
DuplicateNonce,
NotFound,
NoEffect,
IOError,
UnprocessableEntity,
DeserializationError {
error: String,
},
FailedValidation {
error: String,
},
@@ -168,19 +175,13 @@ pub enum ErrorType {
ImageProcessingFailed,
NoEmbedData,
// ? OAuth2 Errors
ExpiredToken,
MissingScope {
scope: String
},
// ? Legacy errors
VosoUnavailable,
// ? Feature flag disabled in the config
FeatureDisabled {
feature: String,
},
}
}
#[macro_export]
@@ -203,6 +204,62 @@ macro_rules! create_database_error {
};
}
pub trait ToRevoltError<T>: Sized {
fn capture_error(self) -> Self;
#[track_caller]
fn to_internal_error(self) -> Result<T, Error>;
}
impl<T, E: std::error::Error> ToRevoltError<T> for Result<T, E> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect_err(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.map_err(|_| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
impl<T: std::error::Error> ToRevoltError<T> for Option<T> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.ok_or_else(|| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
#[cfg(test)]
mod tests {
use crate::ErrorType;

View File

@@ -8,10 +8,9 @@ use rocket::{
use crate::{Error, ErrorType};
/// HTTP response builder for Error enum
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
let status = match self.error_type {
impl Error {
pub fn rocket_status(&self) -> Status {
match self.error_type {
ErrorType::LabelMe => Status::InternalServerError,
ErrorType::AlreadyOnboarded => Status::Forbidden,
@@ -77,24 +76,30 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::InvalidProperty => Status::BadRequest,
ErrorType::InvalidSession => Status::Unauthorized,
ErrorType::NotAuthenticated => Status::Unauthorized,
ErrorType::Conflict => Status::Conflict,
ErrorType::DuplicateNonce => Status::Conflict,
ErrorType::VosoUnavailable => Status::BadRequest,
ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::IOError => Status::BadRequest,
ErrorType::UnprocessableEntity => Status::UnprocessableEntity,
ErrorType::DeserializationError { .. } => Status::UnprocessableEntity,
ErrorType::FailedValidation { .. } => Status::UnprocessableEntity,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::MissingScope { .. } => Status::Unauthorized,
ErrorType::ExpiredToken => Status::Unauthorized,
ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity,
ErrorType::FileTooLarge { .. } => Status::UnprocessableEntity,
ErrorType::FileTypeNotAllowed => Status::BadRequest,
ErrorType::ImageProcessingFailed => Status::InternalServerError,
ErrorType::NoEmbedData => Status::BadRequest,
};
}
}
}
/// HTTP response builder for Error enum
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
// Serialize the error data structure into JSON.
let string = serde_json::to_string(&self).unwrap();
@@ -102,7 +107,7 @@ impl<'r> Responder<'r, 'static> for Error {
Response::build()
.sized_body(string.len(), Cursor::new(string))
.header(ContentType::new("application", "json"))
.status(status)
.status(self.rocket_status())
.ok()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-crond"
version = "0.8.9"
version = "0.8.8"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" }
# Core
revolt-database = { version = "0.8.9", path = "../../core/database" }
revolt-result = { version = "0.8.9", path = "../../core/result" }
revolt-config = { version = "0.8.9", path = "../../core/config" }
revolt-files = { version = "0.8.9", path = "../../core/files" }
revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config" }
revolt-files = { version = "0.8.8", path = "../../core/files" }

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

@@ -1,23 +1,32 @@
use revolt_config::configure;
use revolt_database::DatabaseInfo;
use revolt_result::Result;
use tasks::{file_deletion, prune_dangling_files, prune_members};
use tokio::try_join;
use std::{future::Future, time::Duration};
use crate::tasks::prune_authorized_bots;
use revolt_config::{configure, capture_error};
use revolt_database::{Database, DatabaseInfo};
use revolt_result::Result;
use tasks::{file_deletion, prune_dangling_files};
use tokio::{join, time::sleep};
pub mod tasks;
pub async fn cron_task_wrapper<Fut: Future<Output = Result<()>>>(func: fn(Database) -> Fut, db: Database) {
loop {
if let Err(error) = func(db.clone()).await {
log::error!("cron task failed unexpectidly: {error:?}\nRetrying after 60s");
capture_error(&error);
}
sleep(Duration::from_secs(60)).await;
}
}
#[tokio::main]
async fn main() -> Result<()> {
async fn main() {
configure!(crond);
let db = DatabaseInfo::Auto.connect().await.expect("database");
try_join!(
file_deletion::task(db.clone()),
prune_dangling_files::task(db.clone()),
prune_authorized_bots::task(db.clone()),
prune_members::task(db.clone())
)
.map(|_| ())
join!(
cron_task_wrapper(file_deletion::task, db.clone()),
cron_task_wrapper(prune_dangling_files::task, db.clone()),
);
}

View File

@@ -1,4 +1,2 @@
pub mod file_deletion;
pub mod prune_dangling_files;
pub mod prune_authorized_bots;
pub mod prune_members;

View File

@@ -1,22 +0,0 @@
use revolt_database::{iso8601_timestamp::{Duration, Timestamp}, util::oauth2::TokenType, Database};
use revolt_result::Result;
use tokio::time::sleep;
pub async fn task(db: Database) -> Result<()> {
let lifetime = Duration::new(TokenType::Access.lifetime().num_seconds(), 0);
loop {
let deauthorized_bots = db.fetch_deauthorized_authorized_bots().await?;
for bot in deauthorized_bots {
if let Some(deauthorized_at) = &bot.deauthorized_at {
if deauthorized_at.saturating_add(lifetime) < Timestamp::now_utc() {
db.delete_authorized_bot(&bot.id).await?;
};
};
};
// 1 hour
sleep(std::time::Duration::from_secs(60 * 60)).await;
}
}

View File

@@ -1,18 +0,0 @@
use std::time::Duration;
use log::warn;
use revolt_database::Database;
use revolt_result::Result;
use tokio::time::sleep;
pub async fn task(db: Database) -> Result<()> {
loop {
let success = db.remove_dangling_members().await;
if let Err(s) = success {
revolt_config::capture_error(&s);
warn!("Failed to prune dangling members: {:?}", &s);
}
sleep(Duration::from_secs(90)).await;
}
}

View File

@@ -1,20 +1,20 @@
[package]
name = "revolt-pushd"
version = "0.8.9"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
revolt-result = { version = "0.8.9", path = "../../core/result" }
revolt-config = { version = "0.8.9", path = "../../core/config", features = [
revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config", features = [
"report-macros",
"anyhow"
] }
revolt-database = { version = "0.8.9", path = "../../core/database" }
revolt-models = { version = "0.8.9", path = "../../core/models", features = [
revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-models = { version = "0.8.8", path = "../../core/models", features = [
"validator",
] }
revolt-presence = { version = "0.8.9", path = "../../core/presence", features = [
revolt-presence = { version = "0.8.8", path = "../../core/presence", features = [
"redis-is-patched",
] }

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

@@ -123,24 +123,26 @@ impl AsyncConsumer for AckConsumer {
token: session.subscription.as_ref().unwrap().auth.clone(),
extras: Default::default(),
};
let raw_service_payload = serde_json::to_string(&service_payload);
if let Ok(p) = raw_service_payload {
let args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
match serde_json::to_string(&service_payload) {
Ok(p) => {
let args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
publish_message(self, p.into(), args).await;
} else {
log::warn!("Failed to serialize ack badge update payload!");
revolt_config::capture_error(&raw_service_payload.unwrap_err());
publish_message(self, p.into(), args).await;
},
Err(e) => {
log::warn!("Failed to serialize ack badge update payload!");
revolt_config::capture_error(&e);
}
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.8.9"
version = "0.8.8"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -79,12 +79,8 @@ revolt-models = { path = "../core/models", features = [
"rocket",
] }
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi", "sentry"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
# oauth2
pkce = "0.2.0"
[build-dependencies]
vergen = "7.5.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

@@ -11,7 +11,6 @@ pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::AMQP;
use revolt_ratelimits::rocket as ratelimiter;
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
@@ -123,23 +122,20 @@ pub async fn web() -> Rocket<Build> {
let rocket = rocket::build();
let prometheus = PrometheusMetrics::new();
// Ratelimits
let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits);
routes::mount(config, rocket)
.attach(prometheus.clone())
.mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/", ratelimiter::routes())
.mount("/", util::ratelimiter::routes())
.mount("/swagger/", swagger)
.mount("/0.8/swagger/", swagger_0_8)
.manage(authifier)
.manage(db)
.manage(amqp)
.manage(cors.clone())
.manage(ratelimits)
.attach(ratelimiter::RatelimitFairing)
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)
.register("/", util::catchers::all_catchers())
.configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
address: Ipv4Addr::new(0, 0, 0, 0).into(),

View File

@@ -1,9 +1,8 @@
use crate::util::json::{Json, Validate};
use revolt_database::{Bot, Database, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use revolt_result::Result;
use rocket::State;
use validator::Validate;
/// # Create Bot
///
@@ -13,14 +12,9 @@ use validator::Validate;
pub async fn create_bot(
db: &State<Database>,
user: User,
info: Json<v0::DataCreateBot>,
info: Validate<Json<v0::DataCreateBot>>,
) -> Result<Json<v0::BotWithUserResponse>> {
let info = info.into_inner();
info.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let info = info.into_inner().into_inner();
let (bot, user) = Bot::create(db, info.name, &user, None).await?;
Ok(Json(v0::BotWithUserResponse {

View File

@@ -3,8 +3,7 @@ use revolt_models::v0::{self, DataEditBot};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket::serde::json::Json;
use validator::Validate;
use crate::util::json::{Json, Validate};
/// # Edit Bot
///
@@ -15,14 +14,9 @@ pub async fn edit_bot(
db: &State<Database>,
user: User,
target: Reference<'_>,
data: Json<DataEditBot>,
data: Validate<Json<DataEditBot>>,
) -> Result<Json<v0::BotWithUserResponse>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let mut bot = target.as_bot(db).await?;
if bot.owner != user.id {
@@ -37,7 +31,6 @@ pub async fn edit_bot(
if data.public.is_none()
&& data.analytics.is_none()
&& data.interactions_url.is_none()
&& data.oauth2.is_none()
&& data.remove.is_empty()
{
return Ok(Json(v0::BotWithUserResponse {
@@ -50,43 +43,17 @@ pub async fn edit_bot(
public,
analytics,
interactions_url,
oauth2,
remove,
..
} = data;
let mut partial = PartialBot {
let partial = PartialBot {
public,
analytics,
interactions_url,
..Default::default()
};
if let Some(edit_oauth2) = oauth2 {
let mut oauth2 = bot.oauth2.clone().unwrap_or_default();
if let Some(public) = edit_oauth2.public {
if oauth2.public && !public {
oauth2.secret = Some(nanoid::nanoid!(64))
} else if !oauth2.public && public {
oauth2.secret = None;
};
oauth2.public = public;
}
oauth2.redirects = edit_oauth2.redirects.unwrap_or(oauth2.redirects);
oauth2.allowed_scopes = edit_oauth2.allowed_scopes
.map(|scopes|scopes
.into_iter()
.map(|(scope, value)| (scope.into(), value.into()))
.collect()
)
.unwrap_or(oauth2.allowed_scopes);
partial.oauth2 = Some(oauth2)
}
bot.update(
db,
partial,

View File

@@ -1,7 +1,8 @@
use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::FetchBotResponse;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Bot
///

View File

@@ -2,7 +2,7 @@ use futures::future::join_all;
use revolt_database::{Database, User};
use revolt_models::v0::OwnedBotsResponse;
use revolt_result::Result;
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket::State;
/// # Fetch Owned Bots

View File

@@ -2,7 +2,7 @@ use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::PublicBot;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket::State;
/// # Fetch Public Bot

View File

@@ -8,7 +8,7 @@ use revolt_permissions::{
use revolt_result::{create_error, Result};
use rocket::State;
use rocket::serde::json::Json;
use crate::util::json::Json;
use rocket_empty::EmptyResponse;
/// # Invite Bot
@@ -168,8 +168,8 @@ mod test {
.await;
match event {
EventV1::ServerMemberJoin { member, .. } => {
assert_eq!(bot.id, member.id.user);
EventV1::ServerMemberJoin { user, .. } => {
assert_eq!(bot.id, user);
}
_ => unreachable!(),
}

View File

@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
use rocket::State;
use crate::util::json::{Json, Validate};
/// # Edit Channel
///
@@ -18,14 +18,9 @@ pub async fn edit(
amqp: &State<AMQP>,
user: User,
target: Reference<'_>,
data: Json<v0::DataEditChannel>,
data: Validate<Json<v0::DataEditChannel>>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let data = data.into_inner().into_inner();
let mut channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);

View File

@@ -6,7 +6,8 @@ use revolt_database::{
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
use rocket::State;
use crate::util::json::Json;
/// # Fetch Channel
///

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