Compare commits

..
239 changed files with 33770 additions and 1879 deletions

No files matched your search

-37
View File
@@ -1,37 +0,0 @@
name: Build documentation
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pages: write # To push to a GitHub Pages site
id-token: write # To update the deployment status
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Build Book
run: |
cd doc
mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "doc/book"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+4 -2
View File
@@ -5,8 +5,6 @@ on:
tags: tags:
- "*" - "*"
pull_request: pull_request:
branches:
- "main"
paths: paths:
- "Dockerfile" - "Dockerfile"
@@ -14,6 +12,10 @@ permissions:
contents: read contents: read
packages: write packages: write
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
base: base:
name: Test base image build name: Test base image build
+29
View File
@@ -0,0 +1,29 @@
name: Documentation (test)
on:
pull_request:
jobs:
test-deploy:
name: Test deployment
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
package_json_file: ./docs/package.json
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
cache-dependency-path: ./docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Test build website
run: pnpm run build
+54
View File
@@ -0,0 +1,54 @@
name: Documentation
on:
push:
branches:
- main
jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
package_json_file: ./docs/package.json
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
cache-dependency-path: ./docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Build website
run: pnpm run build
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./docs/build
deploy:
name: Deploy to GitHub Pages
needs: build
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+12 -12
View File
@@ -2,16 +2,12 @@ name: Rust build, test, and generate specification
on: on:
push: push:
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
branches: [main] branches: [main]
pull_request:
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -22,6 +18,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Free up disk space
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
- name: Install latest stable - name: Install latest stable
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@@ -32,7 +32,7 @@ jobs:
uses: baptiste0928/cargo-install@v1 uses: baptiste0928/cargo-install@v1
with: with:
crate: cargo-nextest crate: cargo-nextest
locked: true args: --locked
- name: Run cargo build - name: Run cargo build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -73,9 +73,9 @@ jobs:
if: github.event_name != 'pull_request' && github.ref_name == 'main' if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: stoatchat/api repository: stoatchat/javascript-client-api
path: api path: api
token: ${{ secrets.PAT }} ssh-key: ${{ secrets.DEPLOY_KEY_JAVASCRIPT_CLIENT_API }}
- name: Download OpenAPI specification - name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'main' if: github.event_name != 'pull_request' && github.ref_name == 'main'
-54
View File
@@ -1,54 +0,0 @@
name: Add Issue to Board
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "stoatchat"){
projectV2(number: 3) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
-79
View File
@@ -1,79 +0,0 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "stoatchat"){
projectV2(number: 5) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: {
singleSelectOptionId: $status_value
}
}) {
projectV2Item {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
+20
View File
@@ -0,0 +1,20 @@
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-1
View File
@@ -13,4 +13,3 @@ venv/
livekit.yml livekit.yml
.idea .idea
start
Generated
+313 -194
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -11,8 +11,9 @@ members = [
[patch.crates-io] [patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" } redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" } authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" } rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
iso8601-timestamp = { path = "../iso8601-timestamp" }
# I'm 99% sure this is overloading the GitHub worker # I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just # hence builds have been failing since, let's just
+1 -1
View File
@@ -54,7 +54,7 @@ use_starttls = false
url = "http://livekit" url = "http://livekit"
lat = 0.0 lat = 0.0
lon = 0.0 lon = 0.0
key = "worldwide_key" key = "worldwide"
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0" secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
[files.s3] [files.s3]
-73
View File
@@ -1,73 +0,0 @@
# Code Style Guide
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate
w.r.t. `model.rs` files
- All struct definitions must be commented.
```rust
/// Server
pub struct Server {
/// Name of the server
pub name: String,
```
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
```rust
auto_derived_partial!(
/// Server
pub struct Server { .. },
"PartialServer"
);
```
- `auto_derived!` macro accepts multiple entries and should be used as such:
```rust
auto_derived!(
/// Optional fields on server object
pub enum FieldsServer { .. }
/// Optional fields on server object
pub enum FieldsRole { .. }
);
```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust
#[serde(skip_serializing_if = "crate::if_false", default)]
```
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
w.r.t. `ops` module for models
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
- Traits defined must follow these guidelines:
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
```rust
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
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>;
/// Update information for a server member
async fn update_member(&self, .. ) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}
```
- There should be an empty line break between each method declaration.
- All methods must have an appropriate comment.
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.
+7
View File
@@ -64,3 +64,10 @@ services:
MAILDEV_WEB_PORT: 8080 MAILDEV_WEB_PORT: 8080
MAILDEV_INCOMING_USER: smtp MAILDEV_INCOMING_USER: smtp
MAILDEV_INCOMING_PASS: smtp MAILDEV_INCOMING_PASS: smtp
livekit:
image: ghcr.io/stoatchat/livekit-server:v1.9.9
command: --config /etc/livekit.yml
network_mode: "host"
volumes:
- ./livekit.yml:/etc/livekit.yml
+1 -1
View File
@@ -41,7 +41,7 @@ authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result" } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" } revolt-database = { path = "../core/database", features = ["voice"] }
revolt-permissions = { version = "0.8.9", path = "../core/permissions" } revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
+53 -29
View File
@@ -1,4 +1,4 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use futures::future::join_all; use futures::future::join_all;
use revolt_database::{ use revolt_database::{
@@ -124,12 +124,7 @@ impl State {
.unwrap_or_default(); .unwrap_or_default();
// Fetch all memberships with their corresponding servers. // Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?; let mut members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect(); let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?; let servers = db.fetch_servers(&server_ids).await?;
@@ -158,6 +153,51 @@ impl State {
} }
} }
let voice_states = if fields.voice_states {
let mut voice_state_server_members: HashMap<String, HashSet<String>> = HashMap::new();
// fetch voice states for all the channels we can see
let mut voice_states = Vec::new();
for channel in channels.iter().filter(|c| {
matches!(
c,
Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
if let Some(server) = channel.server() {
let set = voice_state_server_members.entry(server.to_string()).or_default();
for participant in &voice_state.participants {
user_ids.insert(participant.id.clone());
set.insert(participant.id.clone());
}
} else {
for participant in &voice_state.participants {
user_ids.insert(participant.id.clone());
}
}
voice_states.push(voice_state);
}
}
// Fetch all the members for for the participants who are in a server
for (server, user_ids) in voice_state_server_members {
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
let voice_members = db.fetch_members(&server, &user_ids).await?;
members.extend(voice_members);
}
Some(voice_states)
} else {
None
};
// Fetch presence data for known users. // Fetch presence data for known users.
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await; let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
@@ -171,6 +211,12 @@ impl State {
) )
.await?; .await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Fetch customisations. // Fetch customisations.
let emojis = if fields.emojis { let emojis = if fields.emojis {
Some( Some(
@@ -212,28 +258,6 @@ impl State {
None None
}; };
let voice_states = if fields.voice_states {
// fetch voice states for all the channels we can see
let mut voice_states = Vec::new();
for channel in channels.iter().filter(|c| {
matches!(
c,
Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
Some(voice_states)
} else {
None
};
// Copy data into local state cache. // Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect(); self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache self.cache
+1 -14
View File
@@ -1,9 +1,7 @@
use std::{env, sync::Arc}; use std::env;
use async_std::net::TcpListener; use async_std::net::TcpListener;
use revolt_presence::clear_region; use revolt_presence::clear_region;
use once_cell::sync::OnceCell;
use revolt_database::voice::VoiceClient;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@@ -14,15 +12,6 @@ pub mod events;
mod database; mod database;
mod websocket; mod websocket;
pub static VOICE_CLIENT: OnceCell<Arc<VoiceClient>> = OnceCell::new();
pub fn get_voice_client() -> Arc<VoiceClient> {
VOICE_CLIENT
.get()
.expect("get_voice_client called before set")
.clone()
}
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
// Configure requirements for Bonfire. // Configure requirements for Bonfire.
@@ -35,8 +24,6 @@ async fn main() {
clear_region(None).await; clear_region(None).await;
} }
VOICE_CLIENT.set(Arc::new(VoiceClient::from_revolt_config().await)).unwrap();
// Setup a TCP listener to accept WebSocket connections on. // Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 14703 on all interfaces. // By default, we bind to port 14703 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into()); let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
/// # Coalescion service /// Coalescion service
/// ///
/// See module description for example usage. /// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> { pub struct CoalescionService<Id: Hash + Clone + Eq> {
+1 -1
View File
@@ -94,7 +94,7 @@ message_queue = "notifications.origin.message"
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received fr_received_queue = "notifications.ingest.fr_received" # friend request received
dm_call_queue = "notifications.ingest.dm_call" # friend request received dm_call_queue = "notifications.ingest.dm_call" # direct message voice call
generic_queue = "notifications.ingest.generic" # generic messages (title + body) generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices ack_queue = "notifications.process.ack" # updates badges for apple devices
+11 -10
View File
@@ -15,9 +15,10 @@ mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
# ... Other # ... Other
tasks = ["isahc", "linkify", "url-escape"] tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"] async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"] rocket-impl = ["rocket"]
axum-impl = ["axum"] axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"] redis-is-patched = ["revolt-presence/redis-is-patched"]
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
# Default Features # Default Features
default = ["mongodb", "async-std-runtime", "tasks"] default = ["mongodb", "async-std-runtime", "tasks"]
@@ -46,7 +47,7 @@ ulid = "1.0.0"
nanoid = "0.4.0" nanoid = "0.4.0"
base64 = "0.21.3" base64 = "0.21.3"
once_cell = "1.17" once_cell = "1.17"
indexmap = "1.9.1" indexmap = "2.12.0"
decancer = "1.6.2" decancer = "1.6.2"
deadqueue = "0.2.4" deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" } linkify = { optional = true, version = "0.8.1" }
@@ -58,7 +59,7 @@ isahc = { optional = true, version = "1.7", features = ["json"] }
serde_json = "1" serde_json = "1"
revolt_optional_struct = "0.2.0" revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } iso8601-timestamp = { version = "0.4.0", features = ["serde", "bson"] }
# Events # Events
redis-kiss = { version = "0.1.4" } redis-kiss = { version = "0.1.4" }
@@ -81,15 +82,15 @@ async-recursion = "1.0.4"
async-std = { version = "1.8.0", features = ["attributes"], optional = true } async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Axum Impl # Axum Impl
axum = { version = "0.7.5", optional = true } axum = { version = "0.8.6", optional = true }
# Rocket Impl # Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.1", default-features = false, features = [ rocket = { version = "0.5.1", default-features = false, features = [
"json", "json",
], optional = true } ], optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true } # Openapi Schema
utoipa = { version = "5.4.0", optional = true }
# Authifier # Authifier
authifier = { version = "1.0.15" } authifier = { version = "1.0.15" }
@@ -98,6 +99,6 @@ authifier = { version = "1.0.15" }
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
# Voice # Voice
livekit-api = "0.4.4" livekit-api = { version = "=0.4.4", optional = true}
livekit-protocol = "0.4.0" livekit-protocol = { version = "=0.4.0", optional = true }
livekit-runtime = { version = "0.3.1", features = ["tokio"] } livekit-runtime = { version = "=0.3.1", features = ["tokio"], optional = true }
+3 -1
View File
@@ -295,7 +295,9 @@ pub enum EventV1 {
}, },
UserMoveVoiceChannel { UserMoveVoiceChannel {
node: String, node: String,
token: String from: String,
to: String,
token: String,
} }
} }
+8 -3
View File
@@ -25,6 +25,10 @@ pub use mongodb;
#[macro_use] #[macro_use]
extern crate bson; extern crate bson;
#[cfg(feature = "utoipa")]
#[macro_use]
extern crate utoipa;
#[cfg(not(feature = "async-std-runtime"))] #[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled."); compile_error!("async-std-runtime feature must be enabled.");
@@ -60,7 +64,7 @@ macro_rules! database_derived {
macro_rules! auto_derived { macro_rules! auto_derived {
( $( $item:item )+ ) => { ( $( $item:item )+ ) => {
$( $(
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
$item $item
)+ )+
}; };
@@ -68,8 +72,8 @@ macro_rules! auto_derived {
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
#[opt_some_priority] #[opt_some_priority]
@@ -112,6 +116,7 @@ pub mod tasks;
mod amqp; mod amqp;
pub use amqp::amqp::AMQP; pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice; pub mod voice;
@@ -25,7 +25,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 48; // MUST BE +1 to last migration pub const LATEST_REVISION: i32 = 49; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -1246,8 +1246,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update voice channels"); .expect("Failed to update voice channels");
}; };
if revision <= 47 { if revision <= 48 {
info!("Running migration [revision 47 / 29-04-2025]: Add Video to default permissions"); info!("Running migration [revision 48 / 22-10-2025]: Add Video + Listen to default permissions");
db.col::<Document>("servers") db.col::<Document>("servers")
.update_many( .update_many(
@@ -1255,7 +1255,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! { doc! {
"$bit": { "$bit": {
"default_permissions": { "default_permissions": {
"or": ChannelPermission::Video as i64 "or": (ChannelPermission::Video + ChannelPermission::Speak + ChannelPermission::Listen) as i64
}, },
} }
} }
@@ -14,7 +14,7 @@ auto_derived!(
} }
/// Composite primary key consisting of channel and user id /// Composite primary key consisting of channel and user id
#[derive(Hash)] #[derive(Hash, Eq)]
pub struct ChannelCompositeKey { pub struct ChannelCompositeKey {
/// Channel Id /// Channel Id
pub channel: String, pub channel: String,
@@ -443,7 +443,8 @@ impl Message {
} }
// Verify replies are valid. // Verify replies are valid.
let mut replies = HashSet::new(); let mut replies = Vec::new();
if let Some(entries) = data.replies { if let Some(entries) = data.replies {
if entries.len() > config.features.limits.global.message_replies { if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies { return Err(create_error!(TooManyReplies {
@@ -451,6 +452,8 @@ impl Message {
})); }));
} }
replies.reserve(entries.len());
for ReplyIntent { for ReplyIntent {
id, id,
mention, mention,
@@ -464,7 +467,12 @@ impl Message {
user_mentions.insert(message.author.to_owned()); user_mentions.insert(message.author.to_owned());
} }
replies.insert(message.id); // 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);
}
} }
// If the referenced message doesn't exist and fail_if_not_exists // If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply. // is set to false, send the message without the reply.
@@ -537,9 +545,7 @@ impl Message {
} }
if !replies.is_empty() { if !replies.is_empty() {
message message.replies.replace(replies);
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
} }
// Calculate final message flags // Calculate final message flags
@@ -247,7 +247,7 @@ impl AbstractMessages for ReferenceDb {
let mut messages = self.messages.lock().await; let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) { if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) { if let Some(users) = message.reactions.get_mut(emoji) {
users.remove(&user.to_string()); users.swap_remove(&user.to_string());
} }
Ok(()) Ok(())
@@ -260,7 +260,7 @@ impl AbstractMessages for ReferenceDb {
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> { async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
let mut messages = self.messages.lock().await; let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) { if let Some(message) = messages.get_mut(id) {
message.reactions.remove(emoji); message.reactions.swap_remove(emoji);
Ok(()) Ok(())
} else { } else {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
@@ -1,7 +1,6 @@
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::voice::get_channel_voice_state;
use crate::{ use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
@@ -56,7 +55,7 @@ auto_derived_partial!(
auto_derived!( auto_derived!(
/// Composite primary key consisting of server and user id /// Composite primary key consisting of server and user id
#[derive(Hash, Default)] #[derive(Hash, Default, Eq)]
pub struct MemberCompositeKey { pub struct MemberCompositeKey {
/// Server Id /// Server Id
pub server: String, pub server: String,
@@ -148,10 +147,12 @@ impl Member {
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
#[allow(unused_mut)]
let mut voice_states = Vec::new(); let mut voice_states = Vec::new();
#[cfg(feature = "voice")]
for channel in &channels { for channel in &channels {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await { if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(channel).await {
voice_states.push(voice_state) voice_states.push(voice_state)
} }
} }
@@ -336,8 +336,8 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Nickname => "nickname", FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles", FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout", FieldsMember::Timeout => "timeout",
FieldsMember::CanPublish => "is_publishing", FieldsMember::CanPublish => "can_publish",
FieldsMember::CanReceive => "is_receiving", FieldsMember::CanReceive => "can_receive",
}) })
} }
} }
@@ -4,7 +4,6 @@ use revolt_result::{create_error, Error, Result};
use crate::{Database, User}; use crate::{Database, User};
#[async_trait::async_trait]
impl<S> FromRequestParts<S> for User impl<S> FromRequestParts<S> for User
where where
Database: FromRef<S>, Database: FromRef<S>,
@@ -4,8 +4,6 @@ mod model;
mod ops; mod ops;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
mod rocket; mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
pub use model::*; pub use model::*;
pub use ops::*; pub use ops::*;
@@ -15,7 +15,7 @@ use serde_json::json;
use ulid::Ulid; use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// User
pub struct User { pub struct User {
/// Unique Id /// Unique Id
#[serde(rename = "_id")] #[serde(rename = "_id")]
@@ -1,31 +0,0 @@
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
};
use crate::User;
impl OpenApiFromRequest<'_> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ use super::DelayedTask;
use crate::Channel::TextChannel; use crate::Channel::TextChannel;
/// Enumeration of possible events /// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, PartialEq)]
pub enum AckEvent { pub enum AckEvent {
/// Add mentions for a channel /// Add mentions for a channel
ProcessMessage { ProcessMessage {
+47 -49
View File
@@ -10,16 +10,16 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IdempotencyKey { #[cfg_attr(feature = "utoipa", derive(IntoParams))]
key: String, #[cfg_attr(feature = "utoipa", into_params(names("Idempotency-Key"), parameter_in=Header))]
} pub struct IdempotencyKey(String);
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap()))); Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
impl IdempotencyKey { impl IdempotencyKey {
pub fn unchecked_from_string(key: String) -> Self { pub fn unchecked_from_string(key: String) -> Self {
Self { key } Self(key)
} }
// Backwards compatibility. // Backwards compatibility.
@@ -32,56 +32,56 @@ impl IdempotencyKey {
} }
cache.put(v.clone(), ()); cache.put(v.clone(), ());
self.key = v; self.0 = v;
} }
Ok(()) Ok(())
} }
pub fn into_key(self) -> String { pub fn into_key(self) -> String {
self.key self.0
} }
} }
#[cfg(feature = "rocket-impl")] // #[cfg(feature = "rocket-impl")]
use revolt_rocket_okapi::{ // use revolt_rocket_okapi::{
gen::OpenApiGenerator, // gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput}, // request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{Parameter, ParameterValue}, // revolt_okapi::openapi3::{Parameter, ParameterValue},
}; // };
#[cfg(feature = "rocket-impl")] // #[cfg(feature = "rocket-impl")]
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; // use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")] // #[cfg(feature = "rocket-impl")]
impl OpenApiFromRequest<'_> for IdempotencyKey { // impl OpenApiFromRequest<'_> for IdempotencyKey {
fn from_request_input( // fn from_request_input(
_gen: &mut OpenApiGenerator, // _gen: &mut OpenApiGenerator,
_name: String, // _name: String,
_required: bool, // _required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> { // ) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::Parameter(Parameter { // Ok(RequestHeaderInput::Parameter(Parameter {
name: "Idempotency-Key".to_string(), // name: "Idempotency-Key".to_string(),
description: Some("Unique key to prevent duplicate requests".to_string()), // description: Some("Unique key to prevent duplicate requests".to_string()),
allow_empty_value: false, // allow_empty_value: false,
required: false, // required: false,
deprecated: false, // deprecated: false,
extensions: schemars::Map::new(), // extensions: schemars::Map::new(),
location: "header".to_string(), // location: "header".to_string(),
value: ParameterValue::Schema { // value: ParameterValue::Schema {
allow_reserved: false, // allow_reserved: false,
example: None, // example: None,
examples: None, // examples: None,
explode: None, // explode: None,
style: None, // style: None,
schema: SchemaObject { // schema: SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), // instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default() // ..Default::default()
}, // },
}, // },
})) // }))
} // }
} // }
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
use rocket::{ use rocket::{
@@ -110,18 +110,16 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
)); ));
} }
let idempotency = IdempotencyKey { key }; let idempotency = IdempotencyKey(key);
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.key).is_some() { if cache.get(&idempotency.0).is_some() {
return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce))); return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
} }
cache.put(idempotency.key.clone(), ()); cache.put(idempotency.0.clone(), ());
return Outcome::Success(idempotency); return Outcome::Success(idempotency);
} }
Outcome::Success(IdempotencyKey { Outcome::Success(IdempotencyKey(ulid::Ulid::new().to_string()))
key: ulid::Ulid::new().to_string(),
})
} }
} }
+2
View File
@@ -5,5 +5,7 @@ pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
pub mod test_fixtures; pub mod test_fixtures;
#[cfg(feature = "utoipa")]
pub mod utoipa;
pub use funcs::*; pub use funcs::*;
@@ -3,11 +3,6 @@ use std::str::FromStr;
use revolt_result::Result; use revolt_result::Result;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
use rocket::request::FromParam; use rocket::request::FromParam;
#[cfg(feature = "rocket-impl")]
use schemars::{
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
JsonSchema,
};
use crate::{ use crate::{
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook, Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
@@ -112,17 +107,3 @@ impl<'r> FromParam<'r> for Reference<'r> {
Ok(Reference::from_unchecked(param)) Ok(Reference::from_unchecked(param))
} }
} }
#[cfg(feature = "rocket-impl")]
impl<'a> JsonSchema for Reference<'a> {
fn schema_name() -> String {
"Id".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default()
})
}
}
+49
View File
@@ -0,0 +1,49 @@
use utoipa::{
openapi::{
schema::SchemaType,
security::{ApiKey, ApiKeyValue, SecurityScheme},
ObjectBuilder, OpenApi, RefOr, Schema, Type,
},
Modify, PartialSchema, ToSchema,
};
use crate::util::reference::Reference;
pub struct TokenSecurity;
impl Modify for TokenSecurity {
fn modify(&self, openapi: &mut OpenApi) {
let components = openapi.components.get_or_insert_default();
components.add_security_scheme(
"Session-Token",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
"X-Session-Token".to_string(),
))),
);
components.add_security_scheme(
"Bot-Token",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Bot-Ticket".to_string()))),
);
}
}
impl ToSchema for Reference<'_> {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Reference")
}
}
impl PartialSchema for Reference<'_> {
fn schema() -> RefOr<Schema> {
RefOr::T(
ObjectBuilder::new()
.description(Some("An id referencing a stoat model."))
.schema_type(SchemaType::Type(Type::String))
.examples(["01FD58YK5W7QRV5H3D64KTQYX3"])
.build()
.into(),
)
}
}
+33 -1
View File
@@ -16,7 +16,7 @@ mod voice_client;
pub use voice_client::VoiceClient; pub use voice_client::VoiceClient;
async fn get_connection() -> Result<Conn> { async fn get_connection() -> Result<Conn> {
_get_connection().await.to_internal_error() _get_connection().await.map_err(|_| create_error!(InternalError))
} }
pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> { pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
@@ -564,3 +564,35 @@ pub async fn get_call_notification_recipients(
.await .await
.to_internal_error() .to_internal_error()
} }
pub async fn remove_user_from_voice_channels(db: &Database, voice_client: &VoiceClient, user_id: &str) -> Result<()> {
for channel_id in get_user_voice_channels(user_id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, user_id).await?;
};
Ok(())
}
pub async fn remove_user_from_voice_channel(db: &Database, voice_client: &VoiceClient, channel_id: &str, user_id: &str) -> Result<()> {
if let Some(node) = get_channel_node(channel_id).await? {
let _ = voice_client.remove_user(&node, user_id, channel_id).await;
}
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
Ok(())
}
pub async fn delete_voice_channel(voice_client: &VoiceClient, channel_id: &str, server_id: Option<&str>) -> Result<()> {
if let Some(users) = get_voice_channel_members(channel_id).await? {
let node = get_channel_node(channel_id).await?.unwrap();
voice_client.delete_room(&node, channel_id).await?;
delete_channel_voice_state(channel_id, server_id, &users).await?;
};
Ok(())
}
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting"> <meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> <img alt="Stoat Logo" class="w-24" src="https://stoat.chat/favicon.svg" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -107,7 +107,7 @@
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br> This email is intended for {{email}}<br>
Sent from Revolt<br> Sent from Stoat<br>
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,17 +1,15 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style> <style>
/* Add custom classes and styles that you want inlined here */ /* Add custom classes and styles that you want inlined here */
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container"> <div class="container">
<img <img alt="Stoat Logo" class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" />
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4"> <div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Deletion</h1> <h1 class="h3 fw-700">Account Deletion</h1>
<p> <p>
@@ -22,7 +20,7 @@
</div> </div>
<div class="text-muted text-center my-6"> <div class="text-muted text-center my-6">
This email is intended for {{email}}<br /> This email is intended for {{email}}<br />
Sent from Revolt<br /> Sent from Stoat<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center"> <div class="text-muted text-xs my-6 ax-center">
@@ -35,4 +33,5 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@ You requested to have your account deleted, if you did not perform this action p
Please navigate to: {{url}} Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Stoat
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales. Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
@@ -4,6 +4,6 @@ Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd. This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here: Learn more about third party instances here:
https://developers.revolt.chat/faq.html https://developers.stoat.chat/faq.html
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting"> <meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> <img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -120,7 +120,7 @@
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br> This email is intended for {{email}}<br>
Sent from Revolt<br> Sent from Stoat<br>
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -9,7 +9,8 @@
<div class="container"> <div class="container">
<img <img
class="ax-center my-10 w-24" class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png" src="https://stoat.chat/favicon.svg"
alt="Stoat Logo"
/> />
<div class="card p-6 p-lg-10 space-y-4"> <div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1> <h1 class="h3 fw-700">Password Reset</h1>
@@ -25,7 +26,7 @@
</div> </div>
<div class="text-muted text-center my-6"> <div class="text-muted text-center my-6">
This email is intended for {{email}}<br /> This email is intended for {{email}}<br />
Sent from Revolt<br /> Sent from Stoat<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center"> <div class="text-muted text-xs my-6 ax-center">
@@ -8,7 +8,7 @@ password on it, click below to continue.
Please navigate to: {{url}} Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Stoat
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales. Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting"> <meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> <img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -104,7 +104,7 @@
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br> This email is intended for {{email}}<br>
Sent from Revolt<br> Sent from Stoat<br>
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,16 +1,15 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style> <style>
/* Add custom classes and styles that you want inlined here */ /* Add custom classes and styles that you want inlined here */
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container"> <div class="container">
<img <img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4"> <div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1> <h1 class="h3 fw-700">Password Reset</h1>
<p>You requested a password reset, click below to continue.</p> <p>You requested a password reset, click below to continue.</p>
@@ -18,7 +17,7 @@
</div> </div>
<div class="text-muted text-center my-6"> <div class="text-muted text-center my-6">
This email is intended for {{email}}<br /> This email is intended for {{email}}<br />
Sent from Revolt<br /> Sent from Stoat<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center"> <div class="text-muted text-xs my-6 ax-center">
@@ -31,4 +30,5 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -3,7 +3,7 @@ You requested a password reset, if you did not perform this action you can safel
Please navigate to: {{url}} Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Stoat
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales. Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting"> <meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> <img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -134,7 +134,7 @@
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br> This email is intended for {{email}}<br>
Sent from Revolt<br> Sent from Stoat<br>
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,17 +1,15 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style> <style>
/* Add custom classes and styles that you want inlined here */ /* Add custom classes and styles that you want inlined here */
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container"> <div class="container">
<img <img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4"> <div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Suspended</h1> <h1 class="h3 fw-700">Account Suspended</h1>
<p>Your account has been suspended, for one or more reasons:</p> <p>Your account has been suspended, for one or more reasons:</p>
@@ -30,7 +28,7 @@
</div> </div>
<div class="text-muted text-center my-6"> <div class="text-muted text-center my-6">
This email is intended for {{email}}<br /> This email is intended for {{email}}<br />
Sent from Revolt<br /> Sent from Stoat<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center"> <div class="text-muted text-xs my-6 ax-center">
@@ -43,4 +41,5 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
@@ -8,7 +8,7 @@ Further violations may result in a permanent ban depending on severity, please a
Ban evasion is prohibited and will be dealt with accordingly. Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Stoat
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales. Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting"> <meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> <img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -104,7 +104,7 @@
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br> This email is intended for {{email}}<br>
Sent from Revolt<br> Sent from Stoat<br>
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,17 +1,15 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style> <style>
/* Add custom classes and styles that you want inlined here */ /* Add custom classes and styles that you want inlined here */
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<div class="container"> <div class="container">
<img <img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4"> <div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Almost there!</h1> <h1 class="h3 fw-700">Almost there!</h1>
<p>To complete your sign up, we just need to verify your email.</p> <p>To complete your sign up, we just need to verify your email.</p>
@@ -19,7 +17,7 @@
</div> </div>
<div class="text-muted text-center my-6"> <div class="text-muted text-center my-6">
This email is intended for {{email}}<br /> This email is intended for {{email}}<br />
Sent from Revolt<br /> Sent from Stoat<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center"> <div class="text-muted text-xs my-6 ax-center">
@@ -32,4 +30,5 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -4,7 +4,7 @@ To complete your sign up, we just need to verify your email.
Please navigate to: {{url}} Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Stoat
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales. Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+7 -8
View File
@@ -10,22 +10,22 @@ description = "Revolt Backend: API Models"
[features] [features]
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"] serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
utoipa = ["dep:utoipa"]
validator = ["dep:validator"] validator = ["dep:validator"]
rocket = ["dep:rocket"] rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"] partials = ["dep:revolt_optional_struct", "serde", "utoipa"]
default = ["serde", "partials", "rocket"] default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.9", path = "../config" } revolt-config = { version = "0.8.9", path = "../config" }
revolt-permissions = { version = "0.8.9", path = "../permissions" } revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
"utoipa",
] }
# Utility # Utility
regex = "1.11" regex = "1.11"
indexmap = "1.9.3" indexmap = "2.12.0"
once_cell = "1.17.1" once_cell = "1.17.1"
num_enum = "0.6.1" num_enum = "0.6.1"
@@ -35,11 +35,10 @@ rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
# Serialisation # Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true } revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] } iso8601-timestamp = { version = "0.4.0", features = ["utoipa", "bson"] }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] } utoipa = { version = "5.4.0", features = ["indexmap"], optional = true }
utoipa = { version = "4.2.3", optional = true }
# Validation # Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] } validator = { version = "0.16.0", optional = true, features = ["derive"] }
+4 -20
View File
@@ -2,10 +2,6 @@
#[macro_use] #[macro_use]
extern crate serde; extern crate serde;
#[cfg(feature = "schemas")]
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")] #[cfg(feature = "utoipa")]
#[macro_use] #[macro_use]
extern crate utoipa; extern crate utoipa;
@@ -21,9 +17,8 @@ macro_rules! auto_derived {
( $( $item:item )+ ) => { ( $( $item:item )+ ) => {
$( $(
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, PartialEq)]
$item $item
)+ )+
}; };
@@ -32,19 +27,8 @@ macro_rules! auto_derived {
#[cfg(feature = "partials")] #[cfg(feature = "partials")]
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive( #[derive(OptionalStruct, Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, #[optional_derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Default)]
)]
#[optional_derive(
Debug,
Clone,
Eq,
PartialEq,
Serialize,
Deserialize,
JsonSchema,
Default
)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
#[opt_some_priority] #[opt_some_priority]
@@ -55,7 +39,7 @@ macro_rules! auto_derived_partial {
#[cfg(not(feature = "partials"))] #[cfg(not(feature = "partials"))]
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
$item $item
}; };
} }
@@ -86,10 +86,10 @@ auto_derived!(
/// Information for the webhook /// Information for the webhook
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct CreateWebhookBody { pub struct CreateWebhookBody {
#[validate(length(min = 1, max = 32))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String, pub name: String,
#[validate(length(min = 1, max = 128))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>, pub avatar: Option<String>,
} }
); );
+1
View File
@@ -271,6 +271,7 @@ auto_derived!(
/// Options when deleting a channel /// Options when deleting a channel
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsChannelDelete { pub struct OptionsChannelDelete {
/// Whether to not send a leave message /// Whether to not send a leave message
pub leave_silently: Option<bool>, pub leave_silently: Option<bool>,
+78
View File
@@ -0,0 +1,78 @@
auto_derived!(
/// hCaptcha Configuration
pub struct CaptchaFeature {
/// Whether captcha is enabled
pub enabled: bool,
/// Client key used for solving captcha
pub key: String,
}
/// Generic Service Configuration
pub struct Feature {
/// Whether the service is enabled
pub enabled: bool,
/// URL pointing to the service
pub url: String,
}
/// # Information about a livekit node
pub struct VoiceNode {
pub name: String,
pub lat: f64,
pub lon: f64,
pub public_url: String,
}
/// # Voice Server Configuration
pub struct VoiceFeature {
/// Whether voice is enabled
pub enabled: bool,
/// All livekit nodes
pub nodes: Vec<VoiceNode>,
}
/// Feature Configuration
pub struct RevoltFeatures {
/// hCaptcha configuration
pub captcha: CaptchaFeature,
/// Whether email verification is enabled
pub email: bool,
/// Whether this server is invite only
pub invite_only: bool,
/// File server service configuration
pub autumn: Feature,
/// Proxy service configuration
pub january: Feature,
/// Voice server configuration
pub livekit: VoiceFeature,
}
/// Build Information
pub struct BuildInformation {
/// Commit Hash
pub commit_sha: String,
/// Commit Timestamp
pub commit_timestamp: String,
/// Git Semver
pub semver: String,
/// Git Origin URL
pub origin_url: String,
/// Build Timestamp
pub timestamp: String,
}
/// Server Configuration
pub struct RevoltConfig {
/// Revolt API Version
pub revolt: String,
/// Features enabled on this Revolt node
pub features: RevoltFeatures,
/// WebSocket URL
pub ws: String,
/// URL pointing to the client serving this node
pub app: String,
/// Web Push VAPID public key
pub vapid: String,
/// Build information
pub build: BuildInformation,
}
);
+1 -1
View File
@@ -46,7 +46,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateEmoji { pub struct DataCreateEmoji {
/// Server name /// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32), regex = "RE_EMOJI"))]
pub name: String, pub name: String,
/// Parent information /// Parent information
pub parent: EmojiParent, pub parent: EmojiParent,
+6 -4
View File
@@ -141,17 +141,17 @@ auto_derived!(
pub struct Masquerade { pub struct Masquerade {
/// Replace the display name shown on this message /// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 32))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>, pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file) /// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 256))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub avatar: Option<String>, pub avatar: Option<String>,
/// Replace the display role colour shown on this message /// Replace the display role colour shown on this message
/// ///
/// Must have `ManageRole` permission to use /// Must have `ManageRole` permission to use
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128), regex = "RE_COLOUR"))]
pub colour: Option<String>, pub colour: Option<String>,
} }
@@ -281,6 +281,7 @@ auto_derived!(
/// Options for querying messages /// Options for querying messages
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsQueryMessages { pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch /// Maximum number of messages to fetch
/// ///
@@ -353,12 +354,13 @@ auto_derived!(
)] )]
pub struct OptionsBulkDelete { pub struct OptionsBulkDelete {
/// Message IDs /// Message IDs
#[validate(length(min = 1, max = 100))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
pub ids: Vec<String>, pub ids: Vec<String>,
} }
/// Options for removing reaction /// Options for removing reaction
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsUnreact { pub struct OptionsUnreact {
/// Remove a specific user's reaction /// Remove a specific user's reaction
pub user_id: Option<String>, pub user_id: Option<String>,
+2
View File
@@ -3,6 +3,7 @@ mod channel_invites;
mod channel_unreads; mod channel_unreads;
mod channel_webhooks; mod channel_webhooks;
mod channels; mod channels;
mod config;
mod embeds; mod embeds;
mod emojis; mod emojis;
mod files; mod files;
@@ -20,6 +21,7 @@ pub use channel_invites::*;
pub use channel_unreads::*; pub use channel_unreads::*;
pub use channel_webhooks::*; pub use channel_webhooks::*;
pub use channels::*; pub use channels::*;
pub use config::*;
pub use embeds::*; pub use embeds::*;
pub use emojis::*; pub use emojis::*;
pub use files::*; pub use files::*;
@@ -116,6 +116,7 @@ auto_derived!(
/// Options for fetching all members /// Options for fetching all members
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsFetchAllMembers { pub struct OptionsFetchAllMembers {
/// Whether to exclude offline users /// Whether to exclude offline users
pub exclude_offline: Option<bool>, pub exclude_offline: Option<bool>,
+2
View File
@@ -198,6 +198,7 @@ auto_derived!(
/// Options when fetching server /// Options when fetching server
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsFetchServer { pub struct OptionsFetchServer {
/// Whether to include channels /// Whether to include channels
pub include_channels: Option<bool>, pub include_channels: Option<bool>,
@@ -284,6 +285,7 @@ auto_derived!(
/// Options when leaving a server /// Options when leaving a server
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsServerDelete { pub struct OptionsServerDelete {
/// Whether to not send a leave message /// Whether to not send a leave message
pub leave_silently: Option<bool>, pub leave_silently: Option<bool>,
@@ -17,6 +17,7 @@ auto_derived!(
/// Additional options for inserting settings /// Additional options for inserting settings
#[cfg_attr(feature = "rocket", derive(FromForm))] #[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsSetSettings { pub struct OptionsSetSettings {
/// Timestamp of settings change. /// Timestamp of settings change.
/// ///
+2 -2
View File
@@ -142,7 +142,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserStatus { pub struct UserStatus {
/// Custom status text /// Custom status text
#[validate(length(min = 0, max = 128))] #[cfg_attr(feature = "validator", validate(length(min = 0, max = 128)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub text: Option<String>, pub text: Option<String>,
/// Current presence option /// Current presence option
@@ -155,7 +155,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserProfile { pub struct UserProfile {
/// Text content on user's profile /// Text content on user's profile
#[validate(length(min = 0, max = 2000))] #[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>, pub content: Option<String>,
/// Background visible on user's profile /// Background visible on user's profile
+1 -3
View File
@@ -11,10 +11,8 @@ description = "Revolt Backend: Permission Logic"
[features] [features]
bson = ["dep:bson"] bson = ["dep:bson"]
serde = ["dep:serde"] serde = ["dep:serde"]
schemas = ["dep:schemars"]
try-from-primitive = ["dep:num_enum"] try-from-primitive = ["dep:num_enum"]
[dev-dependencies] [dev-dependencies]
# Async # Async
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
@@ -36,4 +34,4 @@ serde = { version = "1", features = ["derive"], optional = true }
bson = { version = "2.1.0", optional = true } bson = { version = "2.1.0", optional = true }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } utoipa = { version = "5.4.0", optional = true }
+7 -7
View File
@@ -1,10 +1,10 @@
#[cfg(feature = "schemas")] #[cfg(feature = "utoipa")]
use schemars::JsonSchema; use utoipa::ToSchema;
/// Representation of a single permission override /// Representation of a single permission override
#[derive(Debug, Clone, Eq, PartialEq, Default)] #[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct Override { pub struct Override {
/// Allow bit flags /// Allow bit flags
pub allow: u64, pub allow: u64,
@@ -15,7 +15,7 @@ pub struct Override {
/// Data permissions Field - contains both allow and deny /// Data permissions Field - contains both allow and deny
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct DataPermissionsField { pub struct DataPermissionsField {
pub permissions: Override, pub permissions: Override,
} }
@@ -23,7 +23,7 @@ pub struct DataPermissionsField {
/// Data permissions Value - contains allow /// Data permissions Value - contains allow
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct DataPermissionsValue { pub struct DataPermissionsValue {
pub permissions: u64, pub permissions: u64,
} }
@@ -31,7 +31,7 @@ pub struct DataPermissionsValue {
/// Data permissions Poly - can contain either Value or Field /// Data permissions Poly - can contain either Value or Field
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "serde", serde(untagged))] #[cfg_attr(feature = "serde", serde(untagged))]
pub enum DataPermissionPoly { pub enum DataPermissionPoly {
Value { Value {
@@ -48,7 +48,7 @@ pub enum DataPermissionPoly {
/// as it appears on models and in the database /// as it appears on models and in the database
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(ToSchema))]
pub struct OverrideField { pub struct OverrideField {
/// Allow bit flags /// Allow bit flags
pub a: i64, pub a: i64,
+5 -3
View File
@@ -4,7 +4,10 @@ version = "0.8.9"
edition = "2024" edition = "2024"
[features] [features]
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"] rocket = [
"dep:rocket",
"revolt-database/rocket-impl",
]
axum = ["dep:axum", "revolt-database/axum-impl"] axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"] default = ["rocket", "axum"]
@@ -15,9 +18,8 @@ revolt-result = { version = "0.8.9", path = "../result" }
revolt-config = { version = "0.8.9", path = "../config" } revolt-config = { version = "0.8.9", path = "../config" }
rocket = { version = "0.5.1", optional = true } 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"] } axum = { version = "0.8.6", optional = true, features = ["macros"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
authifier = { version = "1.0.15" } authifier = { version = "1.0.15" }
-3
View File
@@ -1,6 +1,5 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use async_trait::async_trait;
use axum::{ use axum::{
Json, RequestPartsExt, Router, Json, RequestPartsExt, Router,
body::Body, body::Body,
@@ -44,7 +43,6 @@ async fn to_real_ip(parts: &Parts) -> String {
} }
} }
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
where where
Database: FromRef<S>, Database: FromRef<S>,
@@ -82,7 +80,6 @@ where
} }
} }
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
where where
Database: FromRef<S>, Database: FromRef<S>,
-13
View File
@@ -8,9 +8,6 @@ use rocket::serde::json::Json;
use rocket::{Data, Request, Response, State}; use rocket::{Data, Request, Response, State};
use revolt_config::config; use revolt_config::config;
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use authifier::models::Session; use authifier::models::Session;
use crate::ratelimiter::RequestKind; use crate::ratelimiter::RequestKind;
@@ -78,16 +75,6 @@ impl<'r> FromRequest<'r> for 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 /// Attach ratelimiter to the Rocket application
pub struct RatelimitFairing; pub struct RatelimitFairing;
+6 -8
View File
@@ -10,13 +10,12 @@ description = "Revolt Backend: Result and Error types"
[features] [features]
serde = ["dep:serde"] serde = ["dep:serde"]
schemas = ["dep:schemars"]
utoipa = ["dep:utoipa"] utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"] rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"] axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"] sentry = ["dep:sentry"]
default = ["serde"] default = ["serde", "sentry"]
[dependencies] [dependencies]
# Serialisation # Serialisation
@@ -24,15 +23,14 @@ serde_json = { version = "1", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } utoipa = { version = "5.4.0", optional = true }
utoipa = { version = "4.2.3", optional = true }
# Rocket # Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
# utilities # utilities
log = "0.4" log = "0.4"
# Axum # Axum
axum = { version = "0.7.5", optional = true } axum = { version = "0.8.6", optional = true }
sentry = { version = "0.31.5", optional = true }
+1
View File
@@ -62,6 +62,7 @@ impl IntoResponse for Error {
ErrorType::NotPrivileged => StatusCode::FORBIDDEN, ErrorType::NotPrivileged => StatusCode::FORBIDDEN,
ErrorType::CannotGiveMissingPermissions => StatusCode::FORBIDDEN, ErrorType::CannotGiveMissingPermissions => StatusCode::FORBIDDEN,
ErrorType::NotOwner => StatusCode::FORBIDDEN, ErrorType::NotOwner => StatusCode::FORBIDDEN,
ErrorType::IsElevated => StatusCode::FORBIDDEN,
ErrorType::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR, ErrorType::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::InternalError => StatusCode::INTERNAL_SERVER_ERROR, ErrorType::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
+9 -14
View File
@@ -5,13 +5,10 @@ use std::fmt::Display;
#[macro_use] #[macro_use]
extern crate serde; extern crate serde;
#[cfg(feature = "schemas")]
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")] #[cfg(feature = "utoipa")]
#[macro_use] mod utoipa_impl;
extern crate utoipa; #[cfg(feature = "utoipa")]
pub use crate::utoipa_impl::*;
#[cfg(feature = "rocket")] #[cfg(feature = "rocket")]
pub mod rocket; pub mod rocket;
@@ -19,16 +16,12 @@ pub mod rocket;
#[cfg(feature = "axum")] #[cfg(feature = "axum")]
pub mod axum; pub mod axum;
#[cfg(feature = "okapi")]
pub mod okapi;
/// Result type with custom Error /// Result type with custom Error
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Error information /// Error information
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Error { pub struct Error {
/// Type of error and additional information /// Type of error and additional information
@@ -50,8 +43,7 @@ impl std::error::Error for Error {}
/// Possible error types /// Possible error types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))] #[cfg_attr(feature = "serde", serde(tag = "type"))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ErrorType { pub enum ErrorType {
/// This error was not labeled :( /// This error was not labeled :(
@@ -139,6 +131,7 @@ pub enum ErrorType {
NotPrivileged, NotPrivileged,
CannotGiveMissingPermissions, CannotGiveMissingPermissions,
NotOwner, NotOwner,
IsElevated,
// ? General errors // ? General errors
DatabaseError { DatabaseError {
@@ -226,7 +219,7 @@ pub trait ToRevoltError<T> {
fn to_internal_error(self) -> Result<T, Error>; fn to_internal_error(self) -> Result<T, Error>;
} }
impl<T, E: std::fmt::Debug> ToRevoltError<T> for Result<T, E> { impl<T, E: std::fmt::Debug + std::error::Error> ToRevoltError<T> for Result<T, E> {
#[track_caller] #[track_caller]
fn to_internal_error(self) -> Result<T, Error> { fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller(); let loc = Location::caller();
@@ -234,6 +227,8 @@ impl<T, E: std::fmt::Debug> ToRevoltError<T> for Result<T, E> {
self self
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
#[cfg(feature = "sentry")]
sentry::capture_error(&e);
Error { Error {
error_type: ErrorType::InternalError, error_type: ErrorType::InternalError,
-49
View File
@@ -1,49 +0,0 @@
use revolt_okapi::openapi3::SchemaObject;
use revolt_rocket_okapi::revolt_okapi::openapi3;
use schemars::schema::Schema;
use crate::Error;
impl revolt_rocket_okapi::response::OpenApiResponderInner for Error {
fn responses(
gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, revolt_rocket_okapi::OpenApiError> {
let mut content = revolt_okapi::Map::new();
let settings = schemars::gen::SchemaSettings::default().with(|s| {
s.option_nullable = true;
s.option_add_null_type = false;
s.definitions_path = "#/components/schemas/".to_string();
});
let mut schema_generator = settings.into_generator();
let schema = schema_generator.root_schema_for::<Error>();
let definitions = gen.schema_generator().definitions_mut();
for (key, value) in schema.definitions {
definitions.insert(key, value);
}
definitions.insert("Error".to_string(), Schema::Object(schema.schema));
content.insert(
"application/json".to_string(),
openapi3::MediaType {
schema: Some(SchemaObject {
reference: Some("#/components/schemas/Error".to_string()),
..Default::default()
}),
..Default::default()
},
);
Ok(openapi3::Responses {
default: Some(openapi3::RefOr::Object(openapi3::Response {
content,
description: "An error occurred.".to_string(),
..Default::default()
})),
..Default::default()
})
}
}
+1
View File
@@ -69,6 +69,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotPrivileged => Status::Forbidden, ErrorType::NotPrivileged => Status::Forbidden,
ErrorType::CannotGiveMissingPermissions => Status::Forbidden, ErrorType::CannotGiveMissingPermissions => Status::Forbidden,
ErrorType::NotOwner => Status::Forbidden, ErrorType::NotOwner => Status::Forbidden,
ErrorType::IsElevated => Status::Forbidden,
ErrorType::DatabaseError { .. } => Status::InternalServerError, ErrorType::DatabaseError { .. } => Status::InternalServerError,
ErrorType::InternalError => Status::InternalServerError, ErrorType::InternalError => Status::InternalServerError,
+52
View File
@@ -0,0 +1,52 @@
use utoipa::{Modify, openapi::{Content, OpenApi, Ref, RefOr, ResponseBuilder}};
pub struct ErrorAddon;
impl Modify for ErrorAddon {
fn modify(&self, utoipa: &mut OpenApi) {
let response = ResponseBuilder::new()
.description("An error occurred")
.content(
"application/json",
Content::new(Some(RefOr::Ref(Ref::from_schema_name("Error")))),
)
.build();
for path in utoipa.paths.paths.values_mut() {
if let Some(route) = path.get.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.delete.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.patch.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.post.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.put.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
}
}
}
+1 -1
View File
@@ -38,5 +38,5 @@ pretty_env_logger = "0.4.0"
serde_json = "1" serde_json = "1"
revolt_optional_struct = "0.2.0" revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } iso8601-timestamp = { version = "0.4.0", features = ["serde", "bson"] }
base64 = "0.22.1" base64 = "0.22.1"
+4 -4
View File
@@ -36,13 +36,13 @@ async-std = { version = "1.8.0", features = [
revolt-result = { path = "../../core/result" } revolt-result = { path = "../../core/result" }
revolt-models = { path = "../../core/models" } revolt-models = { path = "../../core/models" }
revolt-config = { path = "../../core/config" } revolt-config = { path = "../../core/config" }
revolt-database = { path = "../../core/database" } revolt-database = { path = "../../core/database", features = ["voice"] }
revolt-permissions = { path = "../../core/permissions" } revolt-permissions = { path = "../../core/permissions" }
# Voice # Voice
livekit-api = "0.4.4" livekit-api = "=0.4.4"
livekit-protocol = "0.4.0" livekit-protocol = "=0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] } livekit-runtime = { version = "=0.3.1", features = ["tokio"] }
# RabbitMQ # RabbitMQ
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
+84 -85
View File
@@ -1,4 +1,3 @@
use chrono::DateTime;
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver}; use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType; use livekit_protocol::TrackType;
use revolt_database::{ use revolt_database::{
@@ -6,18 +5,15 @@ use revolt_database::{
iso8601_timestamp::{Duration, Timestamp}, iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference, util::reference::Reference,
voice::{ voice::{
create_voice_state, delete_voice_state, get_call_notification_recipients, create_voice_state, delete_voice_state,
get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members, get_user_moved_from_voice, get_user_moved_to_voice,
set_channel_call_started_system_message, take_channel_call_started_system_message,
update_voice_state_tracks, VoiceClient, update_voice_state_tracks, VoiceClient,
}, },
Database, PartialMessage, SystemMessage, AMQP, Database, AMQP,
}; };
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError}; use revolt_result::{Result, ToRevoltError};
use rocket::{post, State}; use rocket::{post, State};
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader; use crate::guard::AuthHeader;
@@ -25,7 +21,7 @@ use crate::guard::AuthHeader;
pub async fn ingress( pub async fn ingress(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>, voice_client: &State<VoiceClient>,
amqp: &State<AMQP>, _amqp: &State<AMQP>,
node: &str, node: &str,
auth_header: AuthHeader<'_>, auth_header: AuthHeader<'_>,
body: &str, body: &str,
@@ -90,50 +86,51 @@ pub async fn ingress(
.await; .await;
}; };
// TODO: fix `num_participants` being incorrect sometimes see (#457)
// First user who joined - send call started system message. // First user who joined - send call started system message.
if event.room.as_ref().unwrap().num_participants == 1 { // if event.room.as_ref().unwrap().num_participants == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?; // let user = Reference::from_unchecked(user_id).as_user(db).await?;
let message_id = // let message_id =
Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap()) // Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
.to_string(); // .to_string();
let mut call_started_message = SystemMessage::CallStarted { // let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(), // by: user_id.to_string(),
finished_at: None, // finished_at: None,
} // }
.into_message(channel.id().to_string()); // .into_message(channel.id().to_string());
call_started_message.id = message_id; // call_started_message.id = message_id;
set_channel_call_started_system_message(channel.id(), &call_started_message.id) // set_channel_call_started_system_message(channel.id(), &call_started_message.id)
.await?; // .await?;
call_started_message // call_started_message
.send( // .send(
db, // db,
Some(amqp), // Some(amqp),
v0::MessageAuthor::System { // v0::MessageAuthor::System {
username: &user.username, // username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()), // avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
}, // },
None, // None,
None, // None,
&channel, // &channel,
false, // false,
) // )
.await?; // .await?;
let recipients = get_call_notification_recipients(&channel_id, &user_id).await?; // let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
let now = joined_at.format_short().to_string(); // let now = joined_at.format_short().to_string();
if let Err(e) = amqp // if let Err(e) = amqp
.dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients) // .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
.await // .await
{ // {
revolt_config::capture_error(&e); // revolt_config::capture_error(&e);
} // }
} // }
} }
// User left a channel // User left a channel
"participant_left" => { "participant_left" => {
@@ -157,50 +154,52 @@ pub async fn ingress(
.await; .await;
}; };
// Update CallStarted system message if everyone has left with the end time // See above for why this is commented out
let members = get_voice_channel_members(channel_id).await?;
if members.is_none_or(|m| m.is_empty()) { // // Update CallStarted system message if everyone has left with the end time
// The channel is empty so send out an "end" message for ringing // let members = get_voice_channel_members(channel_id).await?;
if let Err(e) = amqp
.dm_call_updated(user_id, channel_id, None, true, None)
.await
{
revolt_config::capture_internal_error!(&e);
}
if let Some(system_message_id) = // if members.is_none_or(|m| m.is_empty()) {
take_channel_call_started_system_message(channel_id).await? // // The channel is empty so send out an "end" message for ringing
{ // if let Err(e) = amqp
// Could have been deleted // .dm_call_updated(user_id, channel_id, None, true, None)
if let Ok(mut message) = Reference::from_unchecked(&system_message_id) // .await
.as_message(db) // {
.await // revolt_config::capture_internal_error!(&e);
{ // }
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*finished_at = Some(Timestamp::now_utc());
message // if let Some(system_message_id) =
.update( // take_channel_call_started_system_message(channel_id).await?
db, // {
PartialMessage { // // Could have been deleted
system: message.system.clone(), // if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
..Default::default() // .as_message(db)
}, // .await
Vec::new(), // {
) // if let Some(SystemMessage::CallStarted { finished_at, .. }) =
.await?; // &mut message.system
} else { // {
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id) // *finished_at = Some(Timestamp::now_utc());
// message
// .update(
// db,
// PartialMessage {
// system: message.system.clone(),
// ..Default::default()
// },
// Vec::new(),
// )
// .await?;
// } else {
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
// }
// };
// };
// }
} }
}; // Audio/video track was started/stopped/unmuted/muted
}; "track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
}
}
// Audio/video track was started/stopped
"track_published" | "track_unpublished" => {
let channel_id = channel_id.to_internal_error()?; let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?; let user_id = user_id.to_internal_error()?;
let track = event.track.as_ref().to_internal_error()?; let track = event.track.as_ref().to_internal_error()?;
@@ -255,7 +254,7 @@ pub async fn ingress(
channel_id, channel_id,
channel.server(), channel.server(),
user_id, user_id,
event.event == "track_published", // to avoid duplicating this entire case twice event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
track.source, track.source,
) )
.await?; .await?;
+11 -9
View File
@@ -35,7 +35,7 @@ nanoid = "0.4.0"
serde_json = "1.0.57" serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] } serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] } validator = { version = "0.16", features = ["derive"] }
iso8601-timestamp = { version = "0.2.11", features = [] } iso8601-timestamp = { version = "0.4.0", features = [] }
# async # async
futures = "0.3.8" futures = "0.3.8"
@@ -55,13 +55,13 @@ lettre = "0.10.0-alpha.4"
rocket = { version = "0.5.1", default-features = false, features = ["json"] } rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_empty = { version = "0.1.1" }
rocket_authifier = { version = "1.0.15" } rocket_authifier = { version = "1.0.15" }
rocket_prometheus = "0.10.0-rc.3" rocket_prometheus = "0.10.0-rc.3"
# spec generation # spec generation
schemars = "0.8.8" utoipa = { version = "5.4.0", features = ["rocket_extras"] }
revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] } utoipa-scalar = { version = "0.3.0", features = ["rocket"] }
# rabbit # rabbit
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
@@ -72,20 +72,22 @@ revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [ revolt-database = { path = "../core/database", features = [
"rocket-impl", "rocket-impl",
"redis-is-patched", "redis-is-patched",
"utoipa",
"voice",
] } ] }
revolt-models = { path = "../core/models", features = [ revolt-models = { path = "../core/models", features = [
"schemas", "utoipa",
"validator", "validator",
"rocket", "rocket",
] } ] }
revolt-presence = { path = "../core/presence" } revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-result = { path = "../core/result", features = ["rocket", "utoipa"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] } revolt-permissions = { path = "../core/permissions", features = ["utoipa"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] } revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
# voice # voice
livekit-api = "0.4.4" livekit-api = "=0.4.4"
livekit-protocol = "0.4.0" livekit-protocol = "=0.4.0"
[build-dependencies] [build-dependencies]
vergen = "7.5.0" vergen = "7.5.0"
+4 -28
View File
@@ -1,7 +1,7 @@
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
#[macro_use] #[macro_use]
extern crate revolt_rocket_okapi; extern crate utoipa;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
@@ -15,6 +15,8 @@ use revolt_ratelimits::rocket as ratelimiter;
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics; use rocket_prometheus::PrometheusMetrics;
use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable};
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::str::FromStr; use std::str::FromStr;
@@ -75,31 +77,6 @@ pub async fn web() -> Rocket<Build> {
.to_cors() .to_cors()
.expect("Failed to create CORS."); .expect("Failed to create CORS.");
// Configure Swagger
let swagger = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/openapi.json".to_owned(),
..Default::default()
},
)
.into();
let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/0.8/openapi.json".to_owned(),
..Default::default()
},
)
.into();
let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/0.8/openapi.json".to_owned(),
..Default::default()
},
)
.into();
// Voice handler // Voice handler
let voice_client = VoiceClient::new(config.api.livekit.nodes.clone()); let voice_client = VoiceClient::new(config.api.livekit.nodes.clone());
// Configure Rabbit // Configure Rabbit
@@ -143,8 +120,7 @@ pub async fn web() -> Rocket<Build> {
.mount("/metrics", prometheus) .mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes()) .mount("/", rocket_cors::catch_all_options_routes())
.mount("/", ratelimiter::routes()) .mount("/", ratelimiter::routes())
.mount("/swagger/", swagger) .mount("/", Scalar::with_url("/scalar", routes::ApiDoc::openapi()))
.mount("/0.8/swagger/", swagger_0_8)
.manage(authifier) .manage(authifier)
.manage(db) .manage(db)
.manage(amqp) .manage(amqp)
+8 -2
View File
@@ -5,10 +5,16 @@ use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate; use validator::Validate;
/// # Create Bot /// Create Bot
/// ///
/// Create a new Revolt bot. /// Create a new Revolt bot.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[post("/create", data = "<info>")] #[post("/create", data = "<info>")]
pub async fn create_bot( pub async fn create_bot(
db: &State<Database>, db: &State<Database>,
+10 -12
View File
@@ -1,12 +1,18 @@
use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, get_user_voice_channels, VoiceClient}, Database, User}; use revolt_database::{Database, User, util::reference::Reference, voice::{VoiceClient, remove_user_from_voice_channels}};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Delete Bot /// Delete Bot
/// ///
/// Delete a bot by its id. /// Delete a bot by its id.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 204),
),
)]
#[delete("/<target>")] #[delete("/<target>")]
pub async fn delete_bot( pub async fn delete_bot(
db: &State<Database>, db: &State<Database>,
@@ -21,15 +27,7 @@ pub async fn delete_bot(
bot.delete(db).await?; bot.delete(db).await?;
for channel_id in get_user_voice_channels(&bot.id).await? { remove_user_from_voice_channels(db, voice_client, &bot.id).await?;
if let Some(node) = get_channel_node(&channel_id).await? {
let _ = voice_client.remove_user(&node, &bot.id, &channel_id).await;
}
let channel = Reference::from_unchecked(&channel_id).as_channel(db).await?;
delete_voice_state(&channel_id, channel.server(), &bot.id).await?;
}
Ok(EmptyResponse) Ok(EmptyResponse)
} }
+9 -10
View File
@@ -6,10 +6,16 @@ use rocket::State;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use validator::Validate; use validator::Validate;
/// # Edit Bot /// Edit Bot
/// ///
/// Edit bot details by its id. /// Edit bot details by its id.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[patch("/<target>", data = "<data>")] #[patch("/<target>", data = "<data>")]
pub async fn edit_bot( pub async fn edit_bot(
db: &State<Database>, db: &State<Database>,
@@ -60,14 +66,7 @@ pub async fn edit_bot(
..Default::default() ..Default::default()
}; };
bot.update( bot.update(db, partial, remove.into_iter().map(|v| v.into()).collect())
db,
partial,
remove
.into_iter()
.map(|v| v.into())
.collect(),
)
.await?; .await?;
Ok(Json(v0::BotWithUserResponse { Ok(Json(v0::BotWithUserResponse {
+8 -2
View File
@@ -3,10 +3,16 @@ use revolt_models::v0::FetchBotResponse;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
/// # Fetch Bot /// Fetch Bot
/// ///
/// Fetch details of a bot you own by its id. /// Fetch details of a bot you own by its id.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = FetchBotResponse),
),
)]
#[get("/<bot>")] #[get("/<bot>")]
pub async fn fetch_bot( pub async fn fetch_bot(
db: &State<Database>, db: &State<Database>,
+8 -2
View File
@@ -5,10 +5,16 @@ use revolt_result::Result;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
/// # Fetch Owned Bots /// Fetch Owned Bots
/// ///
/// Fetch all of the bots that you have control over. /// Fetch all of the bots that you have control over.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = OwnedBotsResponse),
),
)]
#[get("/@me")] #[get("/@me")]
pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<OwnedBotsResponse>> { pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<OwnedBotsResponse>> {
let mut bots = db.fetch_bots_by_user(&user.id).await?; let mut bots = db.fetch_bots_by_user(&user.id).await?;
+8 -2
View File
@@ -5,10 +5,16 @@ use revolt_result::{create_error, Result};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
/// # Fetch Public Bot /// Fetch Public Bot
/// ///
/// Fetch details of a public (or owned) bot by its id. /// Fetch details of a public (or owned) bot by its id.
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = []), ()),
responses(
(status = 200, body = PublicBot),
),
)]
#[get("/<target>/invite")] #[get("/<target>/invite")]
pub async fn fetch_public_bot( pub async fn fetch_public_bot(
db: &State<Database>, db: &State<Database>,
+8 -2
View File
@@ -11,10 +11,16 @@ use rocket::State;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Invite Bot /// Invite Bot
/// ///
/// Invite a bot to a server or group by its id.` /// Invite a bot to a server or group by its id.`
#[openapi(tag = "Bots")] #[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[post("/<target>/invite", data = "<dest>")] #[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot( pub async fn invite_bot(
db: &State<Database>, db: &State<Database>,
+16 -3
View File
@@ -1,4 +1,3 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route; use rocket::Route;
mod create; mod create;
@@ -9,8 +8,22 @@ mod fetch_owned;
mod fetch_public; mod fetch_public;
mod invite; mod invite;
pub fn routes() -> (Vec<Route>, OpenApi) { #[derive(OpenApi)]
openapi_get_routes_spec![ #[openapi(
paths(
create::create_bot,
invite::invite_bot,
fetch_public::fetch_public_bot,
fetch::fetch_bot,
fetch_owned::fetch_owned_bots,
edit::edit_bot,
delete::delete_bot,
)
)]
pub struct ApiDoc;
pub fn routes() -> Vec<Route> {
routes![
create::create_bot, create::create_bot,
invite::invite_bot, invite::invite_bot,
fetch_public::fetch_public_bot, fetch_public::fetch_public_bot,
@@ -7,10 +7,20 @@ use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Acknowledge Message /// Acknowledge Message
/// ///
/// Lets the server and all other clients know that we've seen this message id in this channel. /// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")] #[utoipa::path(
tag = "Messaging",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
("message" = Reference, Path),
),
responses(
(status = 204)
)
)]
#[put("/<target>/ack/<message>")] #[put("/<target>/ack/<message>")]
pub async fn ack( pub async fn ack(
db: &State<Database>, db: &State<Database>,
@@ -1,9 +1,7 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, AMQP, Channel, Database, PartialChannel, User, util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{
voice::{ VoiceClient, delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel
delete_channel_voice_state, delete_voice_state, get_channel_node, get_voice_channel_members, is_in_voice_channel, VoiceClient }
},
Channel, Database, PartialChannel, User, AMQP,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -11,10 +9,20 @@ use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Close Channel /// Close Channel
/// ///
/// Deletes a server channel, leaves a group or closes a group. /// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")] #[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
v0::OptionsChannelDelete,
),
responses(
(status = 204),
),
)]
#[delete("/<target>?<options..>")] #[delete("/<target>?<options..>")]
pub async fn delete( pub async fn delete(
db: &State<Database>, db: &State<Database>,
@@ -57,26 +65,14 @@ pub async fn delete(
.await?; .await?;
if is_in_voice_channel(&user.id, channel.id()).await? { if is_in_voice_channel(&user.id, channel.id()).await? {
let node = get_channel_node(channel.id()).await?.unwrap(); remove_user_from_voice_channel(db, voice_client, channel.id(), &user.id).await?;
voice_client
.remove_user(&node, &user.id, channel.id())
.await?;
delete_voice_state(channel.id(), None, &user.id).await?;
}; };
} }
Channel::TextChannel { .. } => { Channel::TextChannel { .. } => {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await?; channel.delete(db).await?;
if let Some(users) = get_voice_channel_members(channel.id()).await? { delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.delete_room(&node, channel.id()).await?;
delete_channel_voice_state(channel.id(), channel.server(), &users).await?;
};
} }
}; };
@@ -1,5 +1,7 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{delete_channel_voice_state, get_channel_node, get_voice_channel_members, VoiceClient}, Channel, Database, File, PartialChannel, SystemMessage, User, AMQP util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_channel, VoiceClient},
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -7,10 +9,19 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
use validator::Validate; use validator::Validate;
/// # Edit Channel /// Edit Channel
/// ///
/// Edit a channel object by its id. /// Edit a channel object by its id.
#[openapi(tag = "Channel Information")] #[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Channel),
),
)]
#[patch("/<target>", data = "<data>")] #[patch("/<target>", data = "<data>")]
pub async fn edit( pub async fn edit(
db: &State<Database>, db: &State<Database>,
@@ -253,21 +264,12 @@ pub async fn edit(
.update( .update(
db, db,
partial, partial,
data.remove data.remove.into_iter().map(|f| f.into()).collect(),
.into_iter()
.map(|f| f.into())
.collect(),
) )
.await?; .await?;
if channel.voice().is_none() { if channel.voice().is_none() {
if let Some(users) = get_voice_channel_members(channel.id()).await? { delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.delete_room(&node, channel.id()).await?;
delete_channel_voice_state(channel.id(), channel.server(), &users).await?;
};
} }
Ok(Json(channel.into())) Ok(Json(channel.into()))
@@ -8,10 +8,19 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::Result;
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
/// # Fetch Channel /// Fetch Channel
/// ///
/// Fetch channel by its id. /// Fetch channel by its id.
#[openapi(tag = "Channel Information")] #[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Channel),
),
)]
#[get("/<target>")] #[get("/<target>")]
pub async fn fetch( pub async fn fetch(
db: &State<Database>, db: &State<Database>,
@@ -8,10 +8,20 @@ use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Add Member to Group /// Add Member to Group
/// ///
/// Adds another user to the group. /// Adds another user to the group.
#[openapi(tag = "Groups")] #[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
params(
("group_id" = Reference, Path),
("member_id" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[put("/<group_id>/recipients/<member_id>")] #[put("/<group_id>/recipients/<member_id>")]
pub async fn add_member( pub async fn add_member(
db: &State<Database>, db: &State<Database>,
@@ -6,10 +6,16 @@ use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate; use validator::Validate;
/// # Create Group /// Create Group
/// ///
/// Create a new group channel. /// Create a new group channel.
#[openapi(tag = "Groups")] #[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::Channel),
),
)]
#[post("/create", data = "<data>")] #[post("/create", data = "<data>")]
pub async fn create_group( pub async fn create_group(
db: &State<Database>, db: &State<Database>,
@@ -1,14 +1,28 @@
use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, is_in_voice_channel, VoiceClient}, Channel, Database, User, AMQP}; use revolt_database::{
util::reference::Reference,
voice::{is_in_voice_channel, remove_user_from_voice_channel, VoiceClient},
Channel, Database, User, AMQP,
};
use revolt_permissions::ChannelPermission; use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Remove Member from Group /// Remove Member from Group
/// ///
/// Removes a user from the group. /// Removes a user from the group.
#[openapi(tag = "Groups")] #[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
("member" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[delete("/<target>/recipients/<member>")] #[delete("/<target>/recipients/<member>")]
pub async fn remove_member( pub async fn remove_member(
db: &State<Database>, db: &State<Database>,
@@ -24,7 +38,10 @@ pub async fn remove_member(
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
if let Channel::Group { owner, recipients, .. } = &channel { if let Channel::Group {
owner, recipients, ..
} = &channel
{
if &user.id != owner { if &user.id != owner {
return Err(create_error!(MissingPermission { return Err(create_error!(MissingPermission {
permission: ChannelPermission::ManageChannel.to_string() permission: ChannelPermission::ManageChannel.to_string()
@@ -44,15 +61,11 @@ pub async fn remove_member(
.remove_user_from_group(db, amqp, &member, Some(&user.id), false) .remove_user_from_group(db, amqp, &member, Some(&user.id), false)
.await?; .await?;
} else { } else {
return Err(create_error!(InvalidOperation)) return Err(create_error!(InvalidOperation));
}; };
if is_in_voice_channel(&user.id, channel.id()).await? { if is_in_voice_channel(&member.id, channel.id()).await? {
if let Some(node) = get_channel_node(channel.id()).await? { remove_user_from_voice_channel(db, voice_client, channel.id(), &member.id).await?;
let _ = voice_client.remove_user(&node, &user.id, channel.id()).await;
}
delete_voice_state(channel.id(), None, &user.id).await?;
}; };
Ok(EmptyResponse) Ok(EmptyResponse)
@@ -8,12 +8,21 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
/// # Create Invite /// Create Invite
/// ///
/// Creates an invite to this channel. /// Creates an invite to this channel.
/// ///
/// Channel must be a `TextChannel`. /// Channel must be a `TextChannel`.
#[openapi(tag = "Channel Invites")] #[utoipa::path(
tag = "Channel Invites",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Invite),
),
)]
#[post("/<target>/invites")] #[post("/<target>/invites")]
pub async fn create_invite( pub async fn create_invite(
db: &State<Database>, db: &State<Database>,
@@ -7,12 +7,21 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
/// # Fetch Group Members /// Fetch Group Members
/// ///
/// Retrieves all users who are part of this group. /// Retrieves all users who are part of this group.
/// ///
/// This may not return full user information if users are not friends but have mutual connections. /// This may not return full user information if users are not friends but have mutual connections.
#[openapi(tag = "Groups")] #[utoipa::path(
tag = "Groups",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = Vec<v0::User>),
),
)]
#[get("/<target>/members")] #[get("/<target>/members")]
pub async fn fetch_members( pub async fn fetch_members(
db: &State<Database>, db: &State<Database>,
@@ -10,14 +10,23 @@ use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
use validator::Validate; use validator::Validate;
/// # Bulk Delete Messages /// Bulk Delete Messages
/// ///
/// Delete multiple messages you've sent or one you have permission to delete. /// Delete multiple messages you've sent or one you have permission to delete.
/// ///
/// This will always require `ManageMessages` permission regardless of whether you own the message or not. /// This will always require `ManageMessages` permission regardless of whether you own the message or not.
/// ///
/// Messages must have been sent within the past 1 week. /// Messages must have been sent within the past 1 week.
#[openapi(tag = "Messaging")] #[utoipa::path(
tag = "Messaging",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)] #[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn bulk_delete_messages( pub async fn bulk_delete_messages(
db: &State<Database>, db: &State<Database>,
Loaded 100 of 239 files, more files were not shown because too many files have changed in this diff. Show more