Merge remote-tracking branch 'origin' into feat/elasticsearch
Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -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.
|
||||||
35
README.md
35
README.md
@@ -2,14 +2,14 @@
|
|||||||
<h1>
|
<h1>
|
||||||
Stoat Backend
|
Stoat Backend
|
||||||
|
|
||||||
[](https://github.com/revoltchat/backend/stargazers)
|
[](https://github.com/stoatchat/stoatchat/stargazers)
|
||||||
[](https://github.com/revoltchat/backend/network/members)
|
[](https://github.com/stoatchat/stoatchat/network/members)
|
||||||
[](https://github.com/revoltchat/backend/pulls)
|
[](https://github.com/stoatchat/stoatchat/pulls)
|
||||||
[](https://github.com/revoltchat/backend/issues)
|
[](https://github.com/stoatchat/stoatchat/issues)
|
||||||
[](https://github.com/revoltchat/backend/graphs/contributors)
|
[](https://github.com/stoatchat/stoatchat/graphs/contributors)
|
||||||
[](https://github.com/revoltchat/backend/blob/main/LICENSE)
|
[](https://github.com/stoatchat/stoatchat/blob/main/LICENSE)
|
||||||
</h1>
|
</h1>
|
||||||
The services and libraries that power the Revolt service.<br/>
|
The services and libraries that power the Stoat service.<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
| Crate | Path | Description | |
|
| Crate | Path | Description | |
|
||||||
@@ -39,7 +39,7 @@ Rust 1.86.0 or higher.
|
|||||||
|
|
||||||
## Development Guide
|
## 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:
|
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:
|
Now you can clone and build the project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/revoltchat/backend revolt-backend
|
git clone https://github.com/stoatchat/stoatchat stoat-backend
|
||||||
cd revolt-backend
|
cd stoat-backend
|
||||||
mise build
|
mise build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -158,16 +158,11 @@ You can start a web client by doing the following:
|
|||||||
corepack enable
|
corepack enable
|
||||||
|
|
||||||
# clone the web client and run it:
|
# clone the web client and run it:
|
||||||
git clone --recursive https://github.com/revoltchat/revite
|
git clone --recursive https://github.com/stoatchat/for-web stoat-web
|
||||||
cd revite
|
cd stoat-web
|
||||||
yarn
|
# refer to stoat-web/README.md for startup, creating an account and loging in
|
||||||
yarn build:deps
|
|
||||||
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
|
|
||||||
yarn dev --port 14701
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
|
||||||
|
|
||||||
## Deployment Guide
|
## Deployment Guide
|
||||||
@@ -198,7 +193,7 @@ Tag and push a new release by running:
|
|||||||
just release
|
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
|
## Testing
|
||||||
|
|
||||||
@@ -217,6 +212,6 @@ TEST_DB=MONGODB cargo nextest run
|
|||||||
|
|
||||||
## License
|
## 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!**
|
**Individual crates may supply their own licenses!**
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ host = "127.0.0.1"
|
|||||||
[hosts]
|
[hosts]
|
||||||
# Web locations of various services
|
# Web locations of various services
|
||||||
# Defaults assume all services are reverse-proxied
|
# 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!
|
# Remember to change these to https/wss where appropriate in production!
|
||||||
app = "http://local.revolt.chat:14701"
|
app = "http://local.revolt.chat:14701"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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,
|
voice::{get_channel_voice_state, UserVoiceChannel},
|
||||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
@@ -167,7 +167,9 @@ impl State {
|
|||||||
| Channel::TextChannel { voice: Some(_), .. }
|
| 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() {
|
if let Some(server) = channel.server() {
|
||||||
let set = voice_state_server_members
|
let set = voice_state_server_members
|
||||||
.entry(server.to_string())
|
.entry(server.to_string())
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ redis = "redis://redis/"
|
|||||||
[hosts]
|
[hosts]
|
||||||
# Web locations of various services
|
# Web locations of various services
|
||||||
# Defaults assume all services are reverse-proxied
|
# 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!
|
# Remember to change these to https/wss where appropriate in production!
|
||||||
app = "http://local.revolt.chat"
|
app = "http://local.revolt.chat"
|
||||||
@@ -34,7 +34,7 @@ password = "rabbitpass"
|
|||||||
|
|
||||||
[api.registration]
|
[api.registration]
|
||||||
# Whether an invite should be required for 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
|
invite_only = false
|
||||||
|
|
||||||
[api.smtp]
|
[api.smtp]
|
||||||
|
|||||||
@@ -463,12 +463,14 @@ pub async fn config() -> Settings {
|
|||||||
let mut config = read().await.try_deserialize::<Settings>().unwrap();
|
let mut config = read().await.try_deserialize::<Settings>().unwrap();
|
||||||
|
|
||||||
// inject REDIS_URI for redis-kiss library
|
// 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());
|
std::env::set_var("REDIS_URI", config.database.redis.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto-detect production nodes
|
// 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;
|
config.production = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -687,8 +687,13 @@ impl Message {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let is_dm_or_group = matches!(
|
||||||
|
channel,
|
||||||
|
Channel::DirectMessage { .. } | Channel::Group { .. }
|
||||||
|
);
|
||||||
|
|
||||||
if !self.has_suppressed_notifications()
|
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
|
// send Push notifications
|
||||||
#[cfg(feature = "tasks")]
|
#[cfg(feature = "tasks")]
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
|||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
|
|
||||||
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 {
|
fn default_true() -> bool {
|
||||||
@@ -45,7 +45,6 @@ auto_derived_partial!(
|
|||||||
/// Whether the member is server-wide voice deafened
|
/// Whether the member is server-wide voice deafened
|
||||||
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
||||||
pub can_receive: bool,
|
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>
|
||||||
@@ -153,7 +152,11 @@ impl Member {
|
|||||||
|
|
||||||
#[cfg(feature = "voice")]
|
#[cfg(feature = "voice")]
|
||||||
for channel in &channels {
|
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)
|
voice_states.push(voice_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +178,7 @@ 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
|
voice_states,
|
||||||
}
|
}
|
||||||
.private(user.id.clone())
|
.private(user.id.clone())
|
||||||
.await;
|
.await;
|
||||||
@@ -225,14 +228,14 @@ impl Member {
|
|||||||
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||||
match field {
|
match field {
|
||||||
FieldsMember::JoinedAt => {},
|
FieldsMember::JoinedAt => {}
|
||||||
FieldsMember::Avatar => self.avatar = None,
|
FieldsMember::Avatar => self.avatar = None,
|
||||||
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::CanReceive => self.can_receive = true,
|
||||||
FieldsMember::CanPublish => self.can_publish = true,
|
FieldsMember::CanPublish => self.can_publish = true,
|
||||||
FieldsMember::VoiceChannel => {},
|
FieldsMember::VoiceChannel => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::{Display, Write};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::client::EventV1,
|
events::client::EventV1,
|
||||||
models::{Channel, User},
|
models::{Channel, User},
|
||||||
@@ -6,7 +8,11 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use iso8601_timestamp::{Duration, Timestamp};
|
use iso8601_timestamp::{Duration, Timestamp};
|
||||||
use livekit_protocol::ParticipantPermission;
|
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_config::FeaturesLimits;
|
||||||
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
|
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
|
||||||
@@ -16,16 +22,18 @@ mod voice_client;
|
|||||||
pub use voice_client::VoiceClient;
|
pub use voice_client::VoiceClient;
|
||||||
|
|
||||||
async fn get_connection() -> Result<Conn> {
|
async fn get_connection() -> Result<Conn> {
|
||||||
_get_connection().await.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?;
|
let mut conn = get_connection().await?;
|
||||||
|
|
||||||
if user.bot.is_some() {
|
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
|
// 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
|
if conn
|
||||||
.sismember(format!("vc:{}", &user.id), channel_id)
|
.sismember(format!("vc:{}", &user.id), channel)
|
||||||
.await
|
.await
|
||||||
.to_internal_error()?
|
.to_internal_error()?
|
||||||
{
|
{
|
||||||
@@ -43,23 +51,31 @@ pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
|
|||||||
Ok(())
|
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()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.set(format!("node:{channel}"), node)
|
.set(format!("node:{channel_id}"), node)
|
||||||
.await
|
.await
|
||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_channel_node(channel: &str) -> Result<Option<String>> {
|
pub async fn get_channel_node(channel_id: &str) -> Result<Option<String>> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.get(format!("node:{channel}"))
|
.get(format!("node:{channel_id}"))
|
||||||
.await
|
.await
|
||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> {
|
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<Vec<UserVoiceChannel>> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.smembers(format!("vc:{user_id}"))
|
.smembers(format!("vc:{user_id}"))
|
||||||
@@ -68,14 +84,14 @@ pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_user_moved_from_voice(
|
pub async fn set_user_moved_from_voice(
|
||||||
old_channel: &str,
|
old_channel_id: &str,
|
||||||
new_channel: &str,
|
new_channel: &UserVoiceChannel,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.set_ex(
|
.set_ex(
|
||||||
format!("moved_from:{user_id}:{old_channel}"),
|
format!("moved_from:{user_id}:{old_channel_id}"),
|
||||||
new_channel,
|
new_channel,
|
||||||
10,
|
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(
|
pub async fn set_user_moved_to_voice(
|
||||||
new_channel: &str,
|
new_channel_id: &str,
|
||||||
old_channel: &str,
|
old_channel: &UserVoiceChannel,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.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
|
.await
|
||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> {
|
pub async fn get_user_moved_to_voice(
|
||||||
|
channel_id: &str,
|
||||||
|
user_id: &str,
|
||||||
|
) -> Result<Option<UserVoiceChannel>> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.get_del(format!("moved_to:{user_id}:{channel_id}"))
|
.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()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result<bool> {
|
pub async fn is_in_voice_channel(user_id: &str, channel: &UserVoiceChannel) -> Result<bool> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.sismember(format!("vc:{user_id}"), channel_id)
|
.sismember(format!("vc:{user_id}"), channel)
|
||||||
.await
|
.await
|
||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
@@ -148,12 +171,15 @@ pub fn get_allowed_sources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_voice_state(
|
pub async fn create_voice_state(
|
||||||
channel_id: &str,
|
channel: &UserVoiceChannel,
|
||||||
server_id: Option<&str>,
|
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
joined_at: Timestamp,
|
joined_at: Timestamp,
|
||||||
) -> Result<UserVoiceState> {
|
) -> Result<UserVoiceState> {
|
||||||
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 {
|
let voice_state = UserVoiceState {
|
||||||
joined_at,
|
joined_at,
|
||||||
@@ -165,9 +191,9 @@ pub async fn create_voice_state(
|
|||||||
};
|
};
|
||||||
|
|
||||||
Pipeline::new()
|
Pipeline::new()
|
||||||
.sadd(format!("vc_members:{channel_id}"), user_id)
|
.sadd(format!("vc_members:{}", &channel.id), user_id)
|
||||||
.sadd(format!("vc:{user_id}"), channel_id)
|
.sadd(format!("vc:{user_id}"), channel)
|
||||||
.set(&unique_key, channel_id)
|
.set(&unique_key, &channel.id)
|
||||||
.set(
|
.set(
|
||||||
format!("joined_at:{unique_key}"),
|
format!("joined_at:{unique_key}"),
|
||||||
joined_at
|
joined_at
|
||||||
@@ -194,16 +220,16 @@ pub async fn create_voice_state(
|
|||||||
Ok(voice_state)
|
Ok(voice_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_voice_state(
|
pub async fn delete_voice_state(channel: &UserVoiceChannel, user_id: &str) -> Result<()> {
|
||||||
channel_id: &str,
|
let unique_key = format!(
|
||||||
server_id: Option<&str>,
|
"{}:{}",
|
||||||
user_id: &str,
|
&user_id,
|
||||||
) -> Result<()> {
|
channel.server_id.as_ref().unwrap_or(&channel.id)
|
||||||
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
|
);
|
||||||
|
|
||||||
Pipeline::new()
|
Pipeline::new()
|
||||||
.srem(format!("vc_members:{channel_id}"), user_id)
|
.srem(format!("vc_members:{}", &channel.id), user_id)
|
||||||
.srem(format!("vc:{user_id}"), channel_id)
|
.srem(format!("vc:{user_id}"), channel)
|
||||||
.del(&[
|
.del(&[
|
||||||
format!("joined_at:{unique_key}"),
|
format!("joined_at:{unique_key}"),
|
||||||
format!("is_publishing:{unique_key}"),
|
format!("is_publishing:{unique_key}"),
|
||||||
@@ -218,19 +244,19 @@ pub async fn delete_voice_state(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_channel_voice_state(
|
pub async fn delete_channel_voice_state(
|
||||||
channel_id: &str,
|
channel: &UserVoiceChannel,
|
||||||
server_id: Option<&str>,
|
|
||||||
user_ids: &[String],
|
user_ids: &[String],
|
||||||
) -> Result<()> {
|
) -> 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();
|
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 {
|
for user_id in user_ids {
|
||||||
let unique_key = format!("{user_id}:{parent_id}");
|
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!("joined_at:{unique_key}"),
|
||||||
format!("is_publishing:{unique_key}"),
|
format!("is_publishing:{unique_key}"),
|
||||||
format!("is_receiving:{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(
|
pub async fn update_voice_state_tracks(
|
||||||
channel_id: &str,
|
channel: &UserVoiceChannel,
|
||||||
server_id: Option<&str>,
|
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
added: bool,
|
added: bool,
|
||||||
track: i32,
|
track: i32,
|
||||||
@@ -273,18 +298,21 @@ pub async fn update_voice_state_tracks(
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
update_voice_state(channel_id, server_id, user_id, &partial).await?;
|
update_voice_state(channel, user_id, &partial).await?;
|
||||||
|
|
||||||
Ok(partial)
|
Ok(partial)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_voice_state(
|
pub async fn update_voice_state(
|
||||||
channel_id: &str,
|
channel: &UserVoiceChannel,
|
||||||
server_id: Option<&str>,
|
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
partial: &PartialUserVoiceState,
|
partial: &PartialUserVoiceState,
|
||||||
) -> Result<()> {
|
) -> 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();
|
let mut pipeline = Pipeline::new();
|
||||||
|
|
||||||
@@ -310,21 +338,24 @@ pub async fn update_voice_state(
|
|||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_voice_channel_members(channel_id: &str) -> Result<Option<Vec<String>>> {
|
pub async fn get_voice_channel_members(channel: &UserVoiceChannel) -> Result<Option<Vec<String>>> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.smembers::<_, Option<Vec<String>>>(format!("vc_members:{channel_id}"))
|
.smembers::<_, Option<Vec<String>>>(format!("vc_members:{}", &channel.id))
|
||||||
.await
|
.await
|
||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
.map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) }))
|
.map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_voice_state(
|
pub async fn get_voice_state(
|
||||||
channel_id: &str,
|
channel: &UserVoiceChannel,
|
||||||
server_id: Option<&str>,
|
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> Result<Option<UserVoiceState>> {
|
) -> Result<Option<UserVoiceState>> {
|
||||||
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()
|
let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection()
|
||||||
.await?
|
.await?
|
||||||
@@ -365,21 +396,21 @@ pub async fn get_voice_state(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::ChannelVoiceState>> {
|
pub async fn get_channel_voice_state(
|
||||||
let members = get_voice_channel_members(channel.id()).await?;
|
channel: &UserVoiceChannel,
|
||||||
|
) -> Result<Option<v0::ChannelVoiceState>> {
|
||||||
let server = channel.server();
|
let members = get_voice_channel_members(channel).await?;
|
||||||
|
|
||||||
if let Some(members) = members {
|
if let Some(members) = members {
|
||||||
let mut participants = Vec::with_capacity(members.len());
|
let mut participants = Vec::with_capacity(members.len());
|
||||||
|
|
||||||
for user_id in members {
|
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);
|
participants.push(voice_state);
|
||||||
} else {
|
} else {
|
||||||
log::info!("Voice state not found but member in voice channel members, removing.");
|
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<Option<v0::Cha
|
|||||||
participants.shrink_to_fit();
|
participants.shrink_to_fit();
|
||||||
|
|
||||||
Ok(Some(v0::ChannelVoiceState {
|
Ok(Some(v0::ChannelVoiceState {
|
||||||
id: channel.id().to_string(),
|
id: channel.id.clone(),
|
||||||
participants,
|
participants,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
@@ -395,12 +426,12 @@ pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::Cha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn move_user(user: &str, from: &str, to: &str) -> Result<()> {
|
pub async fn move_user(user: &str, from_channel_id: &str, to_channel_id: &str) -> Result<()> {
|
||||||
get_connection()
|
get_connection()
|
||||||
.await?
|
.await?
|
||||||
.smove(
|
.smove(
|
||||||
format!("vc-members-{from}"),
|
format!("vc_members:{from_channel_id}"),
|
||||||
format!("vc-members-{to}"),
|
format!("vc_members:{to_channel_id}"),
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -414,11 +445,13 @@ pub async fn sync_voice_permissions(
|
|||||||
server: Option<&Server>,
|
server: Option<&Server>,
|
||||||
role_id: Option<&str>,
|
role_id: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let user_voice_channel = UserVoiceChannel::from_channel(channel);
|
||||||
|
|
||||||
let Some(node) = get_channel_node(channel.id()).await? else {
|
let Some(node) = get_channel_node(channel.id()).await? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
for user_id in get_voice_channel_members(channel.id())
|
for user_id in get_voice_channel_members(&user_voice_channel)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
@@ -458,7 +491,9 @@ pub async fn sync_user_voice_permissions(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|member| member.roles.iter().any(|r| r == role_id))
|
.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(());
|
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.screensharing = voice_state.screensharing.then_some(can_video);
|
||||||
update_event.is_publishing = voice_state.is_publishing.then_some(can_speak);
|
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
|
voice_client
|
||||||
.update_permissions(
|
.update_permissions(
|
||||||
@@ -567,34 +602,95 @@ pub async fn get_call_notification_recipients(
|
|||||||
.to_internal_error()
|
.to_internal_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_user_from_voice_channels(db: &Database, voice_client: &VoiceClient, user_id: &str) -> Result<()> {
|
pub async fn remove_user_from_voice_channels(
|
||||||
for channel_id in get_user_voice_channels(user_id).await? {
|
voice_client: &VoiceClient,
|
||||||
remove_user_from_voice_channel(db, voice_client, &channel_id, user_id).await?;
|
user_id: &str,
|
||||||
};
|
) -> Result<()> {
|
||||||
|
for channel in get_user_voice_channels(user_id).await? {
|
||||||
Ok(())
|
remove_user_from_voice_channel(voice_client, &channel, user_id).await?;
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_user_from_voice_channel(db: &Database, voice_client: &VoiceClient, channel_id: &str, user_id: &str) -> Result<()> {
|
|
||||||
if let Some(node) = get_channel_node(channel_id).await? {
|
|
||||||
let _ = voice_client.remove_user(&node, user_id, channel_id).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_voice_channel(voice_client: &VoiceClient, channel_id: &str, server_id: Option<&str>) -> Result<()> {
|
pub async fn delete_voice_channel(
|
||||||
if let Some(users) = get_voice_channel_members(channel_id).await? {
|
voice_client: &VoiceClient,
|
||||||
let node = get_channel_node(channel_id).await?.unwrap();
|
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, &users).await?;
|
||||||
|
|
||||||
delete_channel_voice_state(channel_id, server_id, &users).await?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct RoomMetadata {
|
||||||
|
pub server: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UserVoiceChannel {
|
||||||
|
pub id: String,
|
||||||
|
pub server_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<W: ?Sized + RedisWrite>(&self, out: &mut W) {
|
||||||
|
out.write_arg_fmt(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRedisValue for UserVoiceChannel {
|
||||||
|
fn from_redis_value(v: &Value) -> Result<Self, RedisError> {
|
||||||
|
String::from_redis_value(v).map(UserVoiceChannel::from_string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::{Channel, User},
|
models::{Channel, User},
|
||||||
|
voice::RoomMetadata,
|
||||||
Database,
|
Database,
|
||||||
};
|
};
|
||||||
use livekit_api::{
|
use livekit_api::{
|
||||||
@@ -102,11 +103,16 @@ impl VoiceClient {
|
|||||||
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> {
|
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> {
|
||||||
let room = self.get_node(node)?;
|
let room = self.get_node(node)?;
|
||||||
|
|
||||||
|
let metadata = RoomMetadata {
|
||||||
|
server: channel.server().map(|id| id.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
room.client
|
room.client
|
||||||
.create_room(
|
.create_room(
|
||||||
channel.id(),
|
channel.id(),
|
||||||
CreateRoomOptions {
|
CreateRoomOptions {
|
||||||
empty_timeout: 5 * 60, // 5 minutes,
|
empty_timeout: 5 * 60, // 5 minutes,
|
||||||
|
metadata: serde_json::to_string(&metadata).to_internal_error()?,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ This email is intended for {{email}}
|
|||||||
|
|
||||||
This email has no association with Stoat or Revolt Platforms Ltd.
|
This email has no association with Stoat or Revolt Platforms Ltd.
|
||||||
Learn more about third party instances here:
|
Learn more about third party instances here:
|
||||||
https://developers.stoat.chat/faq.html
|
https://developers.stoat.chat/faq/
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ This email is intended for {{email}}
|
|||||||
|
|
||||||
This email has no association with Stoat or Revolt Platforms Ltd.
|
This email has no association with Stoat or Revolt Platforms Ltd.
|
||||||
Learn more about third party instances here:
|
Learn more about third party instances here:
|
||||||
https://developers.revolt.chat/faq.html
|
https://developers.stoat.chat/faq/
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
|
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
|
||||||
Further violations may result in a permanent ban depending on
|
Further violations may result in a permanent ban depending on
|
||||||
severity, please abide by the
|
severity, please abide by the
|
||||||
<a href="https://revolt.chat/aup" style="color: #0d6efd;">Acceptable Usage Policy</a>.
|
<a href="https://stoat.chat/legal/community-guidelines" style="color: #0d6efd;">Community Guidelines</a>.
|
||||||
</p>
|
</p>
|
||||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -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.
|
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.
|
Ban evasion is prohibited and will be dealt with accordingly.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ Your account has been suspended, for one or more reasons:
|
|||||||
|
|
||||||
This email is intended for {{email}}
|
This email is intended for {{email}}
|
||||||
|
|
||||||
This email has no association with Revolt or Revolt Platforms Ltd.
|
This email has no association with Stoat or Revolt Platforms Ltd.
|
||||||
Learn more about third party instances here:
|
Learn more about third party instances here:
|
||||||
https://developers.revolt.chat/faq.html
|
https://developers.stoat.chat/faq/
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ This email is intended for {{email}}
|
|||||||
|
|
||||||
This email has no association with Stoat or Revolt Platforms Ltd.
|
This email has no association with Stoat or Revolt Platforms Ltd.
|
||||||
Learn more about third party instances here:
|
Learn more about third party instances here:
|
||||||
https://developers.revolt.chat/faq.html
|
https://developers.stoat.chat/faq/
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
|
|||||||
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
|
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
|
||||||
|
|
||||||
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| {
|
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| {
|
||||||
DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React)
|
DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React + ChannelPermission::Masquerade)
|
||||||
});
|
});
|
||||||
|
|
||||||
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
|
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ FROM debian:12 AS debian
|
|||||||
# Bundle Stage
|
# Bundle Stage
|
||||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
COPY --from=builder /home/rust/src/target/release/revolt-crond ./
|
COPY --from=builder /home/rust/src/target/release/revolt-crond ./
|
||||||
|
COPY --from=debian /usr/bin/uname /usr/bin/uname
|
||||||
|
|
||||||
USER nonroot
|
USER nonroot
|
||||||
CMD ["./revolt-crond"]
|
CMD ["./revolt-crond"]
|
||||||
@@ -128,7 +128,7 @@ impl DmCallConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl FRAcceptedConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ impl FRReceivedConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl GenericConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ impl MassMessageConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ impl MessageConsumer {
|
|||||||
config.pushd.vapid.queue.as_str(),
|
config.pushd.vapid.queue.as_str(),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
|
||||||
sendable
|
sendable
|
||||||
.extras
|
.extras
|
||||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use revolt_database::{
|
|||||||
iso8601_timestamp::{Duration, Timestamp},
|
iso8601_timestamp::{Duration, Timestamp},
|
||||||
util::reference::Reference,
|
util::reference::Reference,
|
||||||
voice::{
|
voice::{
|
||||||
create_voice_state, delete_voice_state,
|
create_voice_state, delete_channel_voice_state, delete_voice_state,
|
||||||
get_user_moved_from_voice, get_user_moved_to_voice,
|
get_user_moved_from_voice, get_user_moved_to_voice, update_voice_state_tracks,
|
||||||
update_voice_state_tracks, VoiceClient,
|
RoomMetadata, UserVoiceChannel, VoiceClient,
|
||||||
},
|
},
|
||||||
Database, AMQP,
|
Database, AMQP,
|
||||||
};
|
};
|
||||||
@@ -51,27 +51,34 @@ pub async fn ingress(
|
|||||||
|
|
||||||
let channel_id = event.room.as_ref().map(|r| &r.name);
|
let channel_id = event.room.as_ref().map(|r| &r.name);
|
||||||
let user_id = event.participant.as_ref().map(|r| &r.identity);
|
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::<RoomMetadata>(&room.metadata).to_internal_error()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
match event.event.as_str() {
|
match event.event.as_str() {
|
||||||
// User joined a channel
|
// User joined a channel
|
||||||
"participant_joined" => {
|
"participant_joined" => {
|
||||||
let channel_id = channel_id.to_internal_error()?;
|
let channel_id = channel_id.to_internal_error()?;
|
||||||
let user_id = user_id.to_internal_error()?;
|
let user_id = user_id.to_internal_error()?;
|
||||||
|
let server_id = room_metadata.to_internal_error()?.server;
|
||||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
let channel = UserVoiceChannel {
|
||||||
|
id: channel_id.clone(),
|
||||||
|
server_id: server_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let joined_at = Timestamp::UNIX_EPOCH
|
let joined_at = Timestamp::UNIX_EPOCH
|
||||||
.checked_add(Duration::seconds(event.created_at))
|
.checked_add(Duration::seconds(event.created_at))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let voice_state =
|
let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
|
||||||
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.
|
// 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? {
|
if let Some(moved_from) = get_user_moved_to_voice(channel_id, user_id).await? {
|
||||||
EventV1::VoiceChannelMove {
|
EventV1::VoiceChannelMove {
|
||||||
user: user_id.to_string(),
|
user: user_id.to_string(),
|
||||||
from: moved_from,
|
from: moved_from.id,
|
||||||
to: channel_id.to_string(),
|
to: channel_id.to_string(),
|
||||||
state: voice_state,
|
state: voice_state,
|
||||||
}
|
}
|
||||||
@@ -136,10 +143,13 @@ pub async fn ingress(
|
|||||||
"participant_left" => {
|
"participant_left" => {
|
||||||
let channel_id = channel_id.to_internal_error()?;
|
let channel_id = channel_id.to_internal_error()?;
|
||||||
let user_id = user_id.to_internal_error()?;
|
let user_id = user_id.to_internal_error()?;
|
||||||
|
let 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, user_id).await?;
|
||||||
|
|
||||||
delete_voice_state(channel_id, channel.server(), user_id).await?;
|
|
||||||
|
|
||||||
// Dont send leave event when a user is moved
|
// Dont send leave event when a user is moved
|
||||||
if get_user_moved_from_voice(channel_id, user_id)
|
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 channel_id = channel_id.to_internal_error()?;
|
||||||
let user_id = user_id.to_internal_error()?;
|
let user_id = user_id.to_internal_error()?;
|
||||||
let track = event.track.as_ref().to_internal_error()?;
|
let track = event.track.as_ref().to_internal_error()?;
|
||||||
|
let server_id = room_metadata.to_internal_error()?.server;
|
||||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
let channel = UserVoiceChannel {
|
||||||
|
id: channel_id.clone(),
|
||||||
|
server_id: server_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let user = Reference::from_unchecked(user_id).as_user(db).await?;
|
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.");
|
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;
|
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);
|
return Ok(EmptyResponse);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let partial = update_voice_state_tracks(
|
let partial = update_voice_state_tracks(
|
||||||
channel_id,
|
&channel,
|
||||||
channel.server(),
|
|
||||||
user_id,
|
user_id,
|
||||||
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
|
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
|
||||||
track.source,
|
track.source,
|
||||||
@@ -267,6 +279,16 @@ pub async fn ingress(
|
|||||||
.p(channel_id.clone())
|
.p(channel_id.clone())
|
||||||
.await;
|
.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?;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 revolt_result::{create_error, Result};
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
use rocket_empty::EmptyResponse;
|
use rocket_empty::EmptyResponse;
|
||||||
@@ -21,7 +25,7 @@ pub async fn delete_bot(
|
|||||||
|
|
||||||
bot.delete(db).await?;
|
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)
|
Ok(EmptyResponse)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
AMQP, Channel, Database, PartialChannel, User, util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||||
VoiceClient, delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel
|
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_models::v0;
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
@@ -54,15 +57,17 @@ pub async fn delete(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if is_in_voice_channel(&user.id, channel.id()).await? {
|
let user_voice_channel = UserVoiceChannel::from_channel(&channel);
|
||||||
remove_user_from_voice_channel(db, voice_client, channel.id(), &user.id).await?;
|
|
||||||
|
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 { .. } => {
|
Channel::TextChannel { .. } => {
|
||||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||||
channel.delete(db, Some(amqp)).await?;
|
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?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||||
voice::{delete_voice_channel, VoiceClient},
|
voice::{delete_voice_channel, UserVoiceChannel, VoiceClient},
|
||||||
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
|
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
@@ -260,7 +260,7 @@ pub async fn edit(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if channel.voice().is_none() {
|
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()))
|
Ok(Json(channel.into()))
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::reference::Reference,
|
AMQP, Channel, Database, User, util::reference::Reference, voice::{UserVoiceChannel, VoiceClient, is_in_voice_channel, remove_user_from_voice_channel}
|
||||||
voice::{is_in_voice_channel, remove_user_from_voice_channel, VoiceClient},
|
|
||||||
Channel, Database, User, AMQP,
|
|
||||||
};
|
};
|
||||||
use revolt_permissions::ChannelPermission;
|
use revolt_permissions::ChannelPermission;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
@@ -54,8 +52,10 @@ pub async fn remove_member(
|
|||||||
return Err(create_error!(InvalidOperation));
|
return Err(create_error!(InvalidOperation));
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_in_voice_channel(&member.id, channel.id()).await? {
|
let user_voice_channel = UserVoiceChannel::from_channel(&channel);
|
||||||
remove_user_from_voice_channel(db, voice_client, channel.id(), &member.id).await?;
|
|
||||||
|
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)
|
Ok(EmptyResponse)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use revolt_database::{
|
|||||||
util::{permissions::perms, reference::Reference},
|
util::{permissions::perms, reference::Reference},
|
||||||
voice::{
|
voice::{
|
||||||
delete_voice_state, get_channel_node, get_user_voice_channels, get_voice_channel_members,
|
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,
|
Database, User,
|
||||||
};
|
};
|
||||||
@@ -50,7 +51,9 @@ pub async fn call(
|
|||||||
let current_permissions = calculate_channel_permissions(&mut permissions).await;
|
let current_permissions = calculate_channel_permissions(&mut permissions).await;
|
||||||
current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
|
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?
|
.await?
|
||||||
.zip(voice_info.max_users)
|
.zip(voice_info.max_users)
|
||||||
.is_some_and(|(ms, max_users)| ms.len() >= 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 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
|
let node = existing_node
|
||||||
.or(node)
|
.or(node)
|
||||||
@@ -78,20 +82,18 @@ pub async fn call(
|
|||||||
// Finds and disconnects any existing voice connections by the user,
|
// Finds and disconnects any existing voice connections by the user,
|
||||||
// should only ever loop once but just to cover our backs.
|
// should only ever loop once but just to cover our backs.
|
||||||
|
|
||||||
for channel_id in get_user_voice_channels(&user.id).await? {
|
for previous_channel in get_user_voice_channels(&user.id).await? {
|
||||||
if let Some(node) = get_channel_node(&channel_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
|
// 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)
|
delete_voice_state(&previous_channel, &user.id).await?;
|
||||||
.as_channel(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
delete_voice_state(&channel_id, channel.server(), &user.id).await?;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
raise_if_in_voice(&user, channel.id()).await?;
|
raise_if_in_voice(&user, &user_voice_channel).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = voice_client
|
let token = voice_client
|
||||||
@@ -100,6 +102,10 @@ pub async fn call(
|
|||||||
|
|
||||||
let room = voice_client.create_room(&node, &channel).await?;
|
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);
|
log::debug!("Created room {}", room.name);
|
||||||
|
|
||||||
if let Some(recipients) = recipients {
|
if let Some(recipients) = recipients {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::reference::Reference,
|
util::reference::Reference,
|
||||||
voice::{get_voice_state, VoiceClient},
|
voice::{get_voice_state, UserVoiceChannel, VoiceClient},
|
||||||
Channel, Database, User, AMQP,
|
Channel, Database, User, AMQP,
|
||||||
};
|
};
|
||||||
use revolt_result::{create_error, Result, ToRevoltError};
|
use revolt_result::{create_error, Result, ToRevoltError};
|
||||||
@@ -32,9 +32,15 @@ pub async fn stop_ring(
|
|||||||
return Err(create_error!(NoEffect));
|
return Err(create_error!(NoEffect));
|
||||||
}
|
}
|
||||||
|
|
||||||
if get_voice_state(channel.id(), None, &user.id)
|
if get_voice_state(
|
||||||
.await?
|
&UserVoiceChannel {
|
||||||
.is_none()
|
id: channel.id().to_string(),
|
||||||
|
server_id: None,
|
||||||
|
},
|
||||||
|
&user.id,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
{
|
{
|
||||||
return Err(create_error!(NotConnected));
|
return Err(create_error!(NotConnected));
|
||||||
}
|
}
|
||||||
@@ -46,7 +52,7 @@ pub async fn stop_ring(
|
|||||||
_ => return Err(create_error!(NoEffect)),
|
_ => 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
|
if let Err(e) = amqp
|
||||||
.dm_call_updated(
|
.dm_call_updated(
|
||||||
&user.id,
|
&user.id,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use revolt_config::config;
|
use revolt_config::config;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
@@ -54,6 +55,80 @@ pub struct RevoltFeatures {
|
|||||||
pub january: Feature,
|
pub january: Feature,
|
||||||
/// Voice server configuration
|
/// Voice server configuration
|
||||||
pub livekit: VoiceFeature,
|
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<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
/// # Build Information
|
||||||
@@ -134,6 +209,20 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.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,
|
ws: config.hosts.events,
|
||||||
app: config.hosts.app,
|
app: config.hosts.app,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
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,
|
Database, RemovalIntention, ServerBan, User,
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
@@ -58,8 +61,16 @@ pub async fn ban(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// If the member is in a voice channel while banned kick them from the voice channel
|
// 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? {
|
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?;
|
remove_user_from_voice_channel(
|
||||||
|
voice_client,
|
||||||
|
&UserVoiceChannel {
|
||||||
|
id: channel_id,
|
||||||
|
server_id: Some(server.id.clone()),
|
||||||
|
},
|
||||||
|
target.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ use revolt_database::{
|
|||||||
voice::{
|
voice::{
|
||||||
get_channel_node, get_user_voice_channel_in_server, set_channel_node,
|
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,
|
set_user_moved_from_voice, set_user_moved_to_voice, sync_user_voice_permissions,
|
||||||
VoiceClient,
|
UserVoiceChannel, VoiceClient,
|
||||||
},
|
},
|
||||||
Database, File, PartialMember, User,
|
Database, File, PartialMember, User,
|
||||||
};
|
};
|
||||||
use revolt_models::v0::{self, FieldsMember};
|
use revolt_models::v0::{self, FieldsMember};
|
||||||
|
|
||||||
use revolt_permissions::{
|
use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission, UserPermission};
|
||||||
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::{form::validate::Contains, serde::json::Json, State};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
@@ -52,7 +50,9 @@ pub async fn edit(
|
|||||||
let permissions = calculate_server_permissions(&mut query).await;
|
let permissions = calculate_server_permissions(&mut query).await;
|
||||||
|
|
||||||
// Fetch target permissions
|
// 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;
|
let target_permissions = calculate_server_permissions(&mut target_query).await;
|
||||||
|
|
||||||
// Check permissions in server
|
// Check permissions in server
|
||||||
@@ -67,8 +67,10 @@ pub async fn edit(
|
|||||||
if data.avatar.is_some() || data.remove.contains(&v0::FieldsMember::Avatar) {
|
if data.avatar.is_some() || data.remove.contains(&v0::FieldsMember::Avatar) {
|
||||||
if user.id == member.id.user {
|
if user.id == member.id.user {
|
||||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ChangeAvatar)?;
|
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 {
|
} 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) {
|
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) {
|
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) {
|
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?;
|
let new_user_voice_channel = UserVoiceChannel::from_channel(&new_voice_channel);
|
||||||
set_user_moved_to_voice(new_voice_channel.id(), &channel, &target_user.id).await?;
|
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 mut query = perms(db, &target_user).channel(&new_voice_channel);
|
||||||
let permissions = calculate_channel_permissions(&mut query).await;
|
let permissions = calculate_channel_permissions(&mut query).await;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
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,
|
Database, RemovalIntention, User,
|
||||||
};
|
};
|
||||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||||
@@ -46,8 +49,16 @@ pub async fn kick(
|
|||||||
.remove(db, &server, RemovalIntention::Kick, false)
|
.remove(db, &server, RemovalIntention::Kick, false)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(channel_id) = get_user_voice_channel_in_server(&target.id, &server.id).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?;
|
remove_user_from_voice_channel(
|
||||||
|
voice_client,
|
||||||
|
&UserVoiceChannel {
|
||||||
|
id: channel_id,
|
||||||
|
server_id: Some(server.id.clone()),
|
||||||
|
},
|
||||||
|
target.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(EmptyResponse)
|
Ok(EmptyResponse)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
AMQP, Database, RemovalIntention, User, util::reference::Reference, voice::{
|
util::reference::Reference,
|
||||||
VoiceClient, delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel
|
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_models::v0;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
@@ -27,13 +30,28 @@ pub async fn delete(
|
|||||||
|
|
||||||
if server.owner == user.id {
|
if server.owner == user.id {
|
||||||
for channel_id in &server.channels {
|
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
|
server.delete(db, Some(amqp)).await
|
||||||
} else {
|
} else {
|
||||||
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
|
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
|
member
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
```bash
|
```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.
|
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
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
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:
|
Using SSH:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
USE_SSH=true yarn deploy
|
USE_SSH=true pnpm run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
Not using SSH:
|
Not using SSH:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
GIT_USER=<Your GitHub username> yarn deploy
|
GIT_USER=<Your GitHub username> 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.
|
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.
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ sidebar_position: 1
|
|||||||
|
|
||||||
You can connect to the API on the following URLs:
|
You can connect to the API on the following URLs:
|
||||||
|
|
||||||
| URL | Release | Description |
|
| URL | Release | Description |
|
||||||
| ----------------------------- | :--------: | --------------------------- |
|
| ----------------------------- | :--------: | ---------------------------------- |
|
||||||
| `https://api.revolt.chat` | Production | Primary API endpoint |
|
| `https://api.stoat.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 |
|
|
||||||
|
|
||||||
You can connect to the events server on the following URLs:
|
You can connect to the events server on the following URLs:
|
||||||
|
|
||||||
| URL | Release | Description |
|
| URL | Release | Description |
|
||||||
| ------------------------------ | :--------: | ------------------------------ |
|
| ------------------------------ | :--------: | ------------------------------------- |
|
||||||
| `wss://ws.revolt.chat` | Production | Primary events endpoint |
|
| `wss://events.stoat.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 |
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ The Plugin API is very powerful. **Tread carefully.**
|
|||||||
**Zero guarantees or sandboxes are provided.** Your code is run as-is.
|
**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.
|
This is more or less a proof of concept but can be used to achieve some simple client modifications.
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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)
|
- 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:
|
When your feature is ready to release, ensure to:
|
||||||
|
|
||||||
- Update backend documentation (what you're reading now!) if applicable
|
- 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)
|
- Update the Feature Matrix (or ask someone that can to do so)
|
||||||
- Ensure it is properly listed in the backend release changelog
|
- Ensure it is properly listed in the backend release changelog
|
||||||
|
|||||||
Reference in New Issue
Block a user