Compare commits

..
125 changed files with 1219 additions and 5102 deletions
-20
View File
@@ -152,26 +152,6 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }} labels: ${{ steps.meta-january.outputs.labels }}
# revoltchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/gifbox
ghcr.io/revoltchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/gifbox/Dockerfile
tags: ${{ steps.meta-gifbox.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-gifbox.outputs.labels }}
# revoltchat/crond # revoltchat/crond
- name: Docker meta - name: Docker meta
id: meta-crond id: meta-crond
-2
View File
@@ -11,6 +11,4 @@ venv/
.vercel .vercel
.DS_Store .DS_Store
livekit.yml
.idea .idea
start
Generated
+555 -937
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -27,11 +27,8 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/ COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/ COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/ COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps RUN sh /tmp/build-image-layer.sh deps
-3
View File
@@ -23,11 +23,8 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/ COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/ COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/ COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps RUN sh /tmp/build-image-layer.sh deps
-5
View File
@@ -21,11 +21,9 @@ The services and libraries that power the Revolt service.<br/>
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) | | `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) | | `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) | | `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service | ![Crates.io Version](https://img.shields.io/crates/v/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/size/revolt-coalesced) ![Crates.io License](https://img.shields.io/crates/l/revolt-coalesced) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
@@ -68,7 +66,6 @@ As a heads-up, the development environment uses the following ports:
| `crates/bonfire` | 14703 | | `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 | | `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 | | `crates/services/january` | 14705 |
| `crates/services/gifbox` | 14706 |
Now you can clone and build the project: Now you can clone and build the project:
@@ -144,8 +141,6 @@ cargo run --bin revolt-bonfire
cargo run --bin revolt-autumn cargo run --bin revolt-autumn
# run the proxy server # run the proxy server
cargo run --bin revolt-january cargo run --bin revolt-january
# run the tenor proxy
cargo run --bin revolt-gifbox
# run the push daemon (not usually needed in regular development) # run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd cargo run --bin revolt-pushd
-17
View File
@@ -26,11 +26,6 @@ january = "http://local.revolt.chat:14705"
voso_legacy = "" voso_legacy = ""
voso_legacy_ws = "" voso_legacy_ws = ""
# Public urls for livekit nodes
# each entry here should have a corresponding entry under `api.livekit.nodes`
[hosts.livekit]
worldwide = "ws://local.revolt.chat:14706"
[api] [api]
[api.smtp] [api.smtp]
@@ -45,18 +40,6 @@ port = 14025
use_tls = false use_tls = false
use_starttls = false use_starttls = false
[api.livekit]
# Config for livekit nodes
# Make sure to change the secret when deploying
# The key and secret should match the values livekit is using
[api.livekit.nodes.worldwide]
url = "http://livekit"
lat = 0.0
lon = 0.0
key = "worldwide_key"
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
[files.s3] [files.s3]
# S3 protocol endpoint # S3 protocol endpoint
endpoint = "http://127.0.0.1:14009" endpoint = "http://127.0.0.1:14009"
+1 -1
View File
@@ -40,7 +40,7 @@ services:
# Rabbit # Rabbit
rabbit: rabbit:
image: rabbitmq:4-management image: rabbitmq:3-management
environment: environment:
RABBITMQ_DEFAULT_USER: rabbituser RABBITMQ_DEFAULT_USER: rabbituser
RABBITMQ_DEFAULT_PASS: rabbitpass RABBITMQ_DEFAULT_PASS: rabbitpass
+2 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.8.9" version = "0.8.8"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -19,7 +19,6 @@ async-channel = "2.3.1"
# parsing # parsing
querystring = "1.1.0" querystring = "1.1.0"
regex = "1.11.1"
# serde # serde
bincode = "1.3.3" bincode = "1.3.3"
@@ -42,7 +41,7 @@ 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" }
revolt-permissions = { version = "0.8.9", path = "../core/permissions" } revolt-permissions = { version = "0.8.8", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+8 -54
View File
@@ -1,15 +1,9 @@
use async_tungstenite::tungstenite::{handshake, Message}; use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender; use futures::channel::oneshot::Sender;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::events::client::ReadyPayloadFields; use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
/// Enumeration of supported protocol formats /// Enumeration of supported protocol formats
#[derive(Debug)] #[derive(Debug)]
pub enum ProtocolFormat { pub enum ProtocolFormat {
@@ -23,7 +17,6 @@ pub struct ProtocolConfiguration {
protocol_version: i32, protocol_version: i32,
format: ProtocolFormat, format: ProtocolFormat,
session_token: Option<String>, session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
} }
impl ProtocolConfiguration { impl ProtocolConfiguration {
@@ -32,13 +25,11 @@ impl ProtocolConfiguration {
protocol_version: i32, protocol_version: i32,
format: ProtocolFormat, format: ProtocolFormat,
session_token: Option<String>, session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
) -> Self { ) -> Self {
Self { Self {
protocol_version, protocol_version,
format, format,
session_token, session_token,
ready_payload_fields,
} }
} }
@@ -95,8 +86,14 @@ impl ProtocolConfiguration {
} }
/// Get ready payload fields /// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields { pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
&self.ready_payload_fields vec![
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
} }
} }
@@ -127,23 +124,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
let mut protocol_version = 1; let mut protocol_version = 1;
let mut format = ProtocolFormat::Json; let mut format = ProtocolFormat::Json;
let mut session_token = None; let mut session_token = None;
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
// If they pass the ready field, set all fields to false
ReadyPayloadFields {
users: false,
servers: false,
channels: false,
members: false,
emojis: false,
voice_states: false,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: false,
}
} else {
ReadyPayloadFields::default()
};
// Parse and map parameters from key-value to known variables. // Parse and map parameters from key-value to known variables.
for (key, value) in params { for (key, value) in params {
@@ -159,31 +139,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
_ => {} _ => {}
}, },
"token" => session_token = Some(value.into()), "token" => session_token = Some(value.into()),
"ready" => {
// Re-enable all the fields the client specifies
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
if let Some(field) = captures.get(0) {
match field.as_str() {
"users" => ready_payload_fields.users = true,
"servers" => ready_payload_fields.servers = true,
"channels" => ready_payload_fields.channels = true,
"members" => ready_payload_fields.members = true,
"emojis" => ready_payload_fields.emojis = true,
"voice_states" => ready_payload_fields.voice_states = true,
"channel_unreads" => ready_payload_fields.channel_unreads = true,
"user_settings" => {
if let Some(subkey) = captures.get(1) {
ready_payload_fields
.user_settings
.push(subkey.as_str().to_string());
}
}
"policy_changes" => ready_payload_fields.policy_changes = true,
_ => {}
}
}
}
}
_ => {} _ => {}
} }
} }
@@ -196,7 +151,6 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
protocol_version, protocol_version,
format, format,
session_token, session_token,
ready_payload_fields,
}) })
.is_ok() .is_ok()
{ {
+31 -59
View File
@@ -4,7 +4,6 @@ use futures::future::join_all;
use revolt_database::{ use revolt_database::{
events::client::{EventV1, ReadyPayloadFields}, events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery, util::permissions::DatabasePermissionQuery,
voice::get_channel_voice_state,
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus, Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -18,9 +17,8 @@ use super::state::{Cache, State};
impl Cache { impl Cache {
/// Check whether the current user can view a channel /// Check whether the current user can view a channel
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool { pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
#[allow(deprecated)]
match &channel { match &channel {
Channel::TextChannel { server, .. } => { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self.members.get(server); let member = self.members.get(server);
let server = self.servers.get(server); let server = self.servers.get(server);
let mut query = let mut query =
@@ -97,23 +95,21 @@ impl State {
pub async fn generate_ready_payload( pub async fn generate_ready_payload(
&mut self, &mut self,
db: &Database, db: &Database,
fields: &ReadyPayloadFields, fields: Vec<ReadyPayloadFields>,
) -> Result<EventV1> { ) -> Result<EventV1> {
let user = self.clone_user(); let user = self.clone_user();
self.cache.is_bot = user.bot.is_some(); self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes. // Fetch pending policy changes.
let policy_changes = if user.bot.is_some() || !fields.policy_changes { let policy_changes = if user.bot.is_some() {
None vec![]
} else { } else {
Some(
db.fetch_policy_changes() db.fetch_policy_changes()
.await? .await?
.into_iter() .into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change) .filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into) .map(Into::into)
.collect(), .collect()
)
}; };
// Find all relationships to the user. // Find all relationships to the user.
@@ -172,7 +168,7 @@ impl State {
.await?; .await?;
// Fetch customisations. // Fetch customisations.
let emojis = if fields.emojis { let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
Some( Some(
db.fetch_emoji_by_parent_ids( db.fetch_emoji_by_parent_ids(
&servers &servers
@@ -180,56 +176,25 @@ impl State {
.map(|x| x.id.to_string()) .map(|x| x.id.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.await?
.into_iter()
.map(|emoji| emoji.into())
.collect(),
)
} else {
None
};
// Fetch user settings
let user_settings = if !fields.user_settings.is_empty() {
Some(
db.fetch_user_settings(&user.id, &fields.user_settings)
.await?, .await?,
) )
} else { } else {
None None
}; };
// Fetch channel unreads // Fetch user settings
let channel_unreads = if fields.channel_unreads { let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
Some( .iter()
db.fetch_unreads(&user.id) .find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
.await? {
.into_iter() Some(db.fetch_user_settings(&user.id, keys).await?)
.map(|unread| unread.into())
.collect(),
)
} else { } else {
None None
}; };
let voice_states = if fields.voice_states { // Fetch channel unreads
// fetch voice states for all the channels we can see let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
let mut voice_states = Vec::new(); Some(db.fetch_unreads(&user.id).await?)
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 { } else {
None None
}; };
@@ -276,27 +241,30 @@ impl State {
} }
Ok(EventV1::Ready { Ok(EventV1::Ready {
users: if fields.users { Some(users) } else { None }, users: if fields.contains(&ReadyPayloadFields::Users) {
servers: if fields.servers { Some(users)
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
Some(servers.into_iter().map(Into::into).collect()) Some(servers.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
channels: if fields.channels { channels: if fields.contains(&ReadyPayloadFields::Channels) {
Some(channels.into_iter().map(Into::into).collect()) Some(channels.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
members: if fields.members { members: if fields.contains(&ReadyPayloadFields::Members) {
Some(members.into_iter().map(Into::into).collect()) Some(members.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
voice_states, emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
emojis,
user_settings, user_settings,
channel_unreads, channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
policy_changes, policy_changes,
}) })
@@ -311,7 +279,9 @@ impl State {
let id = &id.to_string(); let id = &id.to_string();
for (channel_id, channel) in &self.cache.channels { for (channel_id, channel) in &self.cache.channels {
if channel.server() == Some(id) { match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
if server == id {
channel_ids.insert(channel_id.clone()); channel_ids.insert(channel_id.clone());
if self.cache.can_view_channel(db, channel).await { if self.cache.can_view_channel(db, channel).await {
@@ -321,6 +291,9 @@ impl State {
} }
} }
} }
_ => {}
}
}
let known_ids = server.channels.iter().cloned().collect::<HashSet<String>>(); let known_ids = server.channels.iter().cloned().collect::<HashSet<String>>();
@@ -486,7 +459,6 @@ impl State {
server, server,
channels, channels,
emojis: _, emojis: _,
voice_states: _,
} => { } => {
self.insert_subscription(id.clone()).await; self.insert_subscription(id.clone()).await;
+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());
-22
View File
@@ -1,22 +0,0 @@
[package]
name = "revolt-coalesced"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
description = "Revolt Backend: Coalescion service"
[features]
tokio = ["dep:tokio"]
queue = ["dep:indexmap"]
cache = ["dep:lru"]
default = ["tokio"]
[dependencies]
tokio = { version = "1.47.0", features = ["sync"], optional = true }
indexmap = { version = "*", optional = true }
lru = { version = "*", optional = true }
[dev-dependencies]
tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
-9
View File
@@ -1,9 +0,0 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-24
View File
@@ -1,24 +0,0 @@
#[derive(Clone, PartialEq, Eq, Debug)]
/// Config values for [`CoalescionService`].
pub struct CoalescionServiceConfig {
/// How many tasks are running at once
pub max_concurrent: Option<usize>,
/// Whether to queue tasks once `max_concurrent` is reached
#[cfg(feature = "queue")]
pub queue_requests: bool,
/// Max amount of tasks in the buffer queue
#[cfg(feature = "queue")]
pub max_queue: Option<usize>,
}
impl Default for CoalescionServiceConfig {
fn default() -> Self {
Self {
max_concurrent: Some(100),
#[cfg(feature = "queue")]
queue_requests: true,
#[cfg(feature = "queue")]
max_queue: Some(100)
}
}
}
-27
View File
@@ -1,27 +0,0 @@
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
/// Coalescion service error.
pub enum Error {
/// Failed to receive the actions return from the channel for unknown reason
RecvError,
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
MaxConcurrent,
/// Reached the `max_queue` amount of actions in the queue
MaxQueue,
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
DowncastError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::RecvError => write!(f, "Unable to receive data from the channel"),
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
}
}
}
impl std::error::Error for Error {}
-39
View File
@@ -1,39 +0,0 @@
//! # Coalesced
//!
//! Coalescion service to group, caching and queue duplicate actions.
//! useful for deduplicating web requests, database lookups and other similar resource
//! intensive or rate-limited actions.
//!
//! ## Features
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
//!
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
//! use in your codebase.
//!
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
//!
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
//! and a type is wrong the library will return an error, values returned from the service are also
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
//!
//! ## Example:
//! ```rs
//! use revolt_coalesced::CoalescionService;
//!
//! let service = CoalescionService::new();
//!
//! let user_id = "my_user_id";
//! let user = service.execute(user_id, || async move {
//! database.fetch_user(user_id).await.unwrap()
//! }).await;
//! ```
mod config;
mod error;
mod service;
pub use config::CoalescionServiceConfig;
pub use error::Error;
pub use service::CoalescionService;
-208
View File
@@ -1,208 +0,0 @@
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
use tokio::sync::{
watch::{channel as watch_channel, Receiver},
RwLock,
};
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "queue")]
use indexmap::IndexMap;
use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
config: Arc<CoalescionServiceConfig>,
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "queue")]
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "cache")]
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
}
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
pub fn new() -> Self {
Default::default()
}
pub fn from_config(config: CoalescionServiceConfig) -> Self {
Self {
config: Arc::new(config),
watchers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "queue")]
queue: Arc::new(RwLock::new(IndexMap::new())),
#[cfg(feature = "cache")]
cache: None,
}
}
#[cfg(feature = "cache")]
pub fn from_cache(
config: CoalescionServiceConfig,
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
) -> Self {
Self {
cache: Some(Arc::new(Mutex::new(cache))),
..Self::from_config(config)
}
}
async fn wait_for<Value: Any + Send + Sync>(
&self,
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
) -> Result<Arc<Value>, Error> {
receiver
.wait_for(|v| v.is_some())
.await
.map_err(|_| Error::RecvError)
.and_then(|r| r.clone().unwrap())
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
}
async fn insert_and_execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
let (send, recv) = watch_channel(None);
self.watchers.write().await.insert(id.clone(), recv);
let value = Ok(Arc::new(func().await));
send.send_modify(|opt| {
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
});
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Ok(value) = &value {
cache.lock().await.push(id.clone(), value.clone());
}
};
self.watchers.write().await.remove(&id);
value
}
/// Coalesces an function, the actual function may not run if one with the same id is already running,
/// queued to be ran, or cached, the id should be globally unique for this specific action.
pub async fn execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Some(value) = cache.lock().await.get(&id) {
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
}
};
let (receiver, length) = {
let watchers = self.watchers.read().await;
let length = watchers.len();
(watchers.get(&id).cloned(), length)
};
if let Some(receiver) = receiver {
self.wait_for(receiver).await
} else {
match self.config.max_concurrent {
Some(max_concurrent) if length >= max_concurrent => {
#[cfg(feature = "queue")]
if self.config.queue_requests {
let (receiver, length) = {
let queue = self.queue.read().await;
(queue.get(&id).cloned(), queue.len())
};
if let Some(receiver) = receiver {
return self.wait_for(receiver).await;
} else {
if self
.config
.max_queue
.is_some_and(|max_queue| max_queue >= length)
{
return Err(Error::MaxQueue);
};
let (send, recv) = watch_channel(None);
self.queue.write().await.insert(id.clone(), recv);
loop {
let length = self.watchers.read().await.len();
if length < max_concurrent {
let first_key = {
let queue = self.queue.read().await;
queue.first().map(|v| v.0).cloned()
};
if first_key == Some(id.clone()) {
self.queue.write().await.shift_remove(&id);
let response = self.insert_and_execute(id, func).await;
send.send_modify(|opt| {
opt.replace(
response
.clone()
.map(|v| v as Arc<dyn Any + Send + Sync>),
);
});
return response;
}
}
}
}
} else {
Err(Error::MaxConcurrent)
}
#[cfg(not(feature = "queue"))]
Err(Error::MaxConcurrent)
}
_ => self.insert_and_execute(id, func).await,
}
}
}
/// Fetches the amount of currently running tasks
pub async fn current_task_count(&self) -> usize {
self.watchers.read().await.len()
}
#[cfg(feature = "queue")]
/// Fetches the current length of the queue
pub async fn current_queue_len(&self) -> usize {
self.queue.read().await.len()
}
}
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
fn default() -> Self {
Self::from_config(CoalescionServiceConfig::default())
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -37,4 +37,4 @@ sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true } sentry-anyhow = { version = "0.38.1", optional = true }
# Core # Core
revolt-result = { version = "0.8.9", path = "../result", optional = true } revolt-result = { version = "0.8.8", path = "../result", optional = true }
+1 -37
View File
@@ -22,8 +22,6 @@ january = "http://local.revolt.chat/january"
voso_legacy = "" voso_legacy = ""
voso_legacy_ws = "" voso_legacy_ws = ""
[hosts.livekit]
[rabbit] [rabbit]
host = "rabbit" host = "rabbit"
port = 5672 port = 5672
@@ -58,8 +56,6 @@ voso_legacy_token = ""
trust_cloudflare = false trust_cloudflare = false
# easypwned endpoint # easypwned endpoint
easypwned = "" easypwned = ""
# Tenor API Key
tenor_key = ""
[api.security.captcha] [api.security.captcha]
# hCaptcha configuration # hCaptcha configuration
@@ -70,14 +66,9 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server) # Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50 max_concurrent_connections = 50
[api.livekit]
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.livekit.nodes]
[api.users] [api.users]
[pushd] [pushd]
# this changes the names of the queues to not overlap # this changes the names of the queues to not overlap
# prod/beta if they happen to be on the same exchange/instance. # prod/beta if they happen to be on the same exchange/instance.
@@ -94,7 +85,6 @@ 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
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
@@ -238,18 +228,6 @@ message_attachments = 5
# Maximum number of servers the user can create/join # Maximum number of servers the user can create/join
servers = 50 servers = 50
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Mamimum resolution (width, height) of video streams in voice calls
video_resolution = [1080, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.new_user.file_upload_size_limit] [features.limits.new_user.file_upload_size_limit]
# Maximum file size limits (in bytes) # Maximum file size limits (in bytes)
attachments = 20_000_000 attachments = 20_000_000
@@ -277,18 +255,6 @@ message_attachments = 5
# Maximum number of servers the user can create/join # Maximum number of servers the user can create/join
servers = 100 servers = 100
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Mamimum resolution (width, height) of video streams in voice calls
video_resolution = [1080, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.default.file_upload_size_limit] [features.limits.default.file_upload_size_limit]
# Maximum file size limits (in bytes) # Maximum file size limits (in bytes)
attachments = 20_000_000 attachments = 20_000_000
@@ -307,9 +273,7 @@ process_message_delay_limit = 5
# Configuration for Sentry error reporting # Configuration for Sentry error reporting
api = "" api = ""
events = "" events = ""
voice_ingress = ""
files = "" files = ""
proxy = "" proxy = ""
pushd = "" pushd = ""
crond = "" crond = ""
gifbox = ""
+2 -33
View File
@@ -125,7 +125,8 @@ pub struct Hosts {
pub events: String, pub events: String,
pub autumn: String, pub autumn: String,
pub january: String, pub january: String,
pub livekit: HashMap<String, String>, pub voso_legacy: String,
pub voso_legacy_ws: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -189,7 +190,6 @@ pub struct ApiSecurity {
pub captcha: ApiSecurityCaptcha, pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool, pub trust_cloudflare: bool,
pub easypwned: String, pub easypwned: String,
pub tenor_key: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -197,25 +197,6 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize, pub max_concurrent_connections: usize,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct ApiLiveKit {
pub call_ring_duration: usize,
pub nodes: HashMap<String, LiveKitNode>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LiveKitNode {
pub url: String,
pub lat: f64,
pub lon: f64,
pub key: String,
pub secret: String,
// whether to hide the node in the nodes list
#[serde(default)]
pub private: bool,
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers { pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>, pub early_adopter_cutoff: Option<u64>,
@@ -227,7 +208,6 @@ pub struct Api {
pub smtp: ApiSmtp, pub smtp: ApiSmtp,
pub security: ApiSecurity, pub security: ApiSecurity,
pub workers: ApiWorkers, pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers, pub users: ApiUsers,
} }
@@ -240,7 +220,6 @@ pub struct Pushd {
// Queues // Queues
pub message_queue: String, pub message_queue: String,
pub mass_mention_queue: String, pub mass_mention_queue: String,
pub dm_call_queue: String,
pub fr_accepted_queue: String, pub fr_accepted_queue: String,
pub fr_received_queue: String, pub fr_received_queue: String,
pub generic_queue: String, pub generic_queue: String,
@@ -271,10 +250,6 @@ impl Pushd {
self.get_routing_key(self.mass_mention_queue.clone()) self.get_routing_key(self.mass_mention_queue.clone())
} }
pub fn get_dm_call_routing_key(&self) -> String {
self.get_routing_key(self.dm_call_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String { pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone()) self.get_routing_key(self.fr_accepted_queue.clone())
} }
@@ -342,10 +317,6 @@ pub struct FeaturesLimits {
pub message_length: usize, pub message_length: usize,
pub message_attachments: usize, pub message_attachments: usize,
pub servers: usize, pub servers: usize,
pub voice_quality: u32,
pub video: bool,
pub video_resolution: [u32; 2],
pub video_aspect_ratio: [f32; 2],
pub file_upload_size_limit: HashMap<String, usize>, pub file_upload_size_limit: HashMap<String, usize>,
} }
@@ -390,12 +361,10 @@ pub struct Features {
pub struct Sentry { pub struct Sentry {
pub api: String, pub api: String,
pub events: String, pub events: String,
pub voice_ingress: String,
pub files: String, pub files: String,
pub proxy: String, pub proxy: String,
pub pushd: String, pub pushd: String,
pub crond: String, pub crond: String,
pub gifbox: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
+7 -12
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,19 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.9", path = "../config", features = [ revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.9", path = "../result" } revolt-result = { version = "0.8.8", path = "../result" }
revolt-models = { version = "0.8.9", path = "../models", features = [ revolt-models = { version = "0.8.8", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.9", path = "../presence" } revolt-presence = { version = "0.8.8", path = "../presence" }
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [ revolt-permissions = { version = "0.8.8", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
revolt-parser = { version = "0.8.9", path = "../parser" } revolt-parser = { version = "0.8.8", path = "../parser" }
# Utility # Utility
log = "0.4" log = "0.4"
@@ -96,8 +96,3 @@ authifier = { version = "1.0.15" }
# RabbitMQ # RabbitMQ
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
# Voice
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] }
+1 -77
View File
@@ -2,8 +2,7 @@ use std::collections::HashSet;
use crate::events::rabbit::*; use crate::events::rabbit::*;
use crate::User; use crate::User;
use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments}; use amqprs::channel::BasicPublishArguments;
use amqprs::connection::OpenConnectionArguments;
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError}; use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
use amqprs::{BasicProperties, FieldTable}; use amqprs::{BasicProperties, FieldTable};
use revolt_models::v0::PushNotification; use revolt_models::v0::PushNotification;
@@ -26,35 +25,6 @@ impl AMQP {
} }
} }
pub async fn new_auto() -> AMQP {
let config = revolt_config::config().await;
let connection = Connection::open(&OpenConnectionArguments::new(
&config.rabbit.host,
config.rabbit.port,
&config.rabbit.username,
&config.rabbit.password,
))
.await
.expect("Failed to connect to RabbitMQ");
let channel = connection
.open_channel(None)
.await
.expect("Failed to open RabbitMQ channel");
channel
.exchange_declare(
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
.durable(true)
.finish(),
)
.await
.expect("Failed to declare exchange");
AMQP::new(connection, channel)
}
pub async fn friend_request_accepted( pub async fn friend_request_accepted(
&self, &self,
accepted_request_user: &User, accepted_request_user: &User,
@@ -270,50 +240,4 @@ impl AMQP {
) )
.await .await
} }
/// # DM Call Update
/// Used to send an update about a DM call, eg. start or end of a call.
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
/// `ended` refers to the ringing period, not necessarily the call itself.
pub async fn dm_call_updated(
&self,
initiator_id: &str,
channel_id: &str,
started_at: Option<&str>,
ended: bool,
recipients: Option<Vec<String>>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = InternalDmCallPayload {
payload: DmCallPayload {
initiator_id: initiator_id.to_string(),
channel_id: channel_id.to_string(),
started_at: started_at.map(|f| f.to_string()),
ended,
},
recipients,
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending dm call update payload on channel {}: {}",
config.pushd.get_dm_call_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_dm_call_routing_key(),
),
)
.await
}
} }
+1 -1
View File
@@ -35,7 +35,7 @@ pub enum DatabaseInfo {
} }
/// Database /// Database
#[derive(Clone, Debug)] #[derive(Clone)]
pub enum Database { pub enum Database {
/// Mock database /// Mock database
Reference(ReferenceDb), Reference(ReferenceDb),
@@ -11,7 +11,6 @@ use serde::Serialize;
database_derived!( database_derived!(
/// MongoDB implementation /// MongoDB implementation
#[derive(Debug)]
pub struct MongoDb(pub ::mongodb::Client, pub String); pub struct MongoDb(pub ::mongodb::Client, pub String);
); );
@@ -10,7 +10,7 @@ use crate::{
database_derived!( database_derived!(
/// Reference implementation /// Reference implementation
#[derive(Default, Debug)] #[derive(Default)]
pub struct ReferenceDb { pub struct ReferenceDb {
pub bots: Arc<Mutex<HashMap<String, Bot>>>, pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>, pub channels: Arc<Mutex<HashMap<String, Channel>>>,
+14 -57
View File
@@ -3,7 +3,10 @@ use revolt_result::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, Webhook,
}; };
use crate::Database; use crate::Database;
@@ -17,33 +20,16 @@ pub enum Ping {
} }
/// Fields provided in Ready payload /// Fields provided in Ready payload
#[derive(PartialEq, Debug, Clone, Deserialize)] #[derive(PartialEq)]
pub struct ReadyPayloadFields { pub enum ReadyPayloadFields {
pub users: bool, Users,
pub servers: bool, Servers,
pub channels: bool, Channels,
pub members: bool, Members,
pub emojis: bool, Emoji,
pub voice_states: bool,
pub user_settings: Vec<String>,
pub channel_unreads: bool,
pub policy_changes: bool,
}
impl Default for ReadyPayloadFields { UserSettings(Vec<String>),
fn default() -> Self { ChannelUnreads,
Self {
users: true,
servers: true,
channels: true,
members: true,
emojis: true,
voice_states: true,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: true,
}
}
} }
/// Protocol Events /// Protocol Events
@@ -71,16 +57,13 @@ pub enum EventV1 {
members: Option<Vec<Member>>, members: Option<Vec<Member>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
emojis: Option<Vec<Emoji>>, emojis: Option<Vec<Emoji>>,
#[serde(skip_serializing_if = "Option::is_none")]
voice_states: Option<Vec<ChannelVoiceState>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
user_settings: Option<UserSettings>, user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>, channel_unreads: Option<Vec<ChannelUnread>>,
#[serde(skip_serializing_if = "Option::is_none")] policy_changes: Vec<PolicyChange>,
policy_changes: Option<Vec<PolicyChange>>,
}, },
/// Ping response /// Ping response
@@ -139,7 +122,6 @@ pub enum EventV1 {
server: Server, server: Server,
channels: Vec<Channel>, channels: Vec<Channel>,
emojis: Vec<Emoji>, emojis: Vec<Emoji>,
voice_states: Vec<ChannelVoiceState>
}, },
/// Update existing server /// Update existing server
@@ -272,31 +254,6 @@ pub enum EventV1 {
/// Auth events /// Auth events
Auth(AuthifierEvent), Auth(AuthifierEvent),
/// Voice events
VoiceChannelJoin {
id: String,
state: UserVoiceState,
},
VoiceChannelLeave {
id: String,
user: String,
},
VoiceChannelMove {
user: String,
from: String,
to: String,
state: UserVoiceState
},
UserVoiceStateUpdate {
id: String,
channel_id: String,
data: PartialUserVoiceState,
},
UserMoveVoiceChannel {
node: String,
token: String
}
} }
impl EventV1 { impl EventV1 {
-15
View File
@@ -37,20 +37,6 @@ pub struct GenericPayload {
pub user: User, pub user: User,
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct DmCallPayload {
pub initiator_id: String,
pub channel_id: String,
pub started_at: Option<String>,
pub ended: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InternalDmCallPayload {
pub payload: DmCallPayload,
pub recipients: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")] #[serde(tag = "type", content = "data")]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@@ -60,7 +46,6 @@ pub enum PayloadKind {
FRReceived(FRReceivedPayload), FRReceived(FRReceivedPayload),
BadgeUpdate(usize), BadgeUpdate(usize),
Generic(GenericPayload), Generic(GenericPayload),
DmCallStartEnd(DmCallPayload),
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
-3
View File
@@ -112,9 +112,6 @@ pub mod tasks;
mod amqp; mod amqp;
pub use amqp::amqp::AMQP; pub use amqp::amqp::AMQP;
pub mod voice;
/// Utility function to check if a boolean value is false /// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool { pub fn if_false(t: &bool) -> bool {
!t !t
@@ -9,13 +9,14 @@ use crate::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document}, bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions, options::FindOptions,
}, },
AbstractServers, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE, AbstractChannels, AbstractServers, Channel, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
}; };
use bson::{oid::ObjectId, to_bson}; use bson::{oid::ObjectId, to_bson};
use futures::StreamExt; use futures::StreamExt;
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_permissions::{ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS}; use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -25,7 +26,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 48; // MUST BE +1 to last migration pub const LATEST_REVISION: i32 = 42; // 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");
@@ -913,7 +914,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
if revision <= 26 { if revision <= 26 {
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging."); info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
auto_derived!( auto_derived!(
@@ -1080,14 +1080,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
channel_id: String, channel_id: String,
} }
#[allow(clippy::enum_variant_names)]
#[derive(serde::Serialize, serde::Deserialize)]
enum Channel {
Group { owner: String },
TextChannel { server: String },
VoiceChannel { server: String }
}
let webhooks = db let webhooks = db
.db() .db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
@@ -1099,8 +1091,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.await; .await;
for webhook in webhooks { for webhook in webhooks {
match db.col::<Channel>("channels").find_one(doc! { "_id": &webhook.channel_id }).await.unwrap() { match db.fetch_channel(&webhook.channel_id).await {
Some(channel) => { Ok(channel) => {
let creator_id = match channel { let creator_id = match channel {
Channel::Group { owner, .. } => owner, Channel::Group { owner, .. } => owner,
Channel::TextChannel { server, .. } Channel::TextChannel { server, .. }
@@ -1108,6 +1100,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let server = db.fetch_server(&server).await.expect("server"); let server = db.fetch_server(&server).await.expect("server");
server.owner server.owner
} }
_ => unreachable!("not server or group channel!"),
}; };
db.db() db.db()
@@ -1125,13 +1118,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.await .await
.expect("update webhook"); .expect("update webhook");
} }
None => { Err(Error {
error_type: ErrorType::NotFound,
..
}) => {
db.db() db.db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }) .delete_one(doc! { "_id": webhook._id })
.await .await
.expect("failed to delete invalid webhook"); .expect("failed to delete invalid webhook");
} }
Err(err) => panic!("{err:?}"),
} }
} }
} }
@@ -1172,9 +1169,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("failed to update users"); .expect("failed to update users");
} }
if revision <= 43 { if revision <= 41 {
info!( info!(
"Running migration [revision 43 / 05-06-2025]: convert role ranks to uniform numbers." "Running migration [revision 41 / 05-06-2025]: convert role ranks to uniform numbers."
); );
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@@ -1229,41 +1226,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
} }
if revision <= 46 {
info!("Running migration [revision 46 / 29-04-2025]: Convert all `VoiceChannel`'s into `TextChannel`");
db.col::<Document>("channels")
.update_many(
doc! { "channel_type": "VoiceChannel" },
doc! {
"$set": {
"channel_type": "TextChannel",
"voice": {}
}
}
)
.await
.expect("Failed to update voice channels");
};
if revision <= 47 {
info!("Running migration [revision 47 / 29-04-2025]: Add Video to default permissions");
db.col::<Document>("servers")
.update_many(
doc! { },
doc! {
"$bit": {
"default_permissions": {
"or": ChannelPermission::Video as i64
},
}
}
)
.await
.expect("Failed to update default_permissions");
};
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision) LATEST_REVISION.max(revision)
} }
@@ -69,7 +69,7 @@ impl Invite {
creator: creator.id.clone(), creator: creator.id.clone(),
channel: id.clone(), channel: id.clone(),
}), }),
Channel::TextChannel { id, server, .. } => { Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
Ok(Invite::Server { Ok(Invite::Server {
code, code,
creator: creator.id.clone(), creator: creator.id.clone(),
@@ -1,5 +1,4 @@
#![allow(deprecated)] use std::collections::HashMap;
use std::{borrow::Cow, collections::HashMap};
use revolt_config::config; use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor}; use revolt_models::v0::{self, MessageAuthor};
@@ -9,7 +8,8 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use crate::{ use crate::{
events::client::EventV1, Database, File, PartialServer, Server, SystemMessage, User, AMQP, events::client::EventV1, Database, File, PartialServer,
Server, SystemMessage, User, AMQP,
}; };
#[cfg(feature = "mongodb")] #[cfg(feature = "mongodb")]
@@ -106,18 +106,38 @@ auto_derived!(
/// Whether this channel is marked as not safe for work /// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)] #[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool, nsfw: bool,
/// Voice Information for when this channel is also a voice channel
#[serde(skip_serializing_if = "Option::is_none")]
voice: Option<VoiceInformation>,
}, },
} /// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
#[derive(Default)] /// Display name of the channel
pub struct VoiceInformation { name: String,
/// Maximium amount of users allowed in the voice channel at once
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_users: Option<usize>, /// Channel description
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
} }
); );
@@ -144,8 +164,6 @@ auto_derived!(
pub default_permissions: Option<OverrideField>, pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>, pub last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceInformation>,
} }
/// Optional fields on channel object /// Optional fields on channel object
@@ -153,7 +171,6 @@ auto_derived!(
Description, Description,
Icon, Icon,
DefaultPermissions, DefaultPermissions,
Voice,
} }
); );
@@ -205,19 +222,16 @@ impl Channel {
default_permissions: None, default_permissions: None,
role_permissions: HashMap::new(), role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false), nsfw: data.nsfw.unwrap_or(false),
voice: data.voice.map(|voice| voice.into()),
}, },
v0::LegacyServerChannelType::Voice => Channel::TextChannel { v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(), id: id.clone(),
server: server.id.to_owned(), server: server.id.to_owned(),
name: data.name, name: data.name,
description: data.description, description: data.description,
icon: None, icon: None,
last_message_id: None,
default_permissions: None, default_permissions: None,
role_permissions: HashMap::new(), role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false), nsfw: data.nsfw.unwrap_or(false),
voice: Some(data.voice.unwrap_or_default().into()),
}, },
}; };
@@ -418,28 +432,8 @@ impl Channel {
Channel::DirectMessage { id, .. } Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } | Channel::Group { id, .. }
| Channel::SavedMessages { id, .. } | Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. } => id, | Channel::TextChannel { id, .. }
} | Channel::VoiceChannel { id, .. } => id,
}
/// Clone this channel's server id
pub fn server(&self) -> Option<&str> {
match self {
Channel::TextChannel { server, .. } => Some(server),
_ => None,
}
}
/// Gets this channel's voice information
pub fn voice(&self) -> Option<Cow<VoiceInformation>> {
match self {
Self::DirectMessage { .. } | Self::Group { .. } => {
Some(Cow::Owned(VoiceInformation::default()))
}
Self::TextChannel {
voice: Some(voice), ..
} => Some(Cow::Borrowed(voice)),
_ => None,
} }
} }
@@ -456,6 +450,12 @@ impl Channel {
server, server,
role_permissions, role_permissions,
.. ..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => { } => {
db.set_channel_role_permission(id, role_id, permissions) db.set_channel_role_permission(id, role_id, permissions)
.await?; .await?;
@@ -502,7 +502,7 @@ impl Channel {
clear: remove.into_iter().map(|v| v.into()).collect(), clear: remove.into_iter().map(|v| v.into()).collect(),
} }
.p(match self { .p(match self {
Self::TextChannel { server, .. } => server.clone(), Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id, _ => id,
}) })
.await; .await;
@@ -514,13 +514,17 @@ impl Channel {
pub fn remove_field(&mut self, field: &FieldsChannel) { pub fn remove_field(&mut self, field: &FieldsChannel) {
match field { match field {
FieldsChannel::Description => match self { FieldsChannel::Description => match self {
Self::Group { description, .. } | Self::TextChannel { description, .. } => { Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
description.take(); description.take();
} }
_ => {} _ => {}
}, },
FieldsChannel::Icon => match self { FieldsChannel::Icon => match self {
Self::Group { icon, .. } | Self::TextChannel { icon, .. } => { Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
icon.take(); icon.take();
} }
_ => {} _ => {}
@@ -529,17 +533,15 @@ impl Channel {
Self::TextChannel { Self::TextChannel {
default_permissions, default_permissions,
.. ..
}
| Self::VoiceChannel {
default_permissions,
..
} => { } => {
default_permissions.take(); default_permissions.take();
} }
_ => {} _ => {}
}, },
FieldsChannel::Voice => match self {
Self::TextChannel { voice, .. } => {
voice.take();
}
_ => {}
},
} }
} }
@@ -551,7 +553,6 @@ impl Channel {
} }
/// Apply partial channel to channel /// Apply partial channel to channel
#[allow(deprecated)]
pub fn apply_options(&mut self, partial: PartialChannel) { pub fn apply_options(&mut self, partial: PartialChannel) {
match self { match self {
Self::SavedMessages { .. } => {} Self::SavedMessages { .. } => {}
@@ -600,7 +601,15 @@ impl Channel {
nsfw, nsfw,
default_permissions, default_permissions,
role_permissions, role_permissions,
voice, ..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
.. ..
} => { } => {
if let Some(v) = partial.name { if let Some(v) = partial.name {
@@ -626,10 +635,6 @@ impl Channel {
if let Some(v) = partial.default_permissions { if let Some(v) = partial.default_permissions {
default_permissions.replace(v); default_permissions.replace(v);
} }
if let Some(v) = partial.voice {
voice.replace(v);
}
} }
} }
} }
@@ -772,7 +777,6 @@ impl IntoDocumentPath for FieldsChannel {
FieldsChannel::Description => "description", FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon", FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions", FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Voice => "voice",
}) })
} }
} }
@@ -184,7 +184,7 @@ impl AbstractChannels for MongoDb {
async fn delete_channel(&self, channel: &Channel) -> Result<()> { async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let id = channel.id().to_string(); let id = channel.id().to_string();
let server_id = match channel { let server_id = match channel {
Channel::TextChannel { server, .. } => { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server) Some(server)
} }
_ => None, _ => None,
@@ -94,6 +94,9 @@ impl AbstractChannels for ReferenceDb {
match &mut channel { match &mut channel {
Channel::TextChannel { Channel::TextChannel {
role_permissions, .. role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => { } => {
if role_permissions.get(role_id).is_some() { if role_permissions.get(role_id).is_some() {
role_permissions.remove(role_id); role_permissions.remove(role_id);
@@ -114,11 +114,6 @@ auto_derived!(
MessagePinned { id: String, by: String }, MessagePinned { id: String, by: String },
#[serde(rename = "message_unpinned")] #[serde(rename = "message_unpinned")]
MessageUnpinned { id: String, by: String }, MessageUnpinned { id: String, by: String },
#[serde(rename = "call_started")]
CallStarted {
by: String,
finished_at: Option<Timestamp>,
},
} }
/// Name and / or avatar override information /// Name and / or avatar override information
@@ -331,7 +326,9 @@ impl Message {
} }
let server_id = match channel { let server_id = match channel {
Channel::TextChannel { ref server, .. } => Some(server.clone()), Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
Some(server.clone())
}
_ => None, _ => None,
}; };
@@ -481,7 +478,6 @@ impl Message {
// Validate the mentions go to users in the channel/server // Validate the mentions go to users in the channel/server
if !user_mentions.is_empty() { if !user_mentions.is_empty() {
#[allow(deprecated)]
match channel { match channel {
Channel::DirectMessage { ref recipients, .. } Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => { | Channel::Group { ref recipients, .. } => {
@@ -489,7 +485,8 @@ impl Message {
user_mentions.retain(|m| recipients_hash.contains(m)); user_mentions.retain(|m| recipients_hash.contains(m));
role_mentions.clear(); role_mentions.clear();
} }
Channel::TextChannel { ref server, .. }=> { Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned()); let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await; let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
@@ -681,6 +678,7 @@ impl Message {
) )
.await?; .await?;
if !self.has_suppressed_notifications() if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention()) && (self.mentions.is_some() || self.contains_mass_push_mention())
{ {
@@ -796,7 +794,7 @@ impl Message {
query: MessageQuery, query: MessageQuery,
perspective: &User, perspective: &User,
include_users: Option<bool>, include_users: Option<bool>,
server_id: Option<&str>, server_id: Option<String>,
) -> Result<BulkMessageResponse> { ) -> Result<BulkMessageResponse> {
let messages: Vec<v0::Message> = db let messages: Vec<v0::Message> = db
.fetch_messages(query) .fetch_messages(query)
@@ -839,7 +837,6 @@ impl Message {
v0::SystemMessage::MessageUnpinned { by, .. } => { v0::SystemMessage::MessageUnpinned { by, .. } => {
users.push(by.clone()); users.push(by.clone());
} }
v0::SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
} }
} }
users users
@@ -854,7 +851,7 @@ impl Message {
users, users,
members: if let Some(server_id) = server_id { members: if let Some(server_id) = server_id {
Some( Some(
db.fetch_members(server_id, &user_ids) db.fetch_members(&server_id, &user_ids)
.await? .await?
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
@@ -1,21 +1,12 @@
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, Database, File,
Database, File, Server, SystemMessage, User, Server, SystemMessage, User,
}; };
fn default_true() -> bool {
true
}
fn is_true(x: &bool) -> bool {
*x
}
auto_derived_partial!( auto_derived_partial!(
/// Server Member /// Server Member
pub struct Member { pub struct Member {
@@ -39,14 +30,6 @@ auto_derived_partial!(
/// Timestamp this member is timed out until /// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_publish: bool,
/// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_receive: bool,
// This value only exists in the database, not the models. // This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries. // If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp> // pub pending_deletion_at: Option<Timestamp>
@@ -70,8 +53,6 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
CanReceive,
CanPublish,
JoinedAt, JoinedAt,
} }
@@ -92,8 +73,6 @@ impl Default for Member {
avatar: None, avatar: None,
roles: vec![], roles: vec![],
timeout: None, timeout: None,
can_publish: true,
can_receive: true,
} }
} }
} }
@@ -148,14 +127,6 @@ impl Member {
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
let mut voice_states = Vec::new();
for channel in &channels {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
EventV1::ServerMemberJoin { EventV1::ServerMemberJoin {
id: server.id.clone(), id: server.id.clone(),
user: user.id.clone(), user: user.id.clone(),
@@ -173,7 +144,6 @@ impl Member {
.map(|channel| channel.into()) .map(|channel| channel.into())
.collect(), .collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(), emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
voice_states
} }
.private(user.id.clone()) .private(user.id.clone())
.await; .await;
@@ -228,8 +198,6 @@ impl Member {
FieldsMember::Nickname => self.nickname = None, FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(), FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None, FieldsMember::Timeout => self.timeout = None,
FieldsMember::CanReceive => self.can_receive = true,
FieldsMember::CanPublish => self.can_publish = true,
} }
} }
@@ -336,8 +336,6 @@ 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::CanReceive => "is_receiving",
}) })
} }
} }
@@ -1,20 +1,21 @@
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts}; use axum::{
extract::{FromRef, FromRequestParts},
http::request::Parts,
};
use revolt_result::{create_error, Error, Result}; use revolt_result::{create_error, Error, Result};
use crate::{Database, User}; use crate::{Database, User};
#[async_trait::async_trait] #[async_trait::async_trait]
impl<S> FromRequestParts<S> for User impl<S: Send + Sync> FromRequestParts<S> for User
where where
Database: FromRef<S>, Database: FromRef<S>,
S: Send + Sync
{ {
type Rejection = Error; type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> { async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
let db = Database::from_ref(state); let db = Database::from_ref(state);
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) { if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?; let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await db.fetch_user(&bot.id).await
+6 -3
View File
@@ -14,7 +14,7 @@ use validator::HasLen;
use revolt_result::Result; use revolt_result::Result;
use super::DelayedTask; use super::DelayedTask;
use crate::Channel::TextChannel; use crate::Channel::{TextChannel, VoiceChannel};
/// Enumeration of possible events /// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@@ -191,17 +191,20 @@ pub async fn handle_ack_event(
.await .await
.expect("Failed to fetch channel from db"); .expect("Failed to fetch channel from db");
if let TextChannel { server, .. } = channel { match channel {
TextChannel { server, .. } | VoiceChannel { server, .. } => {
if let Err(err) = if let Err(err) =
amqp.mass_mention_message_sent(server, mass_mentions).await amqp.mass_mention_message_sent(server, mass_mentions).await
{ {
revolt_config::capture_error(&err); revolt_config::capture_error(&err);
} }
} else { }
_ => {
panic!("Unknown channel type when sending mass mention event"); panic!("Unknown channel type when sending mass mention event");
} }
} }
} }
}
}; };
Ok(()) Ok(())
+38 -39
View File
@@ -143,7 +143,6 @@ impl From<crate::FieldsWebhook> for FieldsWebhook {
} }
impl From<crate::Channel> for Channel { impl From<crate::Channel> for Channel {
#[allow(deprecated)]
fn from(value: crate::Channel) -> Self { fn from(value: crate::Channel) -> Self {
match value { match value {
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user }, crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
@@ -189,7 +188,6 @@ impl From<crate::Channel> for Channel {
default_permissions, default_permissions,
role_permissions, role_permissions,
nsfw, nsfw,
voice,
} => Channel::TextChannel { } => Channel::TextChannel {
id, id,
server, server,
@@ -200,14 +198,31 @@ impl From<crate::Channel> for Channel {
default_permissions, default_permissions,
role_permissions, role_permissions,
nsfw, nsfw,
voice: voice.map(|voice| voice.into()), },
crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
}, },
} }
} }
} }
impl From<Channel> for crate::Channel { impl From<Channel> for crate::Channel {
#[allow(deprecated)]
fn from(value: Channel) -> crate::Channel { fn from(value: Channel) -> crate::Channel {
match value { match value {
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user }, Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
@@ -253,7 +268,6 @@ impl From<Channel> for crate::Channel {
default_permissions, default_permissions,
role_permissions, role_permissions,
nsfw, nsfw,
voice,
} => crate::Channel::TextChannel { } => crate::Channel::TextChannel {
id, id,
server, server,
@@ -264,7 +278,25 @@ impl From<Channel> for crate::Channel {
default_permissions, default_permissions,
role_permissions, role_permissions,
nsfw, nsfw,
voice: voice.map(|voice| voice.into()), },
Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
}, },
} }
} }
@@ -283,7 +315,6 @@ impl From<crate::PartialChannel> for PartialChannel {
role_permissions: value.role_permissions, role_permissions: value.role_permissions,
default_permissions: value.default_permissions, default_permissions: value.default_permissions,
last_message_id: value.last_message_id, last_message_id: value.last_message_id,
voice: value.voice.map(|voice| voice.into())
} }
} }
} }
@@ -301,7 +332,6 @@ impl From<PartialChannel> for crate::PartialChannel {
role_permissions: value.role_permissions, role_permissions: value.role_permissions,
default_permissions: value.default_permissions, default_permissions: value.default_permissions,
last_message_id: value.last_message_id, last_message_id: value.last_message_id,
voice: value.voice.map(|voice| voice.into())
} }
} }
} }
@@ -312,7 +342,6 @@ impl From<FieldsChannel> for crate::FieldsChannel {
FieldsChannel::Description => crate::FieldsChannel::Description, FieldsChannel::Description => crate::FieldsChannel::Description,
FieldsChannel::Icon => crate::FieldsChannel::Icon, FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions, FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
FieldsChannel::Voice => crate::FieldsChannel::Voice,
} }
} }
} }
@@ -323,7 +352,6 @@ impl From<crate::FieldsChannel> for FieldsChannel {
crate::FieldsChannel::Description => FieldsChannel::Description, crate::FieldsChannel::Description => FieldsChannel::Description,
crate::FieldsChannel::Icon => FieldsChannel::Icon, crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions, crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
crate::FieldsChannel::Voice => FieldsChannel::Voice,
} }
} }
} }
@@ -515,7 +543,6 @@ impl From<crate::SystemMessage> for SystemMessage {
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by }, crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by }, crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by }, crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
crate::SystemMessage::CallStarted { by, finished_at } => Self::CallStarted { by, finished_at }
} }
} }
} }
@@ -612,8 +639,6 @@ impl From<crate::Member> for Member {
avatar: value.avatar.map(|f| f.into()), avatar: value.avatar.map(|f| f.into()),
roles: value.roles, roles: value.roles,
timeout: value.timeout, timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
} }
} }
} }
@@ -627,8 +652,6 @@ impl From<Member> for crate::Member {
avatar: value.avatar.map(|f| f.into()), avatar: value.avatar.map(|f| f.into()),
roles: value.roles, roles: value.roles,
timeout: value.timeout, timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
} }
} }
} }
@@ -642,8 +665,6 @@ impl From<crate::PartialMember> for PartialMember {
avatar: value.avatar.map(|f| f.into()), avatar: value.avatar.map(|f| f.into()),
roles: value.roles, roles: value.roles,
timeout: value.timeout, timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
} }
} }
} }
@@ -657,8 +678,6 @@ impl From<PartialMember> for crate::PartialMember {
avatar: value.avatar.map(|f| f.into()), avatar: value.avatar.map(|f| f.into()),
roles: value.roles, roles: value.roles,
timeout: value.timeout, timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
} }
} }
} }
@@ -688,8 +707,6 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname, crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles, crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout, crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::CanReceive => FieldsMember::CanReceive,
crate::FieldsMember::CanPublish => FieldsMember::CanPublish,
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt, crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
} }
} }
@@ -702,8 +719,6 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname, FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles, FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout, FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::CanReceive => crate::FieldsMember::CanReceive,
FieldsMember::CanPublish => crate::FieldsMember::CanPublish,
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt, FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
} }
} }
@@ -1372,19 +1387,3 @@ impl From<FieldsMessage> for crate::FieldsMessage {
} }
} }
} }
impl From<VoiceInformation> for crate::VoiceInformation {
fn from(value: VoiceInformation) -> Self {
crate::VoiceInformation {
max_users: value.max_users
}
}
}
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users
}
}
}
@@ -144,6 +144,10 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
Channel::TextChannel { Channel::TextChannel {
default_permissions, default_permissions,
.. ..
}
| Channel::VoiceChannel {
default_permissions,
..
} => default_permissions.unwrap_or_default().into(), } => default_permissions.unwrap_or_default().into(),
_ => Default::default(), _ => Default::default(),
} }
@@ -152,14 +156,16 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
} }
} }
#[allow(dead_code, deprecated)] #[allow(dead_code)]
fn get_channel_type(&mut self) -> ChannelType { fn get_channel_type(&mut self) -> ChannelType {
if let Some(channel) = &self.channel { if let Some(channel) = &self.channel {
match channel { match channel {
Channel::DirectMessage { .. } => ChannelType::DirectMessage, Channel::DirectMessage { .. } => ChannelType::DirectMessage,
Channel::Group { .. } => ChannelType::Group, Channel::Group { .. } => ChannelType::Group,
Channel::SavedMessages { .. } => ChannelType::SavedMessages, Channel::SavedMessages { .. } => ChannelType::SavedMessages,
Channel::TextChannel { .. } => ChannelType::ServerChannel, Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
ChannelType::ServerChannel
}
} }
} else { } else {
ChannelType::Unknown ChannelType::Unknown
@@ -173,6 +179,9 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
match channel { match channel {
Channel::TextChannel { Channel::TextChannel {
role_permissions, .. role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => role_permissions, } => role_permissions,
_ => panic!("Not supported for non-server channels"), _ => panic!("Not supported for non-server channels"),
} }
@@ -199,6 +208,12 @@ async fn calculate_members_permissions<'a>(
role_permissions, role_permissions,
default_permissions, default_permissions,
.. ..
}
| Channel::VoiceChannel {
id,
role_permissions,
default_permissions,
..
} => (id, role_permissions, default_permissions), } => (id, role_permissions, default_permissions),
_ => panic!("Calculation of member permissions must be done on a server channel"), _ => panic!("Calculation of member permissions must be done on a server channel"),
}; };
-24
View File
@@ -1,24 +0,0 @@
use crate::Database;
use revolt_result::Result;
/// Formats a user's name depending on their optional features and location.
/// Factors in server display names and user display names before falling back to username#discriminator.
/// Passing a server in which the user is not a member will result in an Err.
pub async fn format_display_name(
db: &Database,
user_id: &str,
server_id: Option<&str>,
) -> Result<String> {
if let Some(server_id) = server_id {
let member = db.fetch_member(server_id, user_id).await?;
if let Some(nick) = member.nickname {
return Ok(nick);
}
}
let user = db.fetch_user(user_id).await?;
if let Some(display) = user.display_name {
return Ok(display);
}
Ok(format!("{}#{}", user.username, user.discriminator))
}
-3
View File
@@ -1,9 +1,6 @@
pub mod bridge; pub mod bridge;
pub mod bulk_permissions; pub mod bulk_permissions;
mod funcs;
pub mod idempotency; pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
pub mod test_fixtures; pub mod test_fixtures;
pub use funcs::*;
+20 -20
View File
@@ -185,26 +185,9 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
} }
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_publish
} else {
true
}
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_receive
} else {
true
}
}
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel
#[allow(deprecated)]
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
if let Some(channel) = &self.channel { if let Some(channel) = &self.channel {
match channel { match channel {
@@ -216,7 +199,9 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
Cow::Borrowed(Channel::SavedMessages { .. }) Cow::Borrowed(Channel::SavedMessages { .. })
| Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages, | Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages,
Cow::Borrowed(Channel::TextChannel { .. }) Cow::Borrowed(Channel::TextChannel { .. })
| Cow::Owned(Channel::TextChannel { .. }) => ChannelType::ServerChannel, | Cow::Owned(Channel::TextChannel { .. })
| Cow::Borrowed(Channel::VoiceChannel { .. })
| Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel,
} }
} else { } else {
ChannelType::Unknown ChannelType::Unknown
@@ -240,6 +225,14 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
| Cow::Owned(Channel::TextChannel { | Cow::Owned(Channel::TextChannel {
default_permissions, default_permissions,
.. ..
})
| Cow::Borrowed(Channel::VoiceChannel {
default_permissions,
..
})
| Cow::Owned(Channel::VoiceChannel {
default_permissions,
..
}) => default_permissions.unwrap_or_default().into(), }) => default_permissions.unwrap_or_default().into(),
_ => Default::default(), _ => Default::default(),
} }
@@ -257,6 +250,12 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
}) })
| Cow::Owned(Channel::TextChannel { | Cow::Owned(Channel::TextChannel {
role_permissions, .. role_permissions, ..
})
| Cow::Borrowed(Channel::VoiceChannel {
role_permissions, ..
})
| Cow::Owned(Channel::VoiceChannel {
role_permissions, ..
}) => { }) => {
if let Some(server) = &self.server { if let Some(server) = &self.server {
let member_roles = self let member_roles = self
@@ -344,10 +343,11 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// (this will only ever be called for server channels, use unimplemented!() for other code paths) /// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self) { async fn set_server_from_channel(&mut self) {
if let Some(channel) = &self.channel { if let Some(channel) = &self.channel {
#[allow(deprecated)]
match channel { match channel {
Cow::Borrowed(Channel::TextChannel { server, .. }) Cow::Borrowed(Channel::TextChannel { server, .. })
| Cow::Owned(Channel::TextChannel { server, .. }) => { | Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
if let Some(known_server) = if let Some(known_server) =
// I'm not sure why I can't just pattern match both at once here? // I'm not sure why I can't just pattern match both at once here?
// It throws some weird error and the provided fix doesn't work :/ // It throws some weird error and the provided fix doesn't work :/
-564
View File
@@ -1,564 +0,0 @@
use crate::{
events::client::EventV1,
models::{Channel, User},
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Server,
};
use iso8601_timestamp::{Duration, Timestamp};
use livekit_protocol::ParticipantPermission;
use redis_kiss::{get_connection as _get_connection, redis::Pipeline, AsyncCommands, Conn};
use revolt_config::FeaturesLimits;
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{create_error, Result, ToRevoltError};
mod voice_client;
pub use voice_client::VoiceClient;
async fn get_connection() -> Result<Conn> {
_get_connection().await.to_internal_error()
}
pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
let mut conn = get_connection().await?;
if user.bot.is_some()
// bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to
&& conn.sismember(format!("vc:{}", &user.id), channel_id)
.await
.to_internal_error()?
{
Err(create_error!(AlreadyConnected))
} else if conn
.scard::<_, u32>(format!("vc:{}", &user.id)) // check if the current vc set is empty
.await
.to_internal_error()?
> 0
{
Err(create_error!(NotConnected))
} else {
Ok(())
}
}
pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> {
get_connection()
.await?
.set(format!("node:{channel}"), node)
.await
.to_internal_error()
}
pub async fn get_channel_node(channel: &str) -> Result<Option<String>> {
get_connection()
.await?
.get(format!("node:{channel}"))
.await
.to_internal_error()
}
pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> {
get_connection()
.await?
.smembers(format!("vc:{user_id}"))
.await
.to_internal_error()
}
pub async fn set_user_moved_from_voice(
old_channel: &str,
new_channel: &str,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
.set_ex(
format!("moved_from:{user_id}:{old_channel}"),
new_channel,
10,
)
.await
.to_internal_error()
}
pub async fn get_user_moved_from_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("moved_from:{user_id}:{channel_id}"))
.await
.to_internal_error()
}
pub async fn set_user_moved_to_voice(
new_channel: &str,
old_channel: &str,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
.set_ex(format!("moved_to:{user_id}:{new_channel}"), old_channel, 10)
.await
.to_internal_error()
}
pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("moved_to:{user_id}:{channel_id}"))
.await
.to_internal_error()
}
pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result<bool> {
get_connection()
.await?
.sismember(format!("vc:{user_id}"), channel_id)
.await
.to_internal_error()
}
pub async fn get_user_voice_channel_in_server(
user_id: &str,
server_id: &str,
) -> Result<Option<String>> {
let mut conn = get_connection().await?;
let unique_key = format!("{user_id}:{server_id}");
conn.get(&unique_key).await.to_internal_error()
}
pub fn get_allowed_sources(
limits: &FeaturesLimits,
permissions: PermissionValue,
) -> Vec<&'static str> {
let mut allowed_sources = Vec::new();
if permissions.has(ChannelPermission::Speak as u64) {
allowed_sources.push("microphone")
};
if permissions.has(ChannelPermission::Video as u64) && limits.video {
allowed_sources.extend(["camera", "screen_share", "screen_share_audio"]);
};
allowed_sources
}
pub async fn create_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
joined_at: Timestamp,
) -> Result<UserVoiceState> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
let voice_state = UserVoiceState {
joined_at,
id: user_id.to_string(),
is_receiving: true,
is_publishing: false,
screensharing: false,
camera: false,
};
Pipeline::new()
.sadd(format!("vc_members:{channel_id}"), user_id)
.sadd(format!("vc:{user_id}"), channel_id)
.set(&unique_key, channel_id)
.set(
format!("joined_at:{unique_key}"),
joined_at
.duration_since(Timestamp::UNIX_EPOCH)
.whole_milliseconds() as i64,
)
.set(
format!("is_publishing:{unique_key}"),
voice_state.is_publishing,
)
.set(
format!("is_receiving:{unique_key}"),
voice_state.is_receiving,
)
.set(
format!("screensharing:{unique_key}"),
voice_state.screensharing,
)
.set(format!("camera:{unique_key}"), voice_state.camera)
.query_async::<_, ()>(&mut get_connection().await?.into_inner())
.await
.to_internal_error()?;
Ok(voice_state)
}
pub async fn delete_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
) -> Result<()> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
Pipeline::new()
.srem(format!("vc_members:{channel_id}"), user_id)
.srem(format!("vc:{user_id}"), channel_id)
.del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
unique_key.clone(),
])
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn delete_channel_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_ids: &[String],
) -> Result<()> {
let parent_id = server_id.unwrap_or(channel_id);
let mut pipeline = Pipeline::new();
pipeline.del(format!("vc_members:{channel_id}"));
for user_id in user_ids {
let unique_key = format!("{user_id}:{parent_id}");
pipeline.srem(format!("vc:{user_id}"), channel_id).del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
unique_key.clone(),
]);
}
pipeline
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn update_voice_state_tracks(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
added: bool,
track: i32,
) -> Result<PartialUserVoiceState> {
let partial = match track {
/* TrackSource::Unknown */ 0 => PartialUserVoiceState::default(),
/* TrackSource::Camera */
1 => PartialUserVoiceState {
camera: Some(added),
..Default::default()
},
/* TrackSource::Microphone */
2 => PartialUserVoiceState {
is_publishing: Some(added),
..Default::default()
},
/* TrackSource::ScreenShare | TrackSource::ScreenShareAudio */
3 | 4 => PartialUserVoiceState {
screensharing: Some(added),
..Default::default()
},
_ => unreachable!(),
};
update_voice_state(channel_id, server_id, user_id, &partial).await?;
Ok(partial)
}
pub async fn update_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
partial: &PartialUserVoiceState,
) -> Result<()> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
let mut pipeline = Pipeline::new();
if let Some(camera) = &partial.camera {
pipeline.set(format!("camera:{unique_key}"), camera);
};
if let Some(is_publishing) = &partial.is_publishing {
pipeline.set(format!("is_publishing:{unique_key}"), is_publishing);
}
if let Some(is_receiving) = &partial.is_receiving {
pipeline.set(format!("is_receiving:{unique_key}"), is_receiving);
}
if let Some(screensharing) = &partial.screensharing {
pipeline.set(format!("screensharing:{unique_key}"), screensharing);
}
pipeline
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn get_voice_channel_members(channel_id: &str) -> Result<Option<Vec<String>>> {
get_connection()
.await?
.smembers::<_, Option<Vec<String>>>(format!("vc_members:{channel_id}"))
.await
.to_internal_error()
.map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) }))
}
pub async fn get_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
) -> Result<Option<UserVoiceState>> {
let unique_key = format!("{}:{}", user_id, server_id.unwrap_or(channel_id));
let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection()
.await?
.mget(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
])
.await
.to_internal_error()?;
match (
joined_at,
is_publishing,
is_receiving,
screensharing,
camera,
) {
(
Some(joined_at),
Some(is_publishing),
Some(is_receiving),
Some(screensharing),
Some(camera),
) => Ok(Some(v0::UserVoiceState {
joined_at: Timestamp::UNIX_EPOCH
.checked_add(Duration::milliseconds(joined_at))
.unwrap(),
id: user_id.to_string(),
is_receiving,
is_publishing,
screensharing,
camera,
})),
_ => Ok(None),
}
}
pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::ChannelVoiceState>> {
let members = get_voice_channel_members(channel.id()).await?;
let server = channel.server();
if let Some(members) = members {
let mut participants = Vec::with_capacity(members.len());
for user_id in members {
if let Some(voice_state) = get_voice_state(channel.id(), server, &user_id).await? {
participants.push(voice_state);
} else {
log::info!("Voice state not found but member in voice channel members, removing.");
delete_voice_state(channel.id(), server, &user_id).await?;
}
}
// In case a user voice state failed to be fetched, the vec's capacity will be larger than the length, shrink it
participants.shrink_to_fit();
Ok(Some(v0::ChannelVoiceState {
id: channel.id().to_string(),
participants,
}))
} else {
Ok(None)
}
}
pub async fn move_user(user: &str, from: &str, to: &str) -> Result<()> {
get_connection()
.await?
.smove(
format!("vc-members-{from}"),
format!("vc-members-{to}"),
user,
)
.await
.to_internal_error()
}
pub async fn sync_voice_permissions(
db: &Database,
voice_client: &VoiceClient,
channel: &Channel,
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
let node = get_channel_node(channel.id()).await?.unwrap();
for user_id in get_voice_channel_members(channel.id())
.await?
.iter()
.flatten()
{
let user = Reference::from_unchecked(user_id).as_user(db).await?;
sync_user_voice_permissions(db, voice_client, &node, &user, channel, server, role_id)
.await?;
}
Ok(())
}
pub async fn sync_user_voice_permissions(
db: &Database,
voice_client: &VoiceClient,
node: &str,
user: &User,
channel: &Channel,
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
let channel_id = channel.id();
let server_id = server.as_ref().map(|s| s.id.as_str());
let member = match server_id {
Some(server_id) => Some(
Reference::from_unchecked(&user.id)
.as_member(db, server_id)
.await?,
),
None => None,
};
if role_id.is_none_or(|role_id| {
member
.as_ref()
.is_none_or(|member| member.roles.iter().any(|r| r == role_id))
}) {
let voice_state = get_voice_state(channel_id, server_id, &user.id)
.await?
.unwrap();
let mut query = DatabasePermissionQuery::new(db, user)
.channel(channel)
.user(user);
if let (Some(server), Some(member)) = (server, member.as_ref()) {
query = query.member(member).server(server)
}
let permissions = calculate_channel_permissions(&mut query).await;
let limits = user.limits().await;
let mut update_event = PartialUserVoiceState {
id: Some(user.id.clone()),
..Default::default()
};
let before = update_event.clone();
let can_video =
limits.video && permissions.has_channel_permission(ChannelPermission::Video);
let can_speak = permissions.has_channel_permission(ChannelPermission::Speak);
let can_listen = permissions.has_channel_permission(ChannelPermission::Listen);
update_event.camera = voice_state.camera.then_some(can_video);
update_event.screensharing = voice_state.screensharing.then_some(can_video);
update_event.is_publishing = voice_state.is_publishing.then_some(can_speak);
update_voice_state(channel_id, server_id, &user.id, &update_event).await?;
voice_client
.update_permissions(
node,
user,
channel_id,
ParticipantPermission {
can_subscribe: can_listen,
can_publish: can_speak,
can_publish_data: can_speak,
..Default::default()
},
)
.await?;
if update_event != before {
EventV1::UserVoiceStateUpdate {
id: user.id.clone(),
channel_id: channel_id.to_string(),
data: update_event,
}
.p(channel_id.to_string())
.await;
};
};
Ok(())
}
pub async fn set_channel_call_started_system_message(
channel_id: &str,
message_id: &str,
) -> Result<()> {
get_connection()
.await?
.set(format!("call_started_message:{channel_id}"), message_id)
.await
.to_internal_error()
}
pub async fn take_channel_call_started_system_message(channel_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("call_started_message:{channel_id}"))
.await
.to_internal_error()
}
pub async fn set_call_notification_recipients(
channel_id: &str,
user_id: &str,
recipients: &[String],
) -> Result<()> {
get_connection()
.await?
.set_ex(
format!("call_notification_recipients:{channel_id}-{user_id}"),
recipients,
10,
)
.await
.to_internal_error()
}
pub async fn get_call_notification_recipients(
channel_id: &str,
user_id: &str,
) -> Result<Option<Vec<String>>> {
get_connection()
.await?
.get_del(format!(
"call_notification_recipients:{channel_id}-{user_id}"
))
.await
.to_internal_error()
}
@@ -1,156 +0,0 @@
use crate::{
models::{Channel, User},
Database,
};
use livekit_api::{
access_token::{AccessToken, VideoGrants},
services::room::{CreateRoomOptions, RoomClient as InnerRoomClient, UpdateParticipantOptions},
};
use livekit_protocol::{ParticipantInfo, ParticipantPermission, Room};
use revolt_config::{config, LiveKitNode};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::{create_error, Result, ToRevoltError};
use std::{collections::HashMap, time::Duration};
use super::get_allowed_sources;
#[derive(Debug)]
pub struct RoomClient {
pub client: InnerRoomClient,
pub node: LiveKitNode,
}
#[derive(Debug)]
pub struct VoiceClient {
pub rooms: HashMap<String, RoomClient>,
}
impl VoiceClient {
pub fn new(nodes: HashMap<String, LiveKitNode>) -> Self {
Self {
rooms: nodes
.into_iter()
.map(|(name, node)| {
(
name,
RoomClient {
client: InnerRoomClient::with_api_key(
&node.url,
&node.key,
&node.secret,
),
node,
},
)
})
.collect(),
}
}
pub fn is_enabled(&self) -> bool {
!self.rooms.is_empty()
}
pub async fn from_revolt_config() -> Self {
let config = config().await;
Self::new(config.api.livekit.nodes.clone())
}
pub fn get_node(&self, name: &str) -> Result<&RoomClient> {
self.rooms
.get(name)
.ok_or_else(|| create_error!(UnknownNode))
}
pub async fn create_token(
&self,
node: &str,
db: &Database,
user: &User,
permissions: PermissionValue,
channel: &Channel,
) -> Result<String> {
let room = self.get_node(node)?;
let limits = user.limits().await;
let allowed_sources = get_allowed_sources(&limits, permissions);
AccessToken::with_api_key(&room.node.key, &room.node.secret)
.with_name(&format!("{}#{}", user.username, user.discriminator))
.with_identity(&user.id)
.with_metadata(
&serde_json::to_string(&user.clone().into(db, None).await).to_internal_error()?,
)
.with_ttl(Duration::from_secs(10))
.with_grants(VideoGrants {
room_join: true,
can_publish: true,
can_publish_data: false,
can_publish_sources: allowed_sources
.into_iter()
.map(ToString::to_string)
.collect(),
can_subscribe: permissions.has_channel_permission(ChannelPermission::Listen),
room: channel.id().to_string(),
..Default::default()
})
.to_jwt()
.to_internal_error()
}
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> {
let room = self.get_node(node)?;
room.client
.create_room(
channel.id(),
CreateRoomOptions {
empty_timeout: 5 * 60, // 5 minutes,
..Default::default()
},
)
.await
.to_internal_error()
}
pub async fn update_permissions(
&self,
node: &str,
user: &User,
channel_id: &str,
new_permissions: ParticipantPermission,
) -> Result<ParticipantInfo> {
let room = self.get_node(node)?;
room.client
.update_participant(
channel_id,
&user.id,
UpdateParticipantOptions {
permission: Some(new_permissions),
..Default::default()
},
)
.await
.to_internal_error()
}
pub async fn remove_user(&self, node: &str, user_id: &str, channel_id: &str) -> Result<()> {
let room = self.get_node(node)?;
room.client
.remove_participant(channel_id, user_id)
.await
.to_internal_error()
}
pub async fn delete_room(&self, node: &str, channel_id: &str) -> Result<()> {
let room = self.get_node(node)?;
room.client
.delete_room(channel_id)
.await
.to_internal_error()
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-files" name = "revolt-files"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5" aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.9", path = "../config", features = [ revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.9", path = "../result" } revolt-result = { version = "0.8.8", path = "../result" }
# image processing # image processing
jxl-oxide = "0.8.1" jxl-oxide = "0.8.1"
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.9", path = "../config" } revolt-config = { version = "0.8.8", path = "../config" }
revolt-permissions = { version = "0.8.9", path = "../permissions" } revolt-permissions = { version = "0.8.8", path = "../permissions" }
# Utility # Utility
regex = "1.11" regex = "1.11"
+44 -52
View File
@@ -1,5 +1,4 @@
#![allow(deprecated)] use super::File;
use super::{File, UserVoiceState};
use revolt_permissions::{Override, OverrideField}; use revolt_permissions::{Override, OverrideField};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -108,21 +107,44 @@ auto_derived!(
serde(skip_serializing_if = "crate::if_false", default) serde(skip_serializing_if = "crate::if_false", default)
)] )]
nsfw: bool, nsfw: bool,
/// Voice Information for when this channel is also a voice channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
voice: Option<VoiceInformation>,
}, },
} /// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Voice information for a channel /// Display name of the channel
#[derive(Default)] name: String,
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct VoiceInformation {
/// Maximium amount of users allowed in the voice channel at once
#[cfg_attr(feature = "validator", validate(range(min = 1)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub max_users: Option<usize>, /// Channel description
description: Option<String>,
/// Custom icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
},
} }
/// Partial representation of a channel /// Partial representation of a channel
@@ -148,8 +170,6 @@ auto_derived!(
pub default_permissions: Option<OverrideField>, pub default_permissions: Option<OverrideField>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_message_id: Option<String>, pub last_message_id: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub voice: Option<VoiceInformation>,
} }
/// Optional fields on channel object /// Optional fields on channel object
@@ -157,7 +177,6 @@ auto_derived!(
Description, Description,
Icon, Icon,
DefaultPermissions, DefaultPermissions,
Voice,
} }
/// New webhook information /// New webhook information
@@ -186,9 +205,6 @@ auto_derived!(
/// Whether this channel is archived /// Whether this channel is archived
pub archived: Option<bool>, pub archived: Option<bool>,
/// Voice Information for voice channels
pub voice: Option<VoiceInformation>,
/// Fields to remove from channel /// Fields to remove from channel
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsChannel>, pub remove: Vec<FieldsChannel>,
@@ -244,10 +260,6 @@ auto_derived!(
/// Whether this channel is age restricted /// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
/// Voice Information for when this channel is also a voice channel
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceInformation>,
} }
/// New default permissions /// New default permissions
@@ -258,7 +270,7 @@ auto_derived!(
permissions: u64, permissions: u64,
}, },
Field { Field {
/// Allow / deny values to set for members in this server channel /// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override, permissions: Override,
}, },
} }
@@ -277,32 +289,9 @@ auto_derived!(
} }
/// Voice server token response /// Voice server token response
pub struct CreateVoiceUserResponse { pub struct LegacyCreateVoiceUserResponse {
/// Token for authenticating with the voice server /// Token for authenticating with the voice server
pub token: String, token: String,
/// Url of the livekit server to connect to
pub url: String,
}
/// Voice state for a channel
pub struct ChannelVoiceState {
pub id: String,
/// The states of the users who are connected to the channel
pub participants: Vec<UserVoiceState>,
}
/// Join a voice channel
pub struct DataJoinCall {
/// Name of the node to join
pub node: Option<String>,
/// Whether to force disconnect any other existing voice connections
///
/// Useful for disconnecting on another device and joining on a new.
pub force_disconnect: Option<bool>,
/// Users which should be notified of the call starting
///
/// Only used when the user is the first one connected.
pub recipients: Option<Vec<String>>,
} }
); );
@@ -313,7 +302,8 @@ impl Channel {
Channel::DirectMessage { id, .. } Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } | Channel::Group { id, .. }
| Channel::SavedMessages { id, .. } | Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. } => id, | Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
} }
} }
@@ -325,7 +315,9 @@ impl Channel {
match self { match self {
Channel::DirectMessage { .. } => None, Channel::DirectMessage { .. } => None,
Channel::SavedMessages { .. } => Some("Saved Messages"), Channel::SavedMessages { .. } => Some("Saved Messages"),
Channel::TextChannel { name, .. } | Channel::Group { name, .. } => Some(name), Channel::TextChannel { name, .. }
| Channel::Group { name, .. }
| Channel::VoiceChannel { name, .. } => Some(name),
} }
} }
} }
-3
View File
@@ -132,8 +132,6 @@ auto_derived!(
MessagePinned { id: String, by: String }, MessagePinned { id: String, by: String },
#[serde(rename = "message_unpinned")] #[serde(rename = "message_unpinned")]
MessageUnpinned { id: String, by: String }, MessageUnpinned { id: String, by: String },
#[serde(rename = "call_started")]
CallStarted { by: String, finished_at: Option<Timestamp> },
} }
/// Name and / or avatar override information /// Name and / or avatar override information
@@ -447,7 +445,6 @@ impl From<SystemMessage> for String {
} }
SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(), SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(), SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
SystemMessage::CallStarted { .. } => "Call started.".to_string(),
} }
} }
} }
@@ -31,14 +31,6 @@ pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap() Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
}); });
fn default_true() -> bool {
true
}
fn is_true(x: &bool) -> bool {
*x
}
auto_derived_partial!( auto_derived_partial!(
/// Server Member /// Server Member
pub struct Member { pub struct Member {
@@ -65,13 +57,6 @@ auto_derived_partial!(
/// Timestamp this member is timed out until /// Timestamp this member is timed out until
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_publish: bool,
/// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_receive: bool,
}, },
"PartialMember" "PartialMember"
); );
@@ -92,8 +77,6 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
CanReceive,
CanPublish,
JoinedAt, JoinedAt,
} }
@@ -141,12 +124,6 @@ auto_derived!(
pub roles: Option<Vec<String>>, pub roles: Option<Vec<String>>,
/// Timestamp this member is timed out until /// Timestamp this member is timed out until
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
/// server-wide voice muted
pub can_publish: Option<bool>,
/// server-wide voice deafened
pub can_receive: Option<bool>,
/// voice channel to move to if already in a voice channel
pub voice_channel: Option<String>,
/// Fields to remove from channel object /// Fields to remove from channel object
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsMember>, pub remove: Vec<FieldsMember>,
-14
View File
@@ -1,4 +1,3 @@
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
@@ -280,19 +279,6 @@ auto_derived!(
} }
); );
auto_derived_partial!(
/// Voice State information for a user
pub struct UserVoiceState {
pub id: String,
pub joined_at: Timestamp,
pub is_receiving: bool,
pub is_publishing: bool,
pub screensharing: bool,
pub camera: bool,
},
"PartialUserVoiceState"
);
pub trait CheckRelationship { pub trait CheckRelationship {
fn with(&self, user: &str) -> RelationshipStatus; fn with(&self, user: &str) -> RelationshipStatus;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-parser" name = "revolt-parser"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"] authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.8.9", path = "../result" } revolt-result = { version = "0.8.8", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
-9
View File
@@ -61,15 +61,6 @@ pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) ->
permissions.apply(role_override); permissions.apply(role_override);
} }
if !query.do_we_have_publish_overwrites().await {
permissions.revoke(ChannelPermission::Speak as u64);
permissions.revoke(ChannelPermission::Video as u64);
}
if !query.do_we_have_receive_overwrites().await {
permissions.revoke(ChannelPermission::Listen as u64);
}
if query.are_we_timed_out().await { if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT); permissions.restrict(*ALLOW_IN_TIMEOUT);
} }
@@ -89,8 +89,6 @@ pub enum ChannelPermission {
DeafenMembers = 1 << 34, DeafenMembers = 1 << 34,
/// Move members between voice channels /// Move members between voice channels
MoveMembers = 1 << 35, MoveMembers = 1 << 35,
/// Listen to other users
Listen = 1 << 36,
// * Channel permissions two electric boogaloo // * Channel permissions two electric boogaloo
/// Mention everyone and online members /// Mention everyone and online members
@@ -132,9 +130,7 @@ pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
+ ChannelPermission::SendEmbeds + ChannelPermission::SendEmbeds
+ ChannelPermission::UploadFiles + ChannelPermission::UploadFiles
+ ChannelPermission::Connect + ChannelPermission::Connect
+ ChannelPermission::Speak + ChannelPermission::Speak,
+ ChannelPermission::Listen
+ ChannelPermission::Video
) )
}); });
+1 -1
View File
@@ -39,7 +39,7 @@ pub enum DataPermissionPoly {
permissions: u64, permissions: u64,
}, },
Field { Field {
/// Allow / deny values to set for members in this server channel /// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override, permissions: Override,
}, },
} }
-32
View File
@@ -64,14 +64,6 @@ async fn validate_user_permissions() {
unreachable!() unreachable!()
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::DirectMessage ChannelType::DirectMessage
} }
@@ -161,14 +153,6 @@ async fn validate_group_permissions() {
unreachable!() unreachable!()
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::Group ChannelType::Group
} }
@@ -270,14 +254,6 @@ async fn validate_server_permissions() {
false false
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel ChannelType::ServerChannel
} }
@@ -370,14 +346,6 @@ async fn validate_timed_out_member() {
true true
} }
async fn do_we_have_publish_overwrites(&mut self) -> bool {
true
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
true
}
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel ChannelType::ServerChannel
} }
-6
View File
@@ -39,12 +39,6 @@ pub trait PermissionQuery {
/// Is our perspective user timed out on this server? /// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool; async fn are_we_timed_out(&mut self) -> bool;
/// Is the member muted?
async fn do_we_have_publish_overwrites(&mut self) -> bool;
/// Is the member deafend?
async fn do_we_have_receive_overwrites(&mut self) -> bool;
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -16,7 +16,7 @@ redis-is-patched = []
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
# Config for loading Redis URI # Config for loading Redis URI
revolt-config = { version = "0.8.9", path = "../config" } revolt-config = { version = "0.8.8", path = "../config" }
[dependencies] [dependencies]
# Utility # Utility
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-ratelimits" name = "revolt-ratelimits"
version = "0.8.9" version = "0.8.8"
edition = "2024" edition = "2024"
[features] [features]
@@ -10,9 +10,9 @@ axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"] default = ["rocket", "axum"]
[dependencies] [dependencies]
revolt-database = { version = "0.8.9", path = "../database"} revolt-database = { version = "0.8.8", path = "../database"}
revolt-result = { version = "0.8.9", path = "../result" } revolt-result = { version = "0.8.8", path = "../result" }
revolt-config = { version = "0.8.9", path = "../config" } revolt-config = { version = "0.8.8", 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 } revolt_rocket_okapi = { version = "0.10.0", optional = true }
+1 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -32,7 +32,5 @@ rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
revolt_rocket_okapi = { version = "0.10.0", optional = true } revolt_rocket_okapi = { version = "0.10.0", optional = true }
revolt_okapi = { version = "0.9.1", optional = true } revolt_okapi = { version = "0.9.1", optional = true }
# utilities
log = "0.4"
# Axum # Axum
axum = { version = "0.7.5", optional = true } axum = { version = "0.7.5", optional = true }
-5
View File
@@ -75,11 +75,6 @@ impl IntoResponse for Error {
ErrorType::NotFound => StatusCode::NOT_FOUND, ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK, ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::LiveKitUnavailable => StatusCode::BAD_REQUEST,
ErrorType::NotConnected => StatusCode::BAD_REQUEST,
ErrorType::NotAVoiceChannel => StatusCode::BAD_REQUEST,
ErrorType::AlreadyConnected => StatusCode::BAD_REQUEST,
ErrorType::UnknownNode => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST, ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST, ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
-58
View File
@@ -1,4 +1,3 @@
use std::panic::Location;
use std::fmt::Display; use std::fmt::Display;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@@ -159,12 +158,6 @@ pub enum ErrorType {
error: String, error: String,
}, },
// ? Voice errors
LiveKitUnavailable,
NotAVoiceChannel,
AlreadyConnected,
NotConnected,
UnknownNode,
// ? Micro-service errors // ? Micro-service errors
ProxyError, ProxyError,
FileTooSmall, FileTooSmall,
@@ -204,57 +197,6 @@ macro_rules! create_database_error {
}; };
} }
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
Ok($self.$type($collection, $($rest),+).await.unwrap())
};
}
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! query {
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
$self.$type($collection, $($rest),+).await
.map_err(|_| create_database_error!(stringify!($type), $collection))
};
}
pub trait ToRevoltError<T> {
#[track_caller]
fn to_internal_error(self) -> Result<T, Error>;
}
impl<T, E: std::fmt::Debug> ToRevoltError<T> for Result<T, E> {
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.map_err(|_| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
impl<T> ToRevoltError<T> for Option<T> {
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self.ok_or_else(|| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ErrorType; use crate::ErrorType;
+1 -6
View File
@@ -78,14 +78,10 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::InvalidSession => Status::Unauthorized, ErrorType::InvalidSession => Status::Unauthorized,
ErrorType::NotAuthenticated => Status::Unauthorized, ErrorType::NotAuthenticated => Status::Unauthorized,
ErrorType::DuplicateNonce => Status::Conflict, ErrorType::DuplicateNonce => Status::Conflict,
ErrorType::VosoUnavailable => Status::BadRequest,
ErrorType::NotFound => Status::NotFound, ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok, ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::LiveKitUnavailable => Status::BadRequest,
ErrorType::NotAVoiceChannel => Status::BadRequest,
ErrorType::AlreadyConnected => Status::BadRequest,
ErrorType::NotConnected => Status::BadRequest,
ErrorType::UnknownNode => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest, ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest, ErrorType::ProxyError => Status::BadRequest,
@@ -94,7 +90,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::FileTypeNotAllowed => Status::BadRequest, ErrorType::FileTypeNotAllowed => Status::BadRequest,
ErrorType::ImageProcessingFailed => Status::InternalServerError, ErrorType::ImageProcessingFailed => Status::InternalServerError,
ErrorType::NoEmbedData => Status::BadRequest, ErrorType::NoEmbedData => Status::BadRequest,
ErrorType::VosoUnavailable => Status::BadRequest,
}; };
// Serialize the error data structure into JSON. // Serialize the error data structure into JSON.
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-crond" name = "revolt-crond"
version = "0.8.9" version = "0.8.8"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021" edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" } tokio = { version = "1" }
# Core # Core
revolt-database = { version = "0.8.9", path = "../../core/database" } revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-result = { version = "0.8.9", path = "../../core/result" } revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.9", path = "../../core/config" } revolt-config = { version = "0.8.8", path = "../../core/config" }
revolt-files = { version = "0.8.9", path = "../../core/files" } revolt-files = { version = "0.8.8", path = "../../core/files" }
+6 -6
View File
@@ -1,20 +1,20 @@
[package] [package]
name = "revolt-pushd" name = "revolt-pushd"
version = "0.8.9" version = "0.8.8"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
revolt-result = { version = "0.8.9", path = "../../core/result" } revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.9", path = "../../core/config", features = [ revolt-config = { version = "0.8.8", path = "../../core/config", features = [
"report-macros", "report-macros",
"anyhow" "anyhow"
] } ] }
revolt-database = { version = "0.8.9", path = "../../core/database" } revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-models = { version = "0.8.9", path = "../../core/models", features = [ revolt-models = { version = "0.8.8", path = "../../core/models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.9", path = "../../core/presence", features = [ revolt-presence = { version = "0.8.8", path = "../../core/presence", features = [
"redis-is-patched", "redis-is-patched",
] } ] }
@@ -1,168 +0,0 @@
use std::collections::HashMap;
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use log::debug;
use revolt_database::{events::rabbit::*, Database};
pub struct DmCallConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
}
impl Channeled for DmCallConsumer {
fn get_connection(&self) -> Option<&Connection> {
if self.conn.is_none() {
None
} else {
Some(self.conn.as_ref().unwrap())
}
}
fn get_channel(&self) -> Option<&Channel> {
if self.channel.is_none() {
None
} else {
Some(self.channel.as_ref().unwrap())
}
}
fn set_connection(&mut self, conn: Connection) {
self.conn = Some(conn);
}
fn set_channel(&mut self, channel: Channel) {
self.channel = Some(channel)
}
}
impl DmCallConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> DmCallConsumer {
DmCallConsumer {
db,
authifier_db,
conn: None,
channel: None,
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let _p: InternalDmCallPayload = serde_json::from_str(content.as_str())?;
let payload = _p.payload;
debug!("Received dm call start/stop event");
let (revolt_database::Channel::DirectMessage { recipients, .. }
| revolt_database::Channel::Group { recipients, .. }) =
self.db.fetch_channel(&payload.channel_id).await?
else {
warn!(
"Discarding dm call start/stop event for non-dm/group channel {}",
payload.channel_id
);
return Ok(());
};
let call_recipients = if let Some(user_recipients) = _p.recipients {
user_recipients
.into_iter()
.filter(|user_id| recipients.contains(user_id) && user_id != &payload.initiator_id)
.collect()
} else {
recipients
.into_iter()
.filter(|user_id| user_id != &payload.initiator_id)
.collect::<Vec<_>>()
};
let config = revolt_config::config().await;
for user_id in call_recipients {
if let Ok(sessions) = self.authifier_db.find_sessions(&user_id).await {
for session in sessions {
if let Some(sub) = session.subscription {
let mut sendable = PayloadToService {
notification: PayloadKind::DmCallStartEnd(payload.clone()),
token: sub.auth,
user_id: session.user_id,
session_id: session.id,
extras: HashMap::new(),
};
let args: BasicPublishArguments;
if sub.endpoint == "apn" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
} else if sub.endpoint == "fcm" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.fcm.queue.as_str(),
)
.finish();
} else {
// web push (vapid)
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.vapid.queue.as_str(),
)
.finish();
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
}
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for DmCallConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
warn!("Failed to process dm call start/stop event: {err:?}");
}
}
}
@@ -1,5 +1,4 @@
pub mod ack; pub mod ack;
pub mod dm_call;
pub mod fr_accepted; pub mod fr_accepted;
pub mod fr_received; pub mod fr_received;
pub mod generic; pub mod generic;
@@ -47,32 +47,6 @@ impl<'a> PayloadLike for MessagePayload<'a> {
} }
} }
#[derive(Serialize, Debug)]
struct CallStartStopPayload<'a> {
aps: APS<'a>,
#[serde(skip_serializing)]
options: NotificationOptions<'a>,
#[serde(skip_serializing)]
device_token: &'a str,
initiator_id: &'a str,
#[serde(rename = "camelCase")]
channel_id: &'a str,
#[serde(rename = "camelCase")]
started_at: &'a str,
#[serde(rename = "camelCase")]
ended: bool,
}
impl<'a> PayloadLike for CallStartStopPayload<'a> {
fn get_device_token(&self) -> &'a str {
self.device_token
}
fn get_options(&self) -> &NotificationOptions {
&self.options
}
}
// region: consumer // region: consumer
pub struct ApnsOutboundConsumer { pub struct ApnsOutboundConsumer {
@@ -89,11 +63,10 @@ impl ApnsOutboundConsumer {
// in a dm it should just be "Sendername". // in a dm it should just be "Sendername".
// not sure how feasible all those are given the PushNotification object as it currently stands. // not sure how feasible all those are given the PushNotification object as it currently stands.
#[allow(deprecated)]
match &notification.channel { match &notification.channel {
Channel::DirectMessage { .. } => notification.author.clone(), Channel::DirectMessage { .. } => notification.author.clone(),
Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), Channel::Group { name, .. } => format!("{}, #{}", notification.author, name),
Channel::TextChannel { name, .. } => { Channel::TextChannel { name, .. } | Channel::VoiceChannel { name, .. } => {
format!("{} in #{}", notification.author, name) format!("{} in #{}", notification.author, name)
} }
_ => "Unknown".to_string(), _ => "Unknown".to_string(),
@@ -336,7 +309,6 @@ impl ApnsOutboundConsumer {
); );
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::BadgeUpdate(badge) => { PayloadKind::BadgeUpdate(badge) => {
let apn_payload = Payload { let apn_payload = Payload {
aps: APS { aps: APS {
@@ -351,35 +323,6 @@ impl ApnsOutboundConsumer {
debug!("Sending badge update for user: {:}", &payload.user_id); debug!("Sending badge update for user: {:}", &payload.user_id);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::DmCallStartEnd(alert) => {
let started_at = alert.started_at.map_or(String::new(), |f| f.clone());
let apn_payload = CallStartStopPayload {
aps: APS {
alert: None,
badge: self.get_badge_count(&payload.user_id).await,
sound: None,
thread_id: None,
content_available: None,
category: None,
mutable_content: Some(1),
url_args: None,
},
device_token: &payload.token,
options: payload_options.clone(),
initiator_id: &alert.initiator_id,
channel_id: &alert.channel_id,
started_at: &started_at,
ended: alert.ended,
};
debug!(
"Sending call start/stop notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
}
} }
if let Err(err) = resp { if let Err(err) = resp {
@@ -5,12 +5,11 @@ use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProp
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
use fcm_v1::{ use fcm_v1::{
android::{AndroidConfig, AndroidMessagePriority}, android::AndroidConfig,
auth::{Authenticator, ServiceAccountKey}, auth::{Authenticator, ServiceAccountKey},
message::{Message, Notification}, message::{Message, Notification},
Client, Error as FcmError, Client, Error as FcmError,
}; };
use revolt_config::config;
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
use revolt_models::v0::{Channel, PushNotification}; use revolt_models::v0::{Channel, PushNotification};
use serde_json::Value; use serde_json::Value;
@@ -28,11 +27,10 @@ impl FcmOutboundConsumer {
// in a dm it should just be "Sendername". // in a dm it should just be "Sendername".
// not sure how feasible all those are given the PushNotification object as it currently stands. // not sure how feasible all those are given the PushNotification object as it currently stands.
#[allow(deprecated)]
match &notification.channel { match &notification.channel {
Channel::DirectMessage { .. } => notification.author.clone(), Channel::DirectMessage { .. } => notification.author.clone(),
Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), Channel::Group { name, .. } => format!("{}, #{}", notification.author, name),
Channel::TextChannel { name, .. } => { Channel::TextChannel { name, .. } | Channel::VoiceChannel { name, .. } => {
format!("{} in #{}", notification.author, name) format!("{} in #{}", notification.author, name)
} }
_ => "Unknown".to_string(), _ => "Unknown".to_string(),
@@ -171,37 +169,6 @@ impl FcmOutboundConsumer {
resp = self.client.send(&msg).await; resp = self.client.send(&msg).await;
} }
PayloadKind::DmCallStartEnd(alert) => {
let mut data: HashMap<String, Value> = HashMap::new();
data.insert(
"initiator_id".to_string(),
Value::String(alert.initiator_id),
);
data.insert("channel_id".to_string(), Value::String(alert.channel_id));
data.insert(
"started_at".to_string(),
Value::String(alert.started_at.unwrap_or_else(|| "".to_string())),
);
data.insert("ended".to_string(), Value::Bool(alert.ended));
let msg = Message {
token: Some(payload.token),
notification: None,
data: Some(data),
android: Some(AndroidConfig {
priority: Some(AndroidMessagePriority::High),
ttl: Some(format!(
"{}s",
config().await.api.livekit.call_ring_duration
)),
..Default::default()
}),
..Default::default()
};
resp = self.client.send(&msg).await;
}
PayloadKind::BadgeUpdate(_) => { PayloadKind::BadgeUpdate(_) => {
bail!("FCM cannot handle badge updates and they should not be sent here."); bail!("FCM cannot handle badge updates and they should not be sent here.");
} }
@@ -8,7 +8,7 @@ use base64::{
engine::{self}, engine::{self},
Engine as _, Engine as _,
}; };
use revolt_database::{events::rabbit::*, util::format_display_name, Database}; use revolt_database::{events::rabbit::*, Database};
use web_push::{ use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder, WebPushClient, WebPushError, WebPushMessageBuilder,
@@ -107,33 +107,6 @@ impl VapidOutboundConsumer {
PayloadKind::MessageNotification(alert) => { PayloadKind::MessageNotification(alert) => {
payload_body = serde_json::to_string(&alert)?; payload_body = serde_json::to_string(&alert)?;
} }
PayloadKind::DmCallStartEnd(alert) => {
let initiator_name = if let Some(server_id) =
self.db.fetch_channel(&alert.channel_id).await?.server()
{
format_display_name(&self.db, &alert.initiator_id, Some(server_id)).await
} else {
format_display_name(&self.db, &alert.initiator_id, None).await
}?;
let channel = self.db.fetch_channel(&alert.channel_id).await?;
let mut body = HashMap::new();
match channel {
revolt_database::Channel::DirectMessage { .. } => {
body.insert("body", format!("{} is calling you", initiator_name));
}
revolt_database::Channel::Group { name, .. } => {
body.insert(
"body",
format!("{} is calling your group, {}", initiator_name, name),
);
}
_ => bail!("Invalid DmCallStart/End channel type"),
}
payload_body = serde_json::to_string(&body)?;
}
PayloadKind::BadgeUpdate(_) => { PayloadKind::BadgeUpdate(_) => {
bail!("Vapid cannot handle badge updates and they should not be sent here."); bail!("Vapid cannot handle badge updates and they should not be sent here.");
} }
+2 -16
View File
@@ -16,9 +16,8 @@ use tokio::sync::Notify;
mod consumers; mod consumers;
use consumers::{ use consumers::{
inbound::{ inbound::{
ack::AckConsumer, dm_call::DmCallConsumer, fr_accepted::FRAcceptedConsumer, ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer,
fr_received::FRReceivedConsumer, generic::GenericConsumer, generic::GenericConsumer, mass_mention::MassMessageConsumer, message::MessageConsumer,
mass_mention::MassMessageConsumer, message::MessageConsumer,
}, },
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer}, outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
}; };
@@ -103,7 +102,6 @@ async fn main() {
.await, .await,
); );
// inbound: Mass Mentions
connections.push( connections.push(
make_queue_and_consume( make_queue_and_consume(
&config, &config,
@@ -115,18 +113,6 @@ async fn main() {
.await, .await,
); );
// inbound: Dm Calls
connections.push(
make_queue_and_consume(
&config,
&config.pushd.dm_call_queue,
config.pushd.get_dm_call_routing_key().as_str(),
None,
DmCallConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
if !config.pushd.apn.pkcs8.is_empty() { if !config.pushd.apn.pkcs8.is_empty() {
connections.push( connections.push(
make_queue_and_consume( make_queue_and_consume(
-1
View File
@@ -1 +0,0 @@
/target
-48
View File
@@ -1,48 +0,0 @@
[package]
name = "revolt-voice-ingress"
version = "0.7.1"
license = "AGPL-3.0-or-later"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# util
log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
redis-kiss = "0.1.4"
chrono = "0.4.15"
# Serde
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = "1.0.136"
# Http
rocket = { version = "0.5.0-rc.2", features = ["json"] }
rocket_empty = "0.1.1"
# Async
futures = "0.3.21"
async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
# Core
revolt-result = { path = "../../core/result" }
revolt-models = { path = "../../core/models" }
revolt-config = { path = "../../core/config" }
revolt-database = { path = "../../core/database" }
revolt-permissions = { path = "../../core/permissions" }
# Voice
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] }
# RabbitMQ
amqprs = { version = "1.7.0" }
-11
View File
@@ -1,11 +0,0 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /home/rust/src/target/release/revolt-voice-ingress ./
COPY --from=debian /usr/bin/uname /usr/bin/uname
USER nonroot
CMD ["./revolt-voice-ingress"]
-256
View File
@@ -1,256 +0,0 @@
use chrono::DateTime;
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType;
use revolt_database::{
events::client::EventV1,
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
create_voice_state, delete_voice_state, get_call_notification_recipients,
get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members,
set_channel_call_started_system_message, take_channel_call_started_system_message,
update_voice_state_tracks, VoiceClient,
},
Database, PartialMessage, SystemMessage, AMQP,
};
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError};
use rocket::{post, State};
use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader;
#[post("/<node>", data = "<body>")]
pub async fn ingress(
db: &State<Database>,
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>,
node: &str,
auth_header: AuthHeader<'_>,
body: &str,
) -> Result<EmptyResponse> {
log::debug!("received event: {body:?}");
let config = revolt_config::config().await;
let node_info = config
.api
.livekit
.nodes
.get(node)
.to_internal_error()
.inspect_err(|_| {
log::error!("Unknown node {node}, make sure livekit has the correct node name set and matches `hosts.livekit` and `api.livekit.nodes` in the Revolt config.")
})?;
let webhook_receiver = WebhookReceiver::new(TokenVerifier::with_api_key(
&node_info.key,
&node_info.secret,
));
let event = webhook_receiver
.receive(body, &auth_header)
.to_internal_error()?;
let channel_id = event.room.as_ref().map(|r| &r.name);
let user_id = event.participant.as_ref().map(|r| &r.identity);
match event.event.as_str() {
// User joined a channel
"participant_joined" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
let joined_at = Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap();
let voice_state =
create_voice_state(channel_id, channel.server(), user_id, joined_at).await?;
// Only publish one event when a user is moved from one channel to another.
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
EventV1::VoiceChannelMove {
user: user_id.to_string(),
from: moved_from,
to: channel_id.to_string(),
state: voice_state,
}
.p(channel_id.to_string())
.await;
} else {
EventV1::VoiceChannelJoin {
id: channel_id.to_string(),
state: voice_state,
}
.p(channel_id.to_string())
.await;
};
// First user who joined - send call started system message.
if event.room.as_ref().unwrap().num_participants == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?;
let message_id =
Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
.to_string();
let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(),
finished_at: None,
}
.into_message(channel.id().to_string());
call_started_message.id = message_id;
set_channel_call_started_system_message(channel.id(), &call_started_message.id)
.await?;
call_started_message
.send(
db,
Some(amqp),
v0::MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false,
)
.await?;
let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
let now = joined_at.format_short().to_string();
if let Err(e) = amqp
.dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
.await
{
revolt_config::capture_error(&e);
}
}
}
// User left a channel
"participant_left" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
// Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id)
.await?
.is_none()
{
EventV1::VoiceChannelLeave {
id: channel_id.clone(),
user: user_id.clone(),
}
.p(channel_id.clone())
.await;
};
// Update CallStarted system message if everyone has left with the end time
let members = get_voice_channel_members(channel_id).await?;
if members.is_none_or(|m| m.is_empty()) {
// The channel is empty so send out an "end" message for ringing
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) =
take_channel_call_started_system_message(channel_id).await?
{
// Could have been deleted
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
.as_message(db)
.await
{
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*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
"track_published" | "track_unpublished" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let track = event.track.as_ref().to_internal_error()?;
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
let user = Reference::from_unchecked(user_id).as_user(db).await?;
let user_limits = user.limits().await;
// forbid any size which goes over the limit and also limit the aspect ratio to stop people from making too tall or too wide and bypassing the limit.
// TODO: figure out how to track audio stream quality
if event.event == "track_published"
&& (track.r#type == TrackType::Data as i32
|| (track.r#type == TrackType::Video as i32
&& (user_limits.video_resolution[0] != 0
&& user_limits.video_resolution[1] != 0
&& track.width * track.height
> user_limits.video_resolution[0]
* user_limits.video_resolution[1])
|| (user_limits.video_aspect_ratio[0]
!= user_limits.video_aspect_ratio[1]
&& !(user_limits.video_aspect_ratio[0]
..=user_limits.video_aspect_ratio[1])
.contains(&(track.width as f32 / track.height as f32)))))
{
voice_client.remove_user(node, user_id, channel_id).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
} else {
let partial = update_voice_state_tracks(
channel_id,
channel.server(),
user_id,
event.event == "track_published", // to avoid duplicating this entire case twice
track.source,
)
.await?;
EventV1::UserVoiceStateUpdate {
id: user_id.clone(),
channel_id: channel_id.clone(),
data: partial,
}
.p(channel_id.clone())
.await;
}
}
_ => {}
};
Ok(EmptyResponse)
}
-28
View File
@@ -1,28 +0,0 @@
use revolt_result::{create_error, Error};
use rocket::{
http::Status,
request::{FromRequest, Outcome},
Request,
};
pub struct AuthHeader<'a>(&'a str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthHeader<'r> {
type Error = Error;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get("Authorization").next() {
Some(token) => Outcome::Success(Self(token)),
None => Outcome::Error((Status::Unauthorized, create_error!(NotAuthenticated))),
}
}
}
impl std::ops::Deref for AuthHeader<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
-37
View File
@@ -1,37 +0,0 @@
use std::env;
use revolt_database::DatabaseInfo;
use revolt_database::{voice::VoiceClient, AMQP};
use revolt_result::Result;
use rocket::{build, routes, Config};
use std::net::Ipv4Addr;
mod api;
mod guard;
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
revolt_config::configure!(voice_ingress);
let amqp = AMQP::new_auto().await;
let database = DatabaseInfo::Auto.connect().await.unwrap();
let voice_client = VoiceClient::from_revolt_config().await;
let _rocket = build()
.manage(database)
.manage(voice_client)
.manage(amqp)
.mount("/", routes![api::ingress])
.configure(Config {
port: 8500,
address: Ipv4Addr::new(0, 0, 0, 0).into(),
..Default::default()
})
.ignite()
.await?
.launch()
.await?;
Ok(())
}
+1 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.8.9" version = "0.8.8"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -83,9 +83,5 @@ revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] } revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
# voice
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
[build-dependencies] [build-dependencies]
vergen = "7.5.0" vergen = "7.5.0"
-13
View File
@@ -25,7 +25,6 @@ use amqprs::{
use async_std::channel::unbounded; use async_std::channel::unbounded;
use authifier::AuthifierEvent; use authifier::AuthifierEvent;
use rocket::data::ToByteUnit; use rocket::data::ToByteUnit;
use revolt_database::voice::VoiceClient;
pub async fn web() -> Rocket<Build> { pub async fn web() -> Rocket<Build> {
// Get settings // Get settings
@@ -36,7 +35,6 @@ pub async fn web() -> Rocket<Build> {
// Setup database // Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
log::info!("database_here {db:?}");
db.migrate_database().await.unwrap(); db.migrate_database().await.unwrap();
// Setup Authifier event channel // Setup Authifier event channel
@@ -92,16 +90,6 @@ pub async fn web() -> Rocket<Build> {
) )
.into(); .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
let voice_client = VoiceClient::new(config.api.livekit.nodes.clone());
// Configure Rabbit // Configure Rabbit
let connection = Connection::open(&OpenConnectionArguments::new( let connection = Connection::open(&OpenConnectionArguments::new(
&config.rabbit.host, &config.rabbit.host,
@@ -149,7 +137,6 @@ pub async fn web() -> Rocket<Build> {
.manage(db) .manage(db)
.manage(amqp) .manage(amqp)
.manage(cors.clone()) .manage(cors.clone())
.manage(voice_client)
.manage(ratelimits) .manage(ratelimits)
.attach(ratelimiter::RatelimitFairing) .attach(ratelimiter::RatelimitFairing)
.attach(cors) .attach(cors)
+2 -14
View File
@@ -1,4 +1,4 @@
use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, get_user_voice_channels, VoiceClient}, Database, User}; use revolt_database::{util::reference::Reference, Database, User};
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;
@@ -10,7 +10,6 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>")] #[delete("/<target>")]
pub async fn delete_bot( pub async fn delete_bot(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
@@ -19,18 +18,7 @@ pub async fn delete_bot(
return Err(create_error!(NotFound)); return Err(create_error!(NotFound));
} }
bot.delete(db).await?; bot.delete(db).await.map(|_| EmptyResponse)
for channel_id in get_user_voice_channels(&bot.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
let channel = Reference::from_unchecked(&channel_id).as_channel(db).await?;
voice_client.remove_user(&node, &bot.id, &channel_id).await?;
delete_voice_state(&channel_id, channel.server(), &bot.id).await?;
}
Ok(EmptyResponse)
} }
#[cfg(test)] #[cfg(test)]
@@ -1,8 +1,5 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{
delete_channel_voice_state, delete_voice_state, get_channel_node, get_voice_channel_members, is_in_voice_channel, VoiceClient
},
Channel, Database, PartialChannel, User, AMQP, Channel, Database, PartialChannel, User, AMQP,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -18,7 +15,6 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>?<options..>")] #[delete("/<target>?<options..>")]
pub async fn delete( pub async fn delete(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>, amqp: &State<AMQP>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
@@ -30,11 +26,9 @@ pub async fn delete(
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
#[allow(deprecated)]
match &channel { match &channel {
Channel::SavedMessages { .. } => Err(create_error!(NoEffect))?, Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
Channel::DirectMessage { .. } => { Channel::DirectMessage { .. } => channel
channel
.update( .update(
db, db,
PartialChannel { PartialChannel {
@@ -43,10 +37,9 @@ pub async fn delete(
}, },
vec![], vec![],
) )
.await? .await
} .map(|_| EmptyResponse),
Channel::Group { .. } => { Channel::Group { .. } => channel
channel
.remove_user_from_group( .remove_user_from_group(
db, db,
amqp, amqp,
@@ -54,33 +47,13 @@ pub async fn delete(
None, None,
options.leave_silently.unwrap_or_default(), options.leave_silently.unwrap_or_default(),
) )
.await?; .await
.map(|_| EmptyResponse),
if is_in_voice_channel(&user.id, channel.id()).await? { Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client
.remove_user(&node, &user.id, channel.id())
.await?;
delete_voice_state(channel.id(), None, &user.id).await?;
};
}
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.map(|_| EmptyResponse)
}
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(), channel.server(), &users).await?;
};
} }
};
Ok(EmptyResponse)
} }
#[cfg(test)] #[cfg(test)]
@@ -1,5 +1,6 @@
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},
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};
@@ -14,7 +15,6 @@ use validator::Validate;
#[patch("/<target>", data = "<data>")] #[patch("/<target>", data = "<data>")]
pub async fn edit( pub async fn edit(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>, amqp: &State<AMQP>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
@@ -38,7 +38,6 @@ pub async fn edit(
&& data.icon.is_none() && data.icon.is_none()
&& data.nsfw.is_none() && data.nsfw.is_none()
&& data.owner.is_none() && data.owner.is_none()
&& data.voice.is_none()
&& data.remove.is_empty() && data.remove.is_empty()
{ {
return Ok(Json(channel.into())); return Ok(Json(channel.into()));
@@ -96,6 +95,22 @@ pub async fn edit(
icon, icon,
nsfw, nsfw,
.. ..
}
| Channel::TextChannel {
id,
name,
description,
icon,
nsfw,
..
}
| Channel::VoiceChannel {
id,
name,
description,
icon,
nsfw,
..
} => { } => {
if data.remove.contains(&v0::FieldsChannel::Icon) { if data.remove.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon { if let Some(icon) = &icon {
@@ -136,6 +151,7 @@ pub async fn edit(
} }
// Send out mutation system messages. // Send out mutation system messages.
if let Channel::Group { .. } = &channel {
if let Some(name) = &partial.name { if let Some(name) = &partial.name {
SystemMessage::ChannelRenamed { SystemMessage::ChannelRenamed {
name: name.to_string(), name: name.to_string(),
@@ -191,84 +207,17 @@ pub async fn edit(
.ok(); .ok();
} }
} }
Channel::TextChannel {
id,
name,
description,
icon,
nsfw,
voice,
..
} => {
if data.remove.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?;
}
}
for field in &data.remove {
match field {
v0::FieldsChannel::Description => {
description.take();
}
v0::FieldsChannel::Icon => {
icon.take();
}
v0::FieldsChannel::Voice => {
voice.take();
}
_ => {}
}
}
if let Some(icon_id) = data.icon {
partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?);
*icon = partial.icon.clone();
}
if let Some(new_name) = data.name {
*name = new_name.clone();
partial.name = Some(new_name);
}
if let Some(new_description) = data.description {
partial.description = Some(new_description);
*description = partial.description.clone();
}
if let Some(new_nsfw) = data.nsfw {
*nsfw = new_nsfw;
partial.nsfw = Some(new_nsfw);
}
if let Some(new_voice) = data.voice {
*voice = Some(new_voice.clone().into());
partial.voice = Some(new_voice.into());
}
}
_ => return Err(create_error!(InvalidOperation)),
};
channel channel
.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 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(), channel.server(), &users).await?;
};
} }
_ => return Err(create_error!(InvalidOperation)),
};
Ok(Json(channel.into())) Ok(Json(channel.into()))
} }
@@ -1,4 +1,4 @@
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, Channel, Database, User, AMQP};
use revolt_permissions::ChannelPermission; use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -12,7 +12,6 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>/recipients/<member>")] #[delete("/<target>/recipients/<member>")]
pub async fn remove_member( pub async fn remove_member(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
amqp: &State<AMQP>, amqp: &State<AMQP>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
@@ -24,7 +23,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 { match &channel {
Channel::Group {
owner, recipients, ..
} => {
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()
@@ -36,25 +38,17 @@ pub async fn remove_member(
return Err(create_error!(CannotRemoveYourself)); return Err(create_error!(CannotRemoveYourself));
} }
if !recipients.contains(&member.id) { if !recipients.iter().any(|x| *x == member.id) {
return Err(create_error!(NotInGroup)); return Err(create_error!(NotInGroup));
} }
channel channel
.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 { .map(|_| EmptyResponse)
return Err(create_error!(InvalidOperation)) }
}; _ => Err(create_error!(InvalidOperation)),
}
if is_in_voice_channel(&user.id, channel.id()).await? {
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.remove_user(&node, &user.id, channel.id()).await?;
delete_voice_state(channel.id(), None, &user.id).await?;
};
Ok(EmptyResponse)
} }
#[cfg(test)] #[cfg(test)]
@@ -1,6 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User, Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
}; };
use revolt_models::v0::{self, MessageSort}; use revolt_models::v0::{self, MessageSort};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -65,7 +65,12 @@ pub async fn query(
}, },
&user, &user,
include_users, include_users,
channel.server(), match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
) )
.await .await
.map(Json) .map(Json)
@@ -1,6 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User, Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -31,7 +31,7 @@ pub async fn search(
})?; })?;
if options.query.is_some() && options.pinned.is_some() { if options.query.is_some() && options.pinned.is_some() {
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation))
} }
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
@@ -69,7 +69,12 @@ pub async fn search(
}, },
&user, &user,
include_users, include_users,
channel.server(), match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
) )
.await .await
.map(Json) .map(Json)
@@ -151,7 +151,6 @@ mod test {
name: "Hidden Channel".to_string(), name: "Hidden Channel".to_string(),
description: None, description: None,
nsfw: Some(false), nsfw: Some(false),
voice: None
}, },
true, true,
) )
@@ -194,7 +193,6 @@ mod test {
d: ChannelPermission::ViewChannel as i64, d: ChannelPermission::ViewChannel as i64,
}), }),
last_message_id: None, last_message_id: None,
voice: None,
}; };
locked_channel locked_channel
.update(&harness.db, partial, vec![]) .update(&harness.db, partial, vec![])
@@ -289,8 +287,6 @@ mod test {
avatar: None, avatar: None,
timeout: None, timeout: None,
roles: Some(second_member_roles), roles: Some(second_member_roles),
can_publish: None,
can_receive: None
}; };
second_member second_member
.update(&harness.db, partial, vec![]) .update(&harness.db, partial, vec![])
@@ -617,8 +613,6 @@ mod test {
nickname: None, nickname: None,
roles: Some(vec![role_id.clone()]), roles: Some(vec![role_id.clone()]),
timeout: None, timeout: None,
can_publish: None,
can_receive: None
}, },
vec![], vec![],
) )
-2
View File
@@ -25,7 +25,6 @@ mod message_unreact;
mod permissions_set; mod permissions_set;
mod permissions_set_default; mod permissions_set_default;
mod voice_join; mod voice_join;
mod voice_stop_ring;
mod webhook_create; mod webhook_create;
mod webhook_fetch_all; mod webhook_fetch_all;
@@ -50,7 +49,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
group_add_member::add_member, group_add_member::add_member,
group_remove_member::remove_member, group_remove_member::remove_member,
voice_join::call, voice_join::call,
voice_stop_ring::stop_ring,
permissions_set::set_role_permissions, permissions_set::set_role_permissions,
permissions_set_default::set_default_channel_permissions, permissions_set_default::set_default_channel_permissions,
message_react::react_message, message_react::react_message,
@@ -1,5 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
@@ -10,20 +11,19 @@ use rocket::{serde::json::Json, State};
/// ///
/// Sets permissions for the specified role in this channel. /// Sets permissions for the specified role in this channel.
/// ///
/// Channel must be a `TextChannel`. /// Channel must be a `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")] #[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)] #[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn set_role_permissions( pub async fn set_role_permissions(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
role_id: String, role_id: String,
data: Json<v0::DataSetRolePermissions>, data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Channel>> { ) -> Result<Json<v0::Channel>> {
let channel = target.as_channel(db).await?; let mut channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions: revolt_permissions::PermissionValue = calculate_channel_permissions(&mut query).await; let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
@@ -38,15 +38,11 @@ pub async fn set_role_permissions(
.throw_permission_override(current_value, &data.permissions) .throw_permission_override(current_value, &data.permissions)
.await?; .await?;
let mut new_channel = channel.clone(); channel
new_channel
.set_role_permission(db, &role_id, data.permissions.clone().into()) .set_role_permission(db, &role_id, data.permissions.clone().into())
.await?; .await?;
sync_voice_permissions(db, voice_client, &new_channel, Some(server), Some(&role_id)).await?; Ok(Json(channel.into()))
Ok(Json(new_channel.into()))
} else { } else {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
@@ -1,5 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Channel, Database, PartialChannel, User util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
}; };
use revolt_models::v0::{self, DataDefaultChannelPermissions}; use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -10,12 +11,11 @@ use rocket::{serde::json::Json, State};
/// ///
/// Sets permissions for the default role in this channel. /// Sets permissions for the default role in this channel.
/// ///
/// Channel must be a `Group` or `TextChannel`. /// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")] #[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/default", data = "<data>", rank = 1)] #[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn set_default_channel_permissions( pub async fn set_default_channel_permissions(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Json<v0::DataDefaultChannelPermissions>, data: Json<v0::DataDefaultChannelPermissions>,
@@ -48,6 +48,10 @@ pub async fn set_default_channel_permissions(
Channel::TextChannel { Channel::TextChannel {
default_permissions, default_permissions,
.. ..
}
| Channel::VoiceChannel {
default_permissions,
..
} => { } => {
if let DataDefaultChannelPermissions::Field { permissions: field } = data { if let DataDefaultChannelPermissions::Field { permissions: field } = data {
permissions permissions
@@ -71,12 +75,5 @@ pub async fn set_default_channel_permissions(
_ => return Err(create_error!(InvalidOperation)), _ => return Err(create_error!(InvalidOperation)),
} }
let server = match channel.server() {
Some(server_id) => Some(Reference::from_unchecked(server_id).as_server(db).await?),
None => None
};
sync_voice_permissions(db, voice_client, &channel, server.as_ref(), None).await?;
Ok(Json(channel.into())) Ok(Json(channel.into()))
} }
+82 -92
View File
@@ -1,115 +1,105 @@
use revolt_config::config; use revolt_config::config;
use revolt_database::{ use revolt_database::{
util::{permissions::perms, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{ Channel, Database, User,
delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members,
raise_if_in_voice, set_call_notification_recipients, VoiceClient,
},
Database, User,
}; };
use revolt_models::v0; use revolt_models::v0;
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 rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
/// # Join Call /// # Join Call
/// ///
/// Asks the voice server for a token to join the call. /// Asks the voice server for a token to join the call.
#[openapi(tag = "Voice")] #[openapi(tag = "Voice")]
#[post("/<target>/join_call", data = "<data>")] #[post("/<target>/join_call")]
pub async fn call( pub async fn call(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Json<v0::DataJoinCall>, ) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
) -> Result<Json<v0::CreateVoiceUserResponse>> {
if !voice_client.is_enabled() {
return Err(create_error!(LiveKitUnavailable));
}
let v0::DataJoinCall {
node,
force_disconnect,
recipients,
} = data.into_inner();
if user.bot.is_some() && force_disconnect == Some(true) {
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let Some(voice_info) = channel.voice() else { calculate_channel_permissions(&mut query)
return Err(create_error!(NotAVoiceChannel)); .await
}; .throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
let mut permissions = perms(db, &user).channel(&channel);
let current_permissions = calculate_channel_permissions(&mut permissions).await;
current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
if get_voice_channel_members(channel.id())
.await?
.zip(voice_info.max_users)
.is_some_and(|(ms, max_users)| ms.len() >= max_users)
&& !current_permissions.has(ChannelPermission::ManageChannel as u64)
{
return Err(create_error!(CannotJoinCall));
}
let existing_node = get_channel_node(channel.id()).await?;
let node = existing_node
.or(node)
.ok_or_else(|| create_error!(UnknownNode))?;
let config = config().await; let config = config().await;
if config.api.security.voso_legacy_token.is_empty() {
let node_host = config return Err(create_error!(VosoUnavailable));
.hosts
.livekit
.get(&node)
.ok_or_else(|| create_error!(UnknownNode))?
.clone();
if force_disconnect == Some(true) {
// Finds and disconnects any existing voice connections by the user,
// should only ever loop once but just to cover our backs.
for channel_id in get_user_voice_channels(&user.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
let channel = Reference::from_unchecked(&channel_id)
.as_channel(db)
.await?;
voice_client
.remove_user(&node, &user.id, &channel_id)
.await?;
delete_voice_state(&channel_id, channel.server(), &user.id).await?;
} }
match channel {
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
return Err(create_error!(CannotJoinCall))
}
_ => {}
}
// To join a call:
// - Check if the room exists.
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
config.api.security.voso_legacy_token.clone(),
)
.send()
.await;
match result {
Err(_) => return Err(create_error!(VosoUnavailable)),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if (client
.post(format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
config.api.security.voso_legacy_token.clone(),
)
.send()
.await)
.is_err()
{
return Err(create_error!(VosoUnavailable));
}
}
_ => return Err(create_error!(VosoUnavailable)),
},
}
// Then create a user for the room.
if let Ok(response) = client
.post(format!(
"{}/room/{}/user/{}",
config.hosts.voso_legacy,
channel.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
config.api.security.voso_legacy_token,
)
.send()
.await
{
response
.json()
.await
.map_err(|_| create_error!(InvalidOperation))
.map(Json)
} else { } else {
raise_if_in_voice(&user, channel.id()).await?; Err(create_error!(VosoUnavailable))
} }
let token = voice_client
.create_token(&node, db, &user, current_permissions, &channel)
.await?;
let room = voice_client.create_room(&node, &channel).await?;
log::debug!("Created room {}", room.name);
if let Some(recipients) = recipients {
if room.num_participants == 0 {
set_call_notification_recipients(channel.id(), &user.id, &recipients).await?;
}
}
Ok(Json(v0::CreateVoiceUserResponse {
token,
url: node_host.clone(),
}))
} }
@@ -1,69 +0,0 @@
use revolt_database::{
util::reference::Reference,
voice::{get_voice_state, VoiceClient},
Channel, Database, User, AMQP,
};
use revolt_result::{create_error, Result, ToRevoltError};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Stop Ring
/// Stops ringing a specific user in a dm call.
/// You must be in the call to use this endpoint, returns NotConnected otherwise.
/// Only valid in DM/Group channels, will return NoEffect in servers.
/// Returns NotFound if the user is not in the dm/group channel
#[openapi(tag = "Voice")]
#[put("/<target>/end_ring/<target_user>")]
pub async fn stop_ring(
db: &State<Database>,
amqp: &State<AMQP>,
voice: &State<VoiceClient>,
user: User,
target: Reference<'_>,
target_user: Reference<'_>,
) -> Result<EmptyResponse> {
if !voice.is_enabled() {
return Err(create_error!(LiveKitUnavailable));
}
let channel = target.as_channel(db).await?;
if channel.server().is_some() {
return Err(create_error!(NoEffect));
}
if get_voice_state(channel.id(), None, &user.id)
.await?
.is_none()
{
return Err(create_error!(NotConnected));
}
let members = match channel {
Channel::DirectMessage { ref recipients, .. } | Channel::Group { ref recipients, .. } => {
recipients
}
_ => return Err(create_error!(NoEffect)),
};
if members.iter().any(|m| &target_user.id == m) {
if let Err(e) = amqp
.dm_call_updated(
&user.id,
channel.id(),
None,
true,
Some(vec![target_user.id.to_string()]),
)
.await
.to_internal_error()
{
revolt_config::capture_internal_error!(&e);
return Err(e);
}
Ok(EmptyResponse)
} else {
Err(create_error!(NotFound))
}
}
@@ -23,6 +23,13 @@ pub async fn fetch(db: &State<Database>, target: Reference<'_>) -> Result<Json<v
name, name,
description, description,
.. ..
}
| Channel::VoiceChannel {
id,
server,
name,
description,
..
} => { } => {
let server = db.fetch_server(&server).await?; let server = db.fetch_server(&server).await?;
@@ -195,7 +202,6 @@ mod test {
name: "Voice Channel".to_string(), name: "Voice Channel".to_string(),
description: None, description: None,
nsfw: Some(false), nsfw: Some(false),
voice: None
}, },
true, true,
) )
+12 -35
View File
@@ -21,22 +21,15 @@ pub struct Feature {
pub url: String, pub url: String,
} }
/// # Information about a livekit node
#[derive(Serialize, JsonSchema, Debug)]
pub struct VoiceNode {
pub name: String,
pub lat: f64,
pub lon: f64,
pub public_url: String,
}
/// # Voice Server Configuration /// # Voice Server Configuration
#[derive(Serialize, JsonSchema, Debug)] #[derive(Serialize, JsonSchema, Debug)]
pub struct VoiceFeature { pub struct VoiceFeature {
/// Whether voice is enabled /// Whether voice is enabled
pub enabled: bool, pub enabled: bool,
/// All livekit nodes /// URL pointing to the voice API
pub nodes: Vec<VoiceNode>, pub url: String,
/// URL pointing to the voice WebSocket server
pub ws: String,
} }
/// # Feature Configuration /// # Feature Configuration
@@ -53,7 +46,7 @@ pub struct RevoltFeatures {
/// Proxy service configuration /// Proxy service configuration
pub january: Feature, pub january: Feature,
/// Voice server configuration /// Voice server configuration
pub livekit: VoiceFeature, pub voso: VoiceFeature,
} }
/// # Build Information /// # Build Information
@@ -101,38 +94,22 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
features: RevoltFeatures { features: RevoltFeatures {
captcha: CaptchaFeature { captcha: CaptchaFeature {
enabled: !config.api.security.captcha.hcaptcha_key.is_empty(), enabled: !config.api.security.captcha.hcaptcha_key.is_empty(),
key: config.api.security.captcha.hcaptcha_sitekey.clone(), key: config.api.security.captcha.hcaptcha_sitekey,
}, },
email: !config.api.smtp.host.is_empty(), email: !config.api.smtp.host.is_empty(),
invite_only: config.api.registration.invite_only, invite_only: config.api.registration.invite_only,
autumn: Feature { autumn: Feature {
enabled: !config.hosts.autumn.is_empty(), enabled: !config.hosts.autumn.is_empty(),
url: config.hosts.autumn.clone(), url: config.hosts.autumn,
}, },
january: Feature { january: Feature {
enabled: !config.hosts.january.is_empty(), enabled: !config.hosts.january.is_empty(),
url: config.hosts.january.clone(), url: config.hosts.january,
}, },
livekit: VoiceFeature { voso: VoiceFeature {
enabled: !config.hosts.livekit.is_empty(), enabled: !config.hosts.voso_legacy.is_empty(),
nodes: config url: config.hosts.voso_legacy,
.api ws: config.hosts.voso_legacy_ws,
.livekit
.nodes
.iter()
.filter(|(_, node)| !node.private)
.map(|(name, value)| VoiceNode {
name: name.clone(),
lat: value.lat,
lon: value.lon,
public_url: config
.hosts
.livekit
.get(name)
.expect("Missing corresponding host for voice node")
.clone(),
})
.collect(),
}, },
}, },
ws: config.hosts.events, ws: config.hosts.events,
@@ -1,6 +1,5 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient},
Database, RemovalIntention, ServerBan, User, Database, RemovalIntention, ServerBan, User,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -17,7 +16,6 @@ use validator::Validate;
#[put("/<server>/bans/<target>", data = "<data>")] #[put("/<server>/bans/<target>", data = "<data>")]
pub async fn ban( pub async fn ban(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
server: Reference<'_>, server: Reference<'_>,
target: Reference<'_>, target: Reference<'_>,
@@ -56,15 +54,6 @@ pub async fn ban(
member member
.remove(db, &server, RemovalIntention::Ban, false) .remove(db, &server, RemovalIntention::Ban, false)
.await?; .await?;
// If the member is in a voice channel while banned kick them from the voice channel
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
voice_client.remove_user(&node, &user.id, &channel_id).await?;
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?
}
} }
ServerBan::create(db, &server, target.id, data.reason) ServerBan::create(db, &server, target.id, data.reason)
+4 -118
View File
@@ -1,25 +1,14 @@
use std::collections::HashSet; use std::collections::HashSet;
use revolt_database::{ use revolt_database::{
events::client::EventV1, util::{permissions::DatabasePermissionQuery, reference::Reference},
util::{
permissions::{perms, DatabasePermissionQuery},
reference::Reference,
},
voice::{
get_channel_node, get_user_voice_channel_in_server, set_channel_node,
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
VoiceClient,
},
Database, File, PartialMember, User, Database, File, PartialMember, User,
}; };
use revolt_models::v0::{self, FieldsMember}; use revolt_models::v0;
use revolt_permissions::{ use revolt_permissions::{calculate_server_permissions, ChannelPermission};
calculate_channel_permissions, calculate_server_permissions, ChannelPermission,
};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::{form::validate::Contains, serde::json::Json, State}; use rocket::{serde::json::Json, State};
use validator::Validate; use validator::Validate;
/// # Edit Member /// # Edit Member
@@ -29,7 +18,6 @@ use validator::Validate;
#[patch("/<server>/members/<member>", data = "<data>")] #[patch("/<server>/members/<member>", data = "<data>")]
pub async fn edit( pub async fn edit(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
server: Reference<'_>, server: Reference<'_>,
member: Reference<'_>, member: Reference<'_>,
@@ -44,7 +32,6 @@ pub async fn edit(
// Fetch server and member // Fetch server and member
let mut server = server.as_server(db).await?; let mut server = server.as_server(db).await?;
let target_user = member.as_user(db).await?;
let mut member = member.as_member(db, &server.id).await?; let mut member = member.as_member(db, &server.id).await?;
// Fetch our currrent permissions // Fetch our currrent permissions
@@ -80,44 +67,6 @@ pub async fn edit(
permissions.throw_if_lacking_channel_permission(ChannelPermission::TimeoutMembers)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::TimeoutMembers)?;
} }
if data.can_publish.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::MuteMembers)?;
}
if data.can_receive.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?;
}
let new_voice_channel = if let Some(new_channel) = &data.voice_channel {
if !voice_client.is_enabled() {
return Err(create_error!(LiveKitUnavailable));
};
permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveMembers)?;
// ensure the channel we are moving them to is in the server and is a voice channel
let channel = Reference::from_unchecked(new_channel)
.as_channel(db)
.await
.map_err(|_| create_error!(UnknownChannel))?;
if channel.server().is_none_or(|v| v != member.id.server) {
Err(create_error!(UnknownChannel))?
}
if get_user_voice_channel_in_server(&target_user.id, &server.id)
.await?
.is_none()
{
Err(create_error!(NotConnected))?
};
Some(channel)
} else {
None
};
// Resolve our ranking // Resolve our ranking
let our_ranking = query.get_member_rank().unwrap_or(i64::MIN); let our_ranking = query.get_member_rank().unwrap_or(i64::MIN);
@@ -153,17 +102,12 @@ pub async fn edit(
roles, roles,
timeout, timeout,
remove, remove,
can_publish,
can_receive,
voice_channel: _,
} = data; } = data;
let mut partial = PartialMember { let mut partial = PartialMember {
nickname, nickname,
roles, roles,
timeout, timeout,
can_publish,
can_receive,
..Default::default() ..Default::default()
}; };
@@ -179,67 +123,9 @@ pub async fn edit(
partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?); partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?);
} }
let remove_contains_voice = remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive);
member member
.update(db, partial, remove.into_iter().map(Into::into).collect()) .update(db, partial, remove.into_iter().map(Into::into).collect())
.await?; .await?;
if let Some(new_voice_channel) = new_voice_channel {
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
{
let old_node = get_channel_node(&channel).await?.unwrap();
let new_node = match get_channel_node(new_voice_channel.id()).await? {
Some(node) => node,
None => {
set_channel_node(new_voice_channel.id(), &old_node).await?;
old_node.clone()
}
};
set_user_moved_from_voice(&channel, new_voice_channel.id(), &target_user.id).await?;
set_user_moved_to_voice(new_voice_channel.id(), &channel, &target_user.id).await?;
let mut query = perms(db, &target_user).channel(&new_voice_channel);
let permissions = calculate_channel_permissions(&mut query).await;
voice_client
.create_room(&new_node, &new_voice_channel)
.await?;
let token = voice_client
.create_token(&new_node, db, &target_user, permissions, &new_voice_channel)
.await?;
voice_client
.remove_user(&old_node, &target_user.id, &channel)
.await?;
EventV1::UserMoveVoiceChannel {
node: new_node,
token,
}
.p_user(target_user.id.clone(), db)
.await;
};
} else if can_publish.is_some() || can_receive.is_some() || remove_contains_voice {
if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await?
{
let node = get_channel_node(&channel).await?.unwrap();
let channel = Reference::from_unchecked(&channel).as_channel(db).await?;
sync_user_voice_permissions(
db,
voice_client,
&node,
&user,
&channel,
Some(&server),
None,
)
.await?;
};
};
Ok(Json(member.into())) Ok(Json(member.into()))
} }
@@ -1,6 +1,5 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient},
Database, RemovalIntention, User, Database, RemovalIntention, User,
}; };
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -15,7 +14,6 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>/members/<member>")] #[delete("/<target>/members/<member>")]
pub async fn kick( pub async fn kick(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
member: Reference<'_>, member: Reference<'_>,
@@ -44,15 +42,6 @@ pub async fn kick(
member member
.remove(db, &server, RemovalIntention::Kick, false) .remove(db, &server, RemovalIntention::Kick, false)
.await?; .await
.map(|_| EmptyResponse)
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
voice_client.remove_user(&node, &user.id, &channel_id).await?;
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?;
};
Ok(EmptyResponse)
} }
@@ -1,6 +1,5 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{sync_voice_permissions, VoiceClient},
Database, User, Database, User,
}; };
use revolt_models::v0; use revolt_models::v0;
@@ -15,7 +14,6 @@ use rocket::{serde::json::Json, State};
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)] #[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn set_role_permission( pub async fn set_role_permission(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
role_id: String, role_id: String,
@@ -24,13 +22,8 @@ pub async fn set_role_permission(
let data = data.into_inner(); let data = data.into_inner();
let mut server = target.as_server(db).await?; let mut server = target.as_server(db).await?;
if let Some((current_value, rank)) = server.roles.get(&role_id).map(|x| (x.permissions, x.rank))
let (current_value, rank) = server {
.roles
.get(&role_id)
.map(|x| (x.permissions, x.rank))
.ok_or_else(|| create_error!(NotFound))?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
let permissions = calculate_server_permissions(&mut query).await; let permissions = calculate_server_permissions(&mut query).await;
@@ -51,11 +44,8 @@ pub async fn set_role_permission(
.set_role_permission(db, &role_id, data.permissions.into()) .set_role_permission(db, &role_id, data.permissions.into())
.await?; .await?;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
};
Ok(Json(server.into())) Ok(Json(server.into()))
} else {
Err(create_error!(NotFound))
}
} }
@@ -1,5 +1,6 @@
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, PartialServer, User util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialServer, User,
}; };
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{ use revolt_permissions::{
@@ -15,7 +16,6 @@ use rocket::{serde::json::Json, State};
#[put("/<target>/permissions/default", data = "<data>", rank = 1)] #[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn set_default_server_permissions( pub async fn set_default_server_permissions(
db: &State<Database>, db: &State<Database>,
voice_client: &State<VoiceClient>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Json<DataPermissionsValue>, data: Json<DataPermissionsValue>,
@@ -50,11 +50,5 @@ pub async fn set_default_server_permissions(
) )
.await?; .await?;
for channel_id in &server.channels {
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
};
Ok(Json(server.into())) Ok(Json(server.into()))
} }

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