diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 00000000..867978dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,147 @@
+name: Bug Report
+description: Report a bug in Stoat — please fill out every required field to help us triage quickly.
+title: "[Bug]: "
+labels:
+ - bug
+ - unconfirmed
+
+body:
+ # ── Thank you section ────────────────────────────────────────────────────────
+ - type: markdown
+ attributes:
+ value: |
+ **Thank you for taking the time to report a bug for Stoat!**
+
+ Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue.
+
+ # ── Pre-flight checks ────────────────────────────────────────────────────────
+ - type: checkboxes
+ id: preflight
+ attributes:
+ label: Pre-flight checklist
+ description: Please confirm all of the following before submitting.
+ options:
+ - label: I am on the latest available version of Stoat.
+ required: true
+ - label: I searched existing issues and did not find a duplicate.
+ required: true
+ - label: This is a bug report, not a support question or feature request.
+ required: true
+
+ # ── Bug description ──────────────────────────────────────────────────────────
+ - type: textarea
+ id: description
+ attributes:
+ label: Bug description
+ description: >
+ What happened? What did you expect to happen instead?
+ Keep this brief — detailed steps go in the next field.
+ placeholder: "Example: When I open a DM thread, new messages from the other person do not appear unless I reload the page."
+ validations:
+ required: true
+
+ # ── Steps to reproduce ───────────────────────────────────────────────────────
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to reproduce
+ description: >
+ Provide a numbered list of every step needed to trigger the bug.
+ The clearer this is, the faster we can fix it.
+ placeholder: |
+ 1. Log in to Stoat on the web client.
+ 2. Open a direct message conversation.
+ 3. Ask the other person to send a message.
+ 4. Observe that the message does not appear without reloading.
+ validations:
+ required: true
+
+ # ── Expected behavior ────────────────────────────────────────────────────────
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected behavior
+ description: What should have happened?
+ placeholder: "New messages should appear in real time without a page reload."
+ validations:
+ required: true
+
+ # ── Client / platform ────────────────────────────────────────────────────────
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Client / platform
+ description: Which Stoat client are you using?
+ options:
+ - Web (browser)
+ - Stoat for Desktop
+ - Android
+ - iOS
+ - Self-hosted server
+ validations:
+ required: true
+
+ # ── Client version ───────────────────────────────────────────────────────────
+ - type: input
+ id: version
+ attributes:
+ label: Client version
+ description: >
+ Find this in **Settings**. For the web client, include the build
+ number shown in the footer or About page.
+ placeholder: "e.g. Stoat for Desktop 1.3.0 or 0.2.1 (2025-10-10)"
+ validations:
+ required: true
+
+ # ── OS / browser details ─────────────────────────────────────────────────────
+ - type: input
+ id: os
+ attributes:
+ label: OS / browser details
+ description: Your operating system and, for web bugs, your browser and its version.
+ placeholder: "e.g. Windows 11, Chrome 121 or macOS 14.3, Stoat for Desktop 1.3.0 or Android 14"
+ validations:
+ required: true
+
+ # ── Reproducibility ──────────────────────────────────────────────────────────
+ - type: dropdown
+ id: reproducibility
+ attributes:
+ label: Reproducibility
+ description: How consistently does this bug occur?
+ options:
+ - Always
+ - Usually
+ - Rarely
+ - Only once
+ - Unknown
+ validations:
+ required: true
+
+ # ── Screenshots or video ─────────────────────────────────────────────────────
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots or video
+ description: Paste images or drag-and-drop a screen recording here. GitHub accepts common image and video formats.
+ validations:
+ required: false
+
+ # ── Additional context ───────────────────────────────────────────────────────
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional context
+ description: >
+ Anything else that might help: workarounds you found, links to related issues,
+ self-hosted configuration details (Redis, MongoDB, LiveKit, SMTP), network environment, etc.
+ validations:
+ required: false
+
+ # ── Closing note ─────────────────────────────────────────────────────────────
+ - type: markdown
+ attributes:
+ value: |
+ ---
+ A maintainer will review your report. Please watch for follow-up questions — issues
+ that go unanswered are harder to resolve and may be closed after 30 days of inactivity.
diff --git a/.github/workflows/release-webhook.yml b/.github/workflows/release-webhook.yml
index 2262162c..46504b1a 100644
--- a/.github/workflows/release-webhook.yml
+++ b/.github/workflows/release-webhook.yml
@@ -20,4 +20,4 @@ jobs:
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
- -d "{\"content\": \"$RELEASE_URL\"}"
+ -d "{\"content\": \"$RELEASE_URL\"}"
\ No newline at end of file
diff --git a/README.md b/README.md
index d21bb2c4..c80f4595 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,14 @@
Stoat Backend
- [](https://github.com/revoltchat/backend/stargazers)
- [](https://github.com/revoltchat/backend/network/members)
- [](https://github.com/revoltchat/backend/pulls)
- [](https://github.com/revoltchat/backend/issues)
- [](https://github.com/revoltchat/backend/graphs/contributors)
- [](https://github.com/revoltchat/backend/blob/main/LICENSE)
+ [](https://github.com/stoatchat/stoatchat/stargazers)
+ [](https://github.com/stoatchat/stoatchat/network/members)
+ [](https://github.com/stoatchat/stoatchat/pulls)
+ [](https://github.com/stoatchat/stoatchat/issues)
+ [](https://github.com/stoatchat/stoatchat/graphs/contributors)
+ [](https://github.com/stoatchat/stoatchat/blob/main/LICENSE)
-The services and libraries that power the Revolt service.
+The services and libraries that power the Stoat service.
| Crate | Path | Description | |
@@ -39,7 +39,7 @@ Rust 1.86.0 or higher.
## Development Guide
-Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
+Before contributing, make yourself familiar with [our contribution guidelines](https://developers.stoat.chat/developing/contrib/) and the [technical documentation for this project](https://stoatchat.github.io/stoatchat/).
Before getting started, you'll want to install:
@@ -70,8 +70,8 @@ As a heads-up, the development environment uses the following ports:
Now you can clone and build the project:
```bash
-git clone https://github.com/revoltchat/backend revolt-backend
-cd revolt-backend
+git clone https://github.com/stoatchat/stoatchat stoat-backend
+cd stoat-backend
mise build
```
@@ -158,16 +158,11 @@ You can start a web client by doing the following:
corepack enable
# clone the web client and run it:
-git clone --recursive https://github.com/revoltchat/revite
-cd revite
-yarn
-yarn build:deps
-echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
-yarn dev --port 14701
+git clone --recursive https://github.com/stoatchat/for-web stoat-web
+cd stoat-web
+# refer to stoat-web/README.md for startup, creating an account and loging in
```
-Then go to http://local.revolt.chat:14701 to create an account/login.
-
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
## Deployment Guide
@@ -198,7 +193,7 @@ Tag and push a new release by running:
just release
```
-If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
+If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/stoatchat/stoatchat/releases/new) to create a changelog.
## Testing
@@ -217,6 +212,6 @@ TEST_DB=MONGODB cargo nextest run
## License
-The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
+The Stoat backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/stoatchat/stoatchat/blob/master/LICENSE).
**Individual crates may supply their own licenses!**
diff --git a/Revolt.toml b/Revolt.toml
index 9140e983..666bed14 100644
--- a/Revolt.toml
+++ b/Revolt.toml
@@ -15,7 +15,7 @@ host = "127.0.0.1"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
-# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
+# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat:14701"
diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs
index 0fbe1624..c6d07d2f 100644
--- a/crates/bonfire/src/events/impl.rs
+++ b/crates/bonfire/src/events/impl.rs
@@ -4,7 +4,7 @@ use futures::future::join_all;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
- voice::get_channel_voice_state,
+ voice::{get_channel_voice_state, UserVoiceChannel},
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
@@ -167,7 +167,9 @@ impl State {
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
- if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
+ if let Ok(Some(voice_state)) =
+ get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
+ {
if let Some(server) = channel.server() {
let set = voice_state_server_members
.entry(server.to_string())
diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml
index 2a0b5b01..e65843d7 100644
--- a/crates/core/config/Revolt.toml
+++ b/crates/core/config/Revolt.toml
@@ -11,7 +11,7 @@ redis = "redis://redis/"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
-# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
+# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat"
@@ -34,7 +34,7 @@ password = "rabbitpass"
[api.registration]
# Whether an invite should be required for registration
-# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
+# See https://github.com/stoatchat/self-hosted#making-your-instance-invite-only
invite_only = false
[api.smtp]
diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs
index 57c25149..1a9df853 100644
--- a/crates/core/config/src/lib.rs
+++ b/crates/core/config/src/lib.rs
@@ -463,12 +463,14 @@ pub async fn config() -> Settings {
let mut config = read().await.try_deserialize::().unwrap();
// inject REDIS_URI for redis-kiss library
- if std::env::var("REDIS_URL").is_err() {
+ if std::env::var("REDIS_URI").is_err() {
std::env::set_var("REDIS_URI", config.database.redis.clone());
}
// auto-detect production nodes
- if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
+ if config.hosts.api.contains("https")
+ && (config.hosts.api.contains("revolt.chat") || config.hosts.api.contains("stoat.chat"))
+ {
config.production = true;
}
diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs
index 1ecaddfe..b715bdd4 100644
--- a/crates/core/database/src/models/messages/model.rs
+++ b/crates/core/database/src/models/messages/model.rs
@@ -687,8 +687,13 @@ impl Message {
)
.await?;
+ let is_dm_or_group = matches!(
+ channel,
+ Channel::DirectMessage { .. } | Channel::Group { .. }
+ );
+
if !self.has_suppressed_notifications()
- && (self.mentions.is_some() || self.contains_mass_push_mention())
+ && (is_dm_or_group || self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
#[cfg(feature = "tasks")]
diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs
index 0062a270..e242c62f 100644
--- a/crates/core/database/src/models/server_members/model.rs
+++ b/crates/core/database/src/models/server_members/model.rs
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{
- events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
- Database, File, Server, SystemMessage, User,
+ events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
+ Server, SystemMessage, User,
};
fn default_true() -> bool {
@@ -45,7 +45,6 @@ auto_derived_partial!(
/// 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.
// If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option
@@ -153,7 +152,11 @@ impl Member {
#[cfg(feature = "voice")]
for channel in &channels {
- if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(channel).await {
+ if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(
+ &crate::voice::UserVoiceChannel::from_channel(channel),
+ )
+ .await
+ {
voice_states.push(voice_state)
}
}
@@ -175,7 +178,7 @@ impl Member {
.map(|channel| channel.into())
.collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
- voice_states
+ voice_states,
}
.private(user.id.clone())
.await;
@@ -225,14 +228,14 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
- FieldsMember::JoinedAt => {},
+ FieldsMember::JoinedAt => {}
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
FieldsMember::CanReceive => self.can_receive = true,
FieldsMember::CanPublish => self.can_publish = true,
- FieldsMember::VoiceChannel => {},
+ FieldsMember::VoiceChannel => {}
}
}
diff --git a/crates/core/database/src/voice/mod.rs b/crates/core/database/src/voice/mod.rs
index dde70a71..836de92a 100644
--- a/crates/core/database/src/voice/mod.rs
+++ b/crates/core/database/src/voice/mod.rs
@@ -1,3 +1,5 @@
+use std::fmt::{Display, Write};
+
use crate::{
events::client::EventV1,
models::{Channel, User},
@@ -6,7 +8,11 @@ use crate::{
};
use iso8601_timestamp::{Duration, Timestamp};
use livekit_protocol::ParticipantPermission;
-use redis_kiss::{get_connection as _get_connection, redis::Pipeline, AsyncCommands, Conn};
+use redis_kiss::{
+ get_connection as _get_connection,
+ redis::{FromRedisValue, Pipeline, RedisError, RedisWrite, ToRedisArgs, Value},
+ AsyncCommands, Conn,
+};
use revolt_config::FeaturesLimits;
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
@@ -16,16 +22,18 @@ mod voice_client;
pub use voice_client::VoiceClient;
async fn get_connection() -> Result {
- _get_connection().await.map_err(|_| create_error!(InternalError))
+ _get_connection()
+ .await
+ .map_err(|_| create_error!(InternalError))
}
-pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
+pub async fn raise_if_in_voice(user: &User, channel: &UserVoiceChannel) -> 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
if conn
- .sismember(format!("vc:{}", &user.id), channel_id)
+ .sismember(format!("vc:{}", &user.id), channel)
.await
.to_internal_error()?
{
@@ -43,23 +51,31 @@ pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
Ok(())
}
-pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> {
+pub async fn set_channel_node(channel_id: &str, node: &str) -> Result<()> {
get_connection()
.await?
- .set(format!("node:{channel}"), node)
+ .set(format!("node:{channel_id}"), node)
.await
.to_internal_error()
}
-pub async fn get_channel_node(channel: &str) -> Result> {
+pub async fn get_channel_node(channel_id: &str) -> Result > {
get_connection()
.await?
- .get(format!("node:{channel}"))
+ .get(format!("node:{channel_id}"))
.await
.to_internal_error()
}
-pub async fn get_user_voice_channels(user_id: &str) -> Result> {
+pub async fn delete_channel_node(channel_id: &str) -> Result<()> {
+ get_connection()
+ .await?
+ .del(format!("node:{channel_id}"))
+ .await
+ .to_internal_error()
+}
+
+pub async fn get_user_voice_channels(user_id: &str) -> Result> {
get_connection()
.await?
.smembers(format!("vc:{user_id}"))
@@ -68,14 +84,14 @@ pub async fn get_user_voice_channels(user_id: &str) -> Result> {
}
pub async fn set_user_moved_from_voice(
- old_channel: &str,
- new_channel: &str,
+ old_channel_id: &str,
+ new_channel: &UserVoiceChannel,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
.set_ex(
- format!("moved_from:{user_id}:{old_channel}"),
+ format!("moved_from:{user_id}:{old_channel_id}"),
new_channel,
10,
)
@@ -92,18 +108,25 @@ pub async fn get_user_moved_from_voice(channel_id: &str, user_id: &str) -> Resul
}
pub async fn set_user_moved_to_voice(
- new_channel: &str,
- old_channel: &str,
+ new_channel_id: &str,
+ old_channel: &UserVoiceChannel,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
- .set_ex(format!("moved_to:{user_id}:{new_channel}"), old_channel, 10)
+ .set_ex(
+ format!("moved_to:{user_id}:{new_channel_id}"),
+ old_channel,
+ 10,
+ )
.await
.to_internal_error()
}
-pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result> {
+pub async fn get_user_moved_to_voice(
+ channel_id: &str,
+ user_id: &str,
+) -> Result > {
get_connection()
.await?
.get_del(format!("moved_to:{user_id}:{channel_id}"))
@@ -111,10 +134,10 @@ pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<
.to_internal_error()
}
-pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result {
+pub async fn is_in_voice_channel(user_id: &str, channel: &UserVoiceChannel) -> Result {
get_connection()
.await?
- .sismember(format!("vc:{user_id}"), channel_id)
+ .sismember(format!("vc:{user_id}"), channel)
.await
.to_internal_error()
}
@@ -148,12 +171,15 @@ pub fn get_allowed_sources(
}
pub async fn create_voice_state(
- channel_id: &str,
- server_id: Option<&str>,
+ channel: &UserVoiceChannel,
user_id: &str,
joined_at: Timestamp,
) -> Result {
- let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
+ let unique_key = format!(
+ "{}:{}",
+ &user_id,
+ channel.server_id.as_ref().unwrap_or(&channel.id)
+ );
let voice_state = UserVoiceState {
joined_at,
@@ -165,9 +191,9 @@ pub async fn create_voice_state(
};
Pipeline::new()
- .sadd(format!("vc_members:{channel_id}"), user_id)
- .sadd(format!("vc:{user_id}"), channel_id)
- .set(&unique_key, channel_id)
+ .sadd(format!("vc_members:{}", &channel.id), user_id)
+ .sadd(format!("vc:{user_id}"), channel)
+ .set(&unique_key, &channel.id)
.set(
format!("joined_at:{unique_key}"),
joined_at
@@ -194,16 +220,16 @@ pub async fn create_voice_state(
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));
+pub async fn delete_voice_state(channel: &UserVoiceChannel, user_id: &str) -> Result<()> {
+ let unique_key = format!(
+ "{}:{}",
+ &user_id,
+ channel.server_id.as_ref().unwrap_or(&channel.id)
+ );
Pipeline::new()
- .srem(format!("vc_members:{channel_id}"), user_id)
- .srem(format!("vc:{user_id}"), channel_id)
+ .srem(format!("vc_members:{}", &channel.id), user_id)
+ .srem(format!("vc:{user_id}"), channel)
.del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
@@ -218,19 +244,19 @@ pub async fn delete_voice_state(
}
pub async fn delete_channel_voice_state(
- channel_id: &str,
- server_id: Option<&str>,
+ channel: &UserVoiceChannel,
user_ids: &[String],
) -> Result<()> {
- let parent_id = server_id.unwrap_or(channel_id);
+ let parent_id = channel.server_id.as_ref().unwrap_or(&channel.id);
let mut pipeline = Pipeline::new();
- pipeline.del(format!("vc_members:{channel_id}"));
+ pipeline.del(format!("vc_members:{}", &channel.id));
+ pipeline.del(format!("node:{}", &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(&[
+ pipeline.srem(format!("vc:{user_id}"), channel).del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
@@ -247,8 +273,7 @@ pub async fn delete_channel_voice_state(
}
pub async fn update_voice_state_tracks(
- channel_id: &str,
- server_id: Option<&str>,
+ channel: &UserVoiceChannel,
user_id: &str,
added: bool,
track: i32,
@@ -273,18 +298,21 @@ pub async fn update_voice_state_tracks(
_ => unreachable!(),
};
- update_voice_state(channel_id, server_id, user_id, &partial).await?;
+ update_voice_state(channel, user_id, &partial).await?;
Ok(partial)
}
pub async fn update_voice_state(
- channel_id: &str,
- server_id: Option<&str>,
+ channel: &UserVoiceChannel,
user_id: &str,
partial: &PartialUserVoiceState,
) -> Result<()> {
- let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
+ let unique_key = format!(
+ "{}:{}",
+ &user_id,
+ channel.server_id.as_ref().unwrap_or(&channel.id)
+ );
let mut pipeline = Pipeline::new();
@@ -310,21 +338,24 @@ pub async fn update_voice_state(
.to_internal_error()
}
-pub async fn get_voice_channel_members(channel_id: &str) -> Result>> {
+pub async fn get_voice_channel_members(channel: &UserVoiceChannel) -> Result >> {
get_connection()
.await?
- .smembers::<_, Option>>(format!("vc_members:{channel_id}"))
+ .smembers::<_, Option>>(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>,
+ channel: &UserVoiceChannel,
user_id: &str,
) -> Result> {
- let unique_key = format!("{}:{}", user_id, server_id.unwrap_or(channel_id));
+ let unique_key = format!(
+ "{}:{}",
+ &user_id,
+ channel.server_id.as_ref().unwrap_or(&channel.id)
+ );
let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection()
.await?
@@ -365,21 +396,21 @@ pub async fn get_voice_state(
}
}
-pub async fn get_channel_voice_state(channel: &Channel) -> Result > {
- let members = get_voice_channel_members(channel.id()).await?;
-
- let server = channel.server();
+pub async fn get_channel_voice_state(
+ channel: &UserVoiceChannel,
+) -> Result > {
+ let members = get_voice_channel_members(channel).await?;
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? {
+ if let Some(voice_state) = get_voice_state(channel, &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?;
+ delete_voice_state(channel, &user_id).await?;
}
}
@@ -387,7 +418,7 @@ pub async fn get_channel_voice_state(channel: &Channel) -> Result Result Result<()> {
+pub async fn move_user(user: &str, from_channel_id: &str, to_channel_id: &str) -> Result<()> {
get_connection()
.await?
.smove(
- format!("vc-members-{from}"),
- format!("vc-members-{to}"),
+ format!("vc_members:{from_channel_id}"),
+ format!("vc_members:{to_channel_id}"),
user,
)
.await
@@ -414,11 +445,13 @@ pub async fn sync_voice_permissions(
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
+ let user_voice_channel = UserVoiceChannel::from_channel(channel);
+
let Some(node) = get_channel_node(channel.id()).await? else {
return Ok(());
};
- for user_id in get_voice_channel_members(channel.id())
+ for user_id in get_voice_channel_members(&user_voice_channel)
.await?
.iter()
.flatten()
@@ -458,7 +491,9 @@ pub async fn sync_user_voice_permissions(
.as_ref()
.is_none_or(|member| member.roles.iter().any(|r| r == role_id))
}) {
- let Some(voice_state) = get_voice_state(channel_id, server_id, &user.id).await? else {
+ let user_voice_channel = UserVoiceChannel::from_channel(channel);
+
+ let Some(voice_state) = get_voice_state(&user_voice_channel, &user.id).await? else {
return Ok(());
};
@@ -489,7 +524,7 @@ pub async fn sync_user_voice_permissions(
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?;
+ update_voice_state(&user_voice_channel, &user.id, &update_event).await?;
voice_client
.update_permissions(
@@ -567,34 +602,95 @@ pub async fn get_call_notification_recipients(
.to_internal_error()
}
-pub async fn remove_user_from_voice_channels(db: &Database, voice_client: &VoiceClient, user_id: &str) -> Result<()> {
- for channel_id in get_user_voice_channels(user_id).await? {
- remove_user_from_voice_channel(db, voice_client, &channel_id, user_id).await?;
- };
-
- Ok(())
-}
-
-pub async fn remove_user_from_voice_channel(db: &Database, voice_client: &VoiceClient, channel_id: &str, user_id: &str) -> Result<()> {
- if let Some(node) = get_channel_node(channel_id).await? {
- let _ = voice_client.remove_user(&node, user_id, channel_id).await;
+pub async fn remove_user_from_voice_channels(
+ voice_client: &VoiceClient,
+ user_id: &str,
+) -> Result<()> {
+ for channel in get_user_voice_channels(user_id).await? {
+ remove_user_from_voice_channel(voice_client, &channel, user_id).await?;
}
- let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
+ Ok(())
+}
- delete_voice_state(channel_id, channel.server(), user_id).await?;
+pub async fn remove_user_from_voice_channel(
+ voice_client: &VoiceClient,
+ channel: &UserVoiceChannel,
+ user_id: &str,
+) -> Result<()> {
+ if let Some(node) = get_channel_node(&channel.id).await? {
+ let _ = voice_client.remove_user(&node, user_id, &channel.id).await;
+ }
+
+ delete_voice_state(channel, user_id).await?;
Ok(())
}
-pub async fn delete_voice_channel(voice_client: &VoiceClient, channel_id: &str, server_id: Option<&str>) -> Result<()> {
- if let Some(users) = get_voice_channel_members(channel_id).await? {
- let node = get_channel_node(channel_id).await?.unwrap();
+pub async fn delete_voice_channel(
+ voice_client: &VoiceClient,
+ channel: &UserVoiceChannel,
+) -> Result<()> {
+ if let Some(users) = get_voice_channel_members(channel).await? {
+ let node = get_channel_node(&channel.id).await?.unwrap();
+ voice_client.delete_room(&node, &channel.id).await?;
- voice_client.delete_room(&node, channel_id).await?;
-
- delete_channel_voice_state(channel_id, server_id, &users).await?;
+ delete_channel_voice_state(channel, &users).await?;
};
Ok(())
-}
\ No newline at end of file
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RoomMetadata {
+ pub server: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct UserVoiceChannel {
+ pub id: String,
+ pub server_id: Option,
+}
+
+impl UserVoiceChannel {
+ pub fn from_string(input: String) -> Self {
+ let mut parts = input.splitn(2, '-');
+
+ Self {
+ id: parts.next().unwrap().to_string(),
+ server_id: parts.next().map(ToString::to_string),
+ }
+ }
+
+ pub fn from_channel(channel: &Channel) -> Self {
+ Self {
+ id: channel.id().to_string(),
+ server_id: channel.server().map(ToString::to_string),
+ }
+ }
+}
+
+impl Display for UserVoiceChannel {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(&self.id)?;
+
+ if let Some(server_id) = &self.server_id {
+ f.write_char('-')?;
+ f.write_str(server_id)?
+ };
+
+ Ok(())
+ }
+}
+
+impl ToRedisArgs for UserVoiceChannel {
+ fn write_redis_args(&self, out: &mut W) {
+ out.write_arg_fmt(self);
+ }
+}
+
+impl FromRedisValue for UserVoiceChannel {
+ fn from_redis_value(v: &Value) -> Result {
+ String::from_redis_value(v).map(UserVoiceChannel::from_string)
+ }
+}
diff --git a/crates/core/database/src/voice/voice_client.rs b/crates/core/database/src/voice/voice_client.rs
index 967f3fef..94d1f22b 100644
--- a/crates/core/database/src/voice/voice_client.rs
+++ b/crates/core/database/src/voice/voice_client.rs
@@ -1,5 +1,6 @@
use crate::{
models::{Channel, User},
+ voice::RoomMetadata,
Database,
};
use livekit_api::{
@@ -102,11 +103,16 @@ impl VoiceClient {
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result {
let room = self.get_node(node)?;
+ let metadata = RoomMetadata {
+ server: channel.server().map(|id| id.to_string()),
+ };
+
room.client
.create_room(
channel.id(),
CreateRoomOptions {
empty_timeout: 5 * 60, // 5 minutes,
+ metadata: serde_json::to_string(&metadata).to_internal_error()?,
..Default::default()
},
)
diff --git a/crates/core/database/templates/deletion.whitelabel.txt b/crates/core/database/templates/deletion.whitelabel.txt
index 3447b351..b040a51e 100644
--- a/crates/core/database/templates/deletion.whitelabel.txt
+++ b/crates/core/database/templates/deletion.whitelabel.txt
@@ -6,4 +6,4 @@ This email is intended for {{email}}
This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here:
-https://developers.stoat.chat/faq.html
+https://developers.stoat.chat/faq/
diff --git a/crates/core/database/templates/reset.whitelabel.txt b/crates/core/database/templates/reset.whitelabel.txt
index b8c4adc9..10e8ca2d 100644
--- a/crates/core/database/templates/reset.whitelabel.txt
+++ b/crates/core/database/templates/reset.whitelabel.txt
@@ -6,4 +6,4 @@ This email is intended for {{email}}
This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here:
-https://developers.revolt.chat/faq.html
+https://developers.stoat.chat/faq/
diff --git a/crates/core/database/templates/suspension.html b/crates/core/database/templates/suspension.html
index 2e4c2298..27b7331e 100644
--- a/crates/core/database/templates/suspension.html
+++ b/crates/core/database/templates/suspension.html
@@ -107,7 +107,7 @@
Further violations may result in a permanent ban depending on
severity, please abide by the
- Acceptable Usage Policy .
+ Community Guidelines .
diff --git a/crates/core/database/templates/suspension.txt b/crates/core/database/templates/suspension.txt
index d247eae3..293f8a0c 100644
--- a/crates/core/database/templates/suspension.txt
+++ b/crates/core/database/templates/suspension.txt
@@ -3,7 +3,7 @@ Your account has been suspended, for one or more reasons:
You will be able to use your account again in {{duration}} days.
-Further violations may result in a permanent ban depending on severity, please abide by the Acceptable Usage Policy (https://revolt.chat/aup).
+Further violations may result in a permanent ban depending on severity, please abide by the Community Guidelines (https://stoat.chat/legal/community-guidelines).
Ban evasion is prohibited and will be dealt with accordingly.
diff --git a/crates/core/database/templates/suspension.whitelabel.txt b/crates/core/database/templates/suspension.whitelabel.txt
index 45d9aa56..bd82145d 100644
--- a/crates/core/database/templates/suspension.whitelabel.txt
+++ b/crates/core/database/templates/suspension.whitelabel.txt
@@ -3,6 +3,6 @@ Your account has been suspended, for one or more reasons:
This email is intended for {{email}}
-This email has no association with Revolt or Revolt Platforms Ltd.
+This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here:
-https://developers.revolt.chat/faq.html
+https://developers.stoat.chat/faq/
diff --git a/crates/core/database/templates/verify.whitelabel.txt b/crates/core/database/templates/verify.whitelabel.txt
index 547128b9..16f1c4b1 100644
--- a/crates/core/database/templates/verify.whitelabel.txt
+++ b/crates/core/database/templates/verify.whitelabel.txt
@@ -7,4 +7,4 @@ This email is intended for {{email}}
This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here:
-https://developers.revolt.chat/faq.html
+https://developers.stoat.chat/faq/
diff --git a/crates/core/permissions/src/models/channel.rs b/crates/core/permissions/src/models/channel.rs
index 9e916026..450fda64 100644
--- a/crates/core/permissions/src/models/channel.rs
+++ b/crates/core/permissions/src/models/channel.rs
@@ -141,7 +141,7 @@ pub static DEFAULT_PERMISSION: Lazy = Lazy::new(|| {
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy = Lazy::new(|| {
- DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React)
+ DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React + ChannelPermission::Masquerade)
});
pub static DEFAULT_PERMISSION_SERVER: Lazy = Lazy::new(|| {
diff --git a/crates/daemons/crond/Dockerfile b/crates/daemons/crond/Dockerfile
index c1bdf80f..f1852457 100644
--- a/crates/daemons/crond/Dockerfile
+++ b/crates/daemons/crond/Dockerfile
@@ -5,6 +5,7 @@ FROM debian:12 AS debian
# Bundle Stage
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /home/rust/src/target/release/revolt-crond ./
+COPY --from=debian /usr/bin/uname /usr/bin/uname
USER nonroot
CMD ["./revolt-crond"]
\ No newline at end of file
diff --git a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs
index b5b89dc1..873b55cc 100644
--- a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs
@@ -128,7 +128,7 @@ impl DmCallConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs
index d525138c..115e4a26 100644
--- a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs
@@ -101,7 +101,7 @@ impl FRAcceptedConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs
index f64bc92b..c611dfaf 100644
--- a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs
@@ -101,7 +101,7 @@ impl FRReceivedConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/pushd/src/consumers/inbound/generic.rs b/crates/daemons/pushd/src/consumers/inbound/generic.rs
index aa3950d7..302b1700 100644
--- a/crates/daemons/pushd/src/consumers/inbound/generic.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/generic.rs
@@ -107,7 +107,7 @@ impl GenericConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs
index bf5dd453..cb6f3c44 100644
--- a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs
@@ -104,7 +104,7 @@ impl MassMessageConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/pushd/src/consumers/inbound/message.rs b/crates/daemons/pushd/src/consumers/inbound/message.rs
index 6b9d00fe..420c3707 100644
--- a/crates/daemons/pushd/src/consumers/inbound/message.rs
+++ b/crates/daemons/pushd/src/consumers/inbound/message.rs
@@ -107,7 +107,7 @@ impl MessageConsumer {
config.pushd.vapid.queue.as_str(),
)
.finish();
- sendable.extras.insert("p265dh".to_string(), sub.p256dh);
+ sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
diff --git a/crates/daemons/voice-ingress/src/api.rs b/crates/daemons/voice-ingress/src/api.rs
index 2af3ab26..dbe8934e 100644
--- a/crates/daemons/voice-ingress/src/api.rs
+++ b/crates/daemons/voice-ingress/src/api.rs
@@ -5,9 +5,9 @@ use revolt_database::{
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
- create_voice_state, delete_voice_state,
- get_user_moved_from_voice, get_user_moved_to_voice,
- update_voice_state_tracks, VoiceClient,
+ create_voice_state, delete_channel_voice_state, delete_voice_state,
+ get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
+ RoomMetadata, UserVoiceChannel, VoiceClient,
},
Database, AMQP,
};
@@ -51,27 +51,34 @@ pub async fn ingress(
let channel_id = event.room.as_ref().map(|r| &r.name);
let user_id = event.participant.as_ref().map(|r| &r.identity);
+ let room_metadata = if let Some(room) = event.room.as_ref() {
+ Some(serde_json::from_str::(&room.metadata).to_internal_error()?)
+ } else {
+ None
+ };
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 server_id = room_metadata.to_internal_error()?.server;
+ let channel = UserVoiceChannel {
+ id: channel_id.clone(),
+ server_id: server_id.clone(),
+ };
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?;
+ let voice_state = create_voice_state(&channel, 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,
+ from: moved_from.id,
to: channel_id.to_string(),
state: voice_state,
}
@@ -136,10 +143,13 @@ pub async fn ingress(
"participant_left" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
+ let server_id = room_metadata.to_internal_error()?.server;
+ let channel = UserVoiceChannel {
+ id: channel_id.clone(),
+ server_id: server_id.clone(),
+ };
- let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
-
- delete_voice_state(channel_id, channel.server(), user_id).await?;
+ delete_voice_state(&channel, user_id).await?;
// Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id)
@@ -203,8 +213,11 @@ pub async fn ingress(
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 server_id = room_metadata.to_internal_error()?.server;
+ let channel = UserVoiceChannel {
+ id: channel_id.clone(),
+ server_id: server_id.clone(),
+ };
let user = Reference::from_unchecked(user_id).as_user(db).await?;
@@ -244,15 +257,14 @@ pub async fn ingress(
log::debug!("Removing user {user_id} from channel {channel_id} {event:?} due to forbidden track.");
let _ = voice_client.remove_user(node, user_id, channel_id).await;
- delete_voice_state(channel_id, channel.server(), user_id).await?;
+ delete_voice_state(&channel, user_id).await?;
return Ok(EmptyResponse);
};
};
let partial = update_voice_state_tracks(
- channel_id,
- channel.server(),
+ &channel,
user_id,
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
track.source,
@@ -267,6 +279,16 @@ pub async fn ingress(
.p(channel_id.clone())
.await;
}
+ "room_finished" => {
+ let channel_id = channel_id.to_internal_error()?;
+ let server_id = room_metadata.to_internal_error()?.server;
+ let channel = UserVoiceChannel {
+ id: channel_id.clone(),
+ server_id: server_id.clone(),
+ };
+
+ delete_channel_voice_state(&channel, &[]).await?;
+ }
_ => {}
};
diff --git a/crates/delta/src/routes/bots/delete.rs b/crates/delta/src/routes/bots/delete.rs
index 73a00156..11132f27 100644
--- a/crates/delta/src/routes/bots/delete.rs
+++ b/crates/delta/src/routes/bots/delete.rs
@@ -1,4 +1,8 @@
-use revolt_database::{Database, User, util::reference::Reference, voice::{VoiceClient, remove_user_from_voice_channels}};
+use revolt_database::{
+ util::reference::Reference,
+ voice::{remove_user_from_voice_channels, VoiceClient},
+ Database, User,
+};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
@@ -21,7 +25,7 @@ pub async fn delete_bot(
bot.delete(db).await?;
- remove_user_from_voice_channels(db, voice_client, &bot.id).await?;
+ remove_user_from_voice_channels(voice_client, &bot.id).await?;
Ok(EmptyResponse)
}
diff --git a/crates/delta/src/routes/channels/channel_delete.rs b/crates/delta/src/routes/channels/channel_delete.rs
index 68ae63cc..38555786 100644
--- a/crates/delta/src/routes/channels/channel_delete.rs
+++ b/crates/delta/src/routes/channels/channel_delete.rs
@@ -1,7 +1,10 @@
use revolt_database::{
- AMQP, Channel, Database, PartialChannel, User, util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{
- VoiceClient, delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel
- }
+ util::{permissions::DatabasePermissionQuery, reference::Reference},
+ voice::{
+ delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel,
+ UserVoiceChannel, VoiceClient,
+ },
+ Channel, Database, PartialChannel, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -54,15 +57,17 @@ pub async fn delete(
)
.await?;
- if is_in_voice_channel(&user.id, channel.id()).await? {
- remove_user_from_voice_channel(db, voice_client, channel.id(), &user.id).await?;
+ let user_voice_channel = UserVoiceChannel::from_channel(&channel);
+
+ if is_in_voice_channel(&user.id, &user_voice_channel).await? {
+ remove_user_from_voice_channel(voice_client, &user_voice_channel, &user.id).await?;
};
}
Channel::TextChannel { .. } => {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db, Some(amqp)).await?;
- delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
+ delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
}
};
diff --git a/crates/delta/src/routes/channels/channel_edit.rs b/crates/delta/src/routes/channels/channel_edit.rs
index 64b3b06a..e60664e0 100644
--- a/crates/delta/src/routes/channels/channel_edit.rs
+++ b/crates/delta/src/routes/channels/channel_edit.rs
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
- voice::{delete_voice_channel, VoiceClient},
+ voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
};
use revolt_models::v0;
@@ -260,7 +260,7 @@ pub async fn edit(
.await?;
if channel.voice().is_none() {
- delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
+ delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?;
}
Ok(Json(channel.into()))
diff --git a/crates/delta/src/routes/channels/group_remove_member.rs b/crates/delta/src/routes/channels/group_remove_member.rs
index db09e4c7..90465b78 100644
--- a/crates/delta/src/routes/channels/group_remove_member.rs
+++ b/crates/delta/src/routes/channels/group_remove_member.rs
@@ -1,7 +1,5 @@
use revolt_database::{
- util::reference::Reference,
- voice::{is_in_voice_channel, remove_user_from_voice_channel, VoiceClient},
- Channel, Database, User, AMQP,
+ AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
@@ -54,8 +52,10 @@ pub async fn remove_member(
return Err(create_error!(InvalidOperation));
};
- if is_in_voice_channel(&member.id, channel.id()).await? {
- remove_user_from_voice_channel(db, voice_client, channel.id(), &member.id).await?;
+ let user_voice_channel = UserVoiceChannel::from_channel(&channel);
+
+ if is_in_voice_channel(member.id, &user_voice_channel).await? {
+ remove_user_from_voice_channel(voice_client, &user_voice_channel, member.id).await?;
};
Ok(EmptyResponse)
diff --git a/crates/delta/src/routes/channels/voice_join.rs b/crates/delta/src/routes/channels/voice_join.rs
index 670c5032..0d8aecdd 100644
--- a/crates/delta/src/routes/channels/voice_join.rs
+++ b/crates/delta/src/routes/channels/voice_join.rs
@@ -3,7 +3,8 @@ use revolt_database::{
util::{permissions::perms, reference::Reference},
voice::{
delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members,
- raise_if_in_voice, set_call_notification_recipients, VoiceClient,
+ raise_if_in_voice, set_call_notification_recipients, set_channel_node, UserVoiceChannel,
+ VoiceClient,
},
Database, User,
};
@@ -50,7 +51,9 @@ pub async fn call(
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())
+ let user_voice_channel = UserVoiceChannel::from_channel(&channel);
+
+ if get_voice_channel_members(&user_voice_channel)
.await?
.zip(voice_info.max_users)
.is_some_and(|(ms, max_users)| ms.len() >= max_users)
@@ -60,6 +63,7 @@ pub async fn call(
}
let existing_node = get_channel_node(channel.id()).await?;
+ let has_existing_node = existing_node.is_some(); // we move existing_node in the next statement so this is the quickest way to know if we need to set it.
let node = existing_node
.or(node)
@@ -78,20 +82,18 @@ pub async fn call(
// 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? {
- if let Some(node) = get_channel_node(&channel_id).await? {
+ for previous_channel in get_user_voice_channels(&user.id).await? {
+ if let Some(node) = get_channel_node(&previous_channel.id).await? {
// if this errors its just a mismatching state - ignore and proceed to still delete our state
- let _ = voice_client.remove_user(&node, &user.id, &channel_id).await;
+ let _ = voice_client
+ .remove_user(&node, &user.id, &previous_channel.id)
+ .await;
};
- let channel = Reference::from_unchecked(&channel_id)
- .as_channel(db)
- .await?;
-
- delete_voice_state(&channel_id, channel.server(), &user.id).await?;
+ delete_voice_state(&previous_channel, &user.id).await?;
}
} else {
- raise_if_in_voice(&user, channel.id()).await?;
+ raise_if_in_voice(&user, &user_voice_channel).await?;
}
let token = voice_client
@@ -100,6 +102,10 @@ pub async fn call(
let room = voice_client.create_room(&node, &channel).await?;
+ if !has_existing_node {
+ set_channel_node(channel.id(), &node).await?;
+ }
+
log::debug!("Created room {}", room.name);
if let Some(recipients) = recipients {
diff --git a/crates/delta/src/routes/channels/voice_stop_ring.rs b/crates/delta/src/routes/channels/voice_stop_ring.rs
index f8db8fc4..768b861a 100644
--- a/crates/delta/src/routes/channels/voice_stop_ring.rs
+++ b/crates/delta/src/routes/channels/voice_stop_ring.rs
@@ -1,6 +1,6 @@
use revolt_database::{
util::reference::Reference,
- voice::{get_voice_state, VoiceClient},
+ voice::{get_voice_state, UserVoiceChannel, VoiceClient},
Channel, Database, User, AMQP,
};
use revolt_result::{create_error, Result, ToRevoltError};
@@ -32,9 +32,15 @@ pub async fn stop_ring(
return Err(create_error!(NoEffect));
}
- if get_voice_state(channel.id(), None, &user.id)
- .await?
- .is_none()
+ if get_voice_state(
+ &UserVoiceChannel {
+ id: channel.id().to_string(),
+ server_id: None,
+ },
+ &user.id,
+ )
+ .await?
+ .is_none()
{
return Err(create_error!(NotConnected));
}
@@ -46,7 +52,7 @@ pub async fn stop_ring(
_ => return Err(create_error!(NoEffect)),
};
- if members.iter().any(|m| &target_user.id == m) {
+ if members.iter().any(|m| target_user.id == m) {
if let Err(e) = amqp
.dm_call_updated(
&user.id,
diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs
index 3ddbfcc7..44b697d4 100644
--- a/crates/delta/src/routes/root.rs
+++ b/crates/delta/src/routes/root.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
use revolt_config::config;
use revolt_result::Result;
use rocket::serde::json::Json;
@@ -54,6 +55,80 @@ pub struct RevoltFeatures {
pub january: Feature,
/// Voice server configuration
pub livekit: VoiceFeature,
+ /// Limits
+ pub limits: LimitsConfig,
+}
+
+/// # Limits For Users
+#[derive(Serialize, JsonSchema, Debug)]
+pub struct LimitsConfig {
+ /// Global Limits
+ pub global: GlobalLimits,
+ /// New User Limits
+ pub new_user: UserLimits,
+ /// Default User Limits
+ pub default: UserLimits,
+}
+
+/// # Global limits
+#[derive(Serialize, JsonSchema, Debug)]
+pub struct GlobalLimits {
+ /// max group size
+ group_size: i64,
+ /// max message embeds
+ message_embeds: i64,
+ /// max replies
+ message_replies: i64,
+ /// max reactions per message
+ message_reactions: i64,
+ /// max server emoji
+ server_emoji: i64,
+ /// max server roles
+ server_roles: i64,
+ /// max server channels
+ server_channels: i64,
+ body_limit_size: i64,
+}
+
+/// # User Limits
+#[derive(Serialize, JsonSchema, Debug)]
+pub struct UserLimits {
+ /// Max Outgoing Friend Requests
+ pub outgoing_friend_requests: i64,
+ /// Max Owned Bots
+ pub bots: i64,
+ /// Max message content length
+ pub message_length: i64,
+ /// max message attachments
+ pub message_attachments: i64,
+ /// max servers
+ pub servers: i64,
+ /// max audio quality
+ pub voice_quality: i64,
+ /// video streaming enabled
+ pub video: bool,
+ /// max video resolution (vertical, horizontal)
+ pub video_resolution: [i64; 2],
+ /// min/max aspect ratios
+ pub video_aspect_ratio: [f64; 2],
+ pub file_upload_size_limits: HashMap,
+}
+
+impl UserLimits {
+ fn from_feature_limits(fl: revolt_config::FeaturesLimits) -> UserLimits {
+ UserLimits {
+ outgoing_friend_requests: fl.outgoing_friend_requests as i64,
+ bots: fl.bots as i64,
+ message_length: fl.message_length as i64,
+ message_attachments: fl.message_attachments as i64,
+ servers: fl.servers as i64,
+ voice_quality: fl.voice_quality as i64,
+ video: fl.video,
+ video_resolution: [fl.video_resolution[0] as i64, fl.video_resolution[1] as i64],
+ video_aspect_ratio: [fl.video_aspect_ratio[0] as f64, fl.video_aspect_ratio[1] as f64],
+ file_upload_size_limits: fl.file_upload_size_limit,
+ }
+ }
}
/// # Build Information
@@ -134,6 +209,20 @@ pub async fn root() -> Result> {
})
.collect(),
},
+ limits: LimitsConfig {
+ global: GlobalLimits {
+ group_size: config.features.limits.global.group_size as i64,
+ message_embeds: config.features.limits.global.message_embeds as i64,
+ message_replies: config.features.limits.global.message_replies as i64,
+ message_reactions: config.features.limits.global.message_reactions as i64,
+ server_emoji: config.features.limits.global.server_emoji as i64,
+ server_roles: config.features.limits.global.server_roles as i64,
+ server_channels: config.features.limits.global.server_channels as i64,
+ body_limit_size: config.features.limits.global.body_limit_size as i64,
+ },
+ new_user: UserLimits::from_feature_limits(config.features.limits.new_user),
+ default: UserLimits::from_feature_limits(config.features.limits.default),
+ }
},
ws: config.hosts.events,
app: config.hosts.app,
diff --git a/crates/delta/src/routes/servers/ban_create.rs b/crates/delta/src/routes/servers/ban_create.rs
index 17589dad..4ff10329 100644
--- a/crates/delta/src/routes/servers/ban_create.rs
+++ b/crates/delta/src/routes/servers/ban_create.rs
@@ -1,6 +1,9 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
- voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient},
+ voice::{
+ get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
+ VoiceClient,
+ },
Database, RemovalIntention, ServerBan, User,
};
use revolt_models::v0;
@@ -58,8 +61,16 @@ pub async fn ban(
.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(&target.id, &server.id).await? {
- remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?;
+ if let Some(channel_id) = get_user_voice_channel_in_server(target.id, &server.id).await? {
+ remove_user_from_voice_channel(
+ voice_client,
+ &UserVoiceChannel {
+ id: channel_id,
+ server_id: Some(server.id.clone()),
+ },
+ target.id,
+ )
+ .await?;
}
}
diff --git a/crates/delta/src/routes/servers/member_edit.rs b/crates/delta/src/routes/servers/member_edit.rs
index ecc1d919..87b45524 100644
--- a/crates/delta/src/routes/servers/member_edit.rs
+++ b/crates/delta/src/routes/servers/member_edit.rs
@@ -9,15 +9,13 @@ use revolt_database::{
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,
+ UserVoiceChannel, VoiceClient,
},
Database, File, PartialMember, User,
};
use revolt_models::v0::{self, FieldsMember};
-use revolt_permissions::{
- calculate_channel_permissions, calculate_server_permissions, ChannelPermission,
-};
+use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission};
use revolt_result::{create_error, Result};
use rocket::{form::validate::Contains, serde::json::Json, State};
use validator::Validate;
@@ -52,7 +50,9 @@ pub async fn edit(
let permissions = calculate_server_permissions(&mut query).await;
// Fetch target permissions
- let mut target_query = DatabasePermissionQuery::new(db, &target_user).server(&server).member(&member);
+ let mut target_query = DatabasePermissionQuery::new(db, &target_user)
+ .server(&server)
+ .member(&member);
let target_permissions = calculate_server_permissions(&mut target_query).await;
// Check permissions in server
@@ -67,8 +67,10 @@ pub async fn edit(
if data.avatar.is_some() || data.remove.contains(&v0::FieldsMember::Avatar) {
if user.id == member.id.user {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ChangeAvatar)?;
+ } else if data.remove.contains(&v0::FieldsMember::Avatar) {
+ permissions.throw_if_lacking_channel_permission(ChannelPermission::RemoveAvatars)?;
} else {
- return Err(create_error!(InvalidOperation));
+ return Err(create_error!(InvalidOperation))
}
}
@@ -83,7 +85,7 @@ pub async fn edit(
}
if target_permissions.has_channel_permission(ChannelPermission::TimeoutMembers) {
- return Err(create_error!(IsElevated))
+ return Err(create_error!(IsElevated));
}
}
@@ -99,7 +101,7 @@ pub async fn edit(
}
if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) {
- return Err(create_error!(InvalidOperation))
+ return Err(create_error!(InvalidOperation));
}
if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) {
@@ -217,8 +219,19 @@ pub async fn edit(
}
};
- 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 new_user_voice_channel = UserVoiceChannel::from_channel(&new_voice_channel);
+ let old_user_voice_channel = UserVoiceChannel {
+ id: channel.clone(),
+ server_id: new_user_voice_channel.server_id.clone(),
+ };
+
+ set_user_moved_from_voice(&channel, &new_user_voice_channel, &target_user.id).await?;
+ set_user_moved_to_voice(
+ new_voice_channel.id(),
+ &old_user_voice_channel,
+ &target_user.id,
+ )
+ .await?;
let mut query = perms(db, &target_user).channel(&new_voice_channel);
let permissions = calculate_channel_permissions(&mut query).await;
diff --git a/crates/delta/src/routes/servers/member_remove.rs b/crates/delta/src/routes/servers/member_remove.rs
index 510da42e..de5b0254 100644
--- a/crates/delta/src/routes/servers/member_remove.rs
+++ b/crates/delta/src/routes/servers/member_remove.rs
@@ -1,6 +1,9 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
- voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient},
+ voice::{
+ get_user_voice_channel_in_server, remove_user_from_voice_channel, UserVoiceChannel,
+ VoiceClient,
+ },
Database, RemovalIntention, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -46,8 +49,16 @@ pub async fn kick(
.remove(db, &server, RemovalIntention::Kick, false)
.await?;
- if let Some(channel_id) = get_user_voice_channel_in_server(&target.id, &server.id).await? {
- remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?;
+ if let Some(channel_id) = get_user_voice_channel_in_server(target.id, &server.id).await? {
+ remove_user_from_voice_channel(
+ voice_client,
+ &UserVoiceChannel {
+ id: channel_id,
+ server_id: Some(server.id.clone()),
+ },
+ target.id,
+ )
+ .await?;
};
Ok(EmptyResponse)
diff --git a/crates/delta/src/routes/servers/server_delete.rs b/crates/delta/src/routes/servers/server_delete.rs
index 96507cac..9e2a3827 100644
--- a/crates/delta/src/routes/servers/server_delete.rs
+++ b/crates/delta/src/routes/servers/server_delete.rs
@@ -1,7 +1,10 @@
use revolt_database::{
- AMQP, Database, RemovalIntention, User, util::reference::Reference, voice::{
- VoiceClient, delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel
- }
+ util::reference::Reference,
+ voice::{
+ delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel,
+ UserVoiceChannel, VoiceClient,
+ },
+ Database, RemovalIntention, User, AMQP,
};
use revolt_models::v0;
use revolt_result::Result;
@@ -27,13 +30,28 @@ pub async fn delete(
if server.owner == user.id {
for channel_id in &server.channels {
- delete_voice_channel(voice_client, channel_id, Some(&server.id)).await?;
+ delete_voice_channel(
+ voice_client,
+ &UserVoiceChannel {
+ id: channel_id.clone(),
+ server_id: Some(server.id.clone()),
+ },
+ )
+ .await?;
}
server.delete(db, Some(amqp)).await
} else {
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
- remove_user_from_voice_channel(db, voice_client, &channel_id, &user.id).await?;
+ remove_user_from_voice_channel(
+ voice_client,
+ &UserVoiceChannel {
+ id: channel_id,
+ server_id: Some(server.id.clone()),
+ },
+ &user.id,
+ )
+ .await?;
};
member
diff --git a/docs/README.md b/docs/README.md
index b28211a9..a31d8713 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
## Installation
```bash
-yarn
+pnpm install
```
## Local Development
```bash
-yarn start
+pnpm start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
## Build
```bash
-yarn build
+pnpm build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
Using SSH:
```bash
-USE_SSH=true yarn deploy
+USE_SSH=true pnpm run deploy
```
Not using SSH:
```bash
-GIT_USER= yarn deploy
+GIT_USER= pnpm run deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
diff --git a/docs/docs/developers/endpoints.md b/docs/docs/developers/endpoints.md
index b22c64e2..a89b4d3e 100644
--- a/docs/docs/developers/endpoints.md
+++ b/docs/docs/developers/endpoints.md
@@ -8,16 +8,12 @@ sidebar_position: 1
You can connect to the API on the following URLs:
-| URL | Release | Description |
-| ----------------------------- | :--------: | --------------------------- |
-| `https://api.revolt.chat` | Production | Primary API endpoint |
-| `https://app.revolt.chat/api` | Production | API endpoint for old client |
-| `https://revolt.chat/api` | Staging | API endpoint for new client |
+| URL | Release | Description |
+| ----------------------------- | :--------: | ---------------------------------- |
+| `https://api.stoat.chat` | Production | Primary API endpoint |
You can connect to the events server on the following URLs:
-| URL | Release | Description |
-| ------------------------------ | :--------: | ------------------------------ |
-| `wss://ws.revolt.chat` | Production | Primary events endpoint |
-| `wss://app.revolt.chat/events` | Production | Events endpoint for old client |
-| `wss://revolt.chat/events` | Staging | Events endpoint for new client |
+| URL | Release | Description |
+| ------------------------------ | :--------: | ------------------------------------- |
+| `wss://events.stoat.chat` | Production | Primary events endpoint |
diff --git a/docs/docs/developers/legacy-plugin-api.md b/docs/docs/developers/legacy-plugin-api.md
index ac5c58fc..d2c452c8 100644
--- a/docs/docs/developers/legacy-plugin-api.md
+++ b/docs/docs/developers/legacy-plugin-api.md
@@ -10,7 +10,7 @@ The Plugin API is very powerful. **Tread carefully.**
**Zero guarantees or sandboxes are provided.** Your code is run as-is.
:::
-This document details the very experimental plugin API available in [Revite](https://github.com/revoltchat/revite).
+This document details the very experimental plugin API available in [Revite](https://github.com/stoatchat/for-legacy-web).
This is more or less a proof of concept but can be used to achieve some simple client modifications.
diff --git a/docs/docs/developing/backend/new_features.md b/docs/docs/developing/backend/new_features.md
index c9517180..029e1325 100644
--- a/docs/docs/developing/backend/new_features.md
+++ b/docs/docs/developing/backend/new_features.md
@@ -4,12 +4,12 @@ New API features must be documented where appropriate, this document aims to cov
Before writing new API features, generally a good idea to:
-- Consult with other developers in the [Revolt Developers space](https://rvlt.gg/API)
+- Consult with other developers in the [Stoat Developers space](https://stt.gg/API)
- If it's a relatively big feature, also [write an RFC](https://github.com/revoltchat/rfcs)
When your feature is ready to release, ensure to:
- Update backend documentation (what you're reading now!) if applicable
-- Update the [developers documentation](https://github.com/revoltchat/wiki) if applicable
+- Update the [developers documentation](https://github.com/stoatchat/developer-wiki) if applicable
- Update the Feature Matrix (or ask someone that can to do so)
- Ensure it is properly listed in the backend release changelog