Compare commits

..
Author SHA1 Message Date
stoat-release[bot] ae41fd6e6a chore(deps): update rust crate anyhow to v1.0.103 2026-07-07 11:32:26 +00:00
Zomatree 0b53db9921 fix: voice system messages and call notifs by fetching participant list (#846)
Signed-off-by: Zomatree <me@zomatree.live>
2026-07-06 22:37:27 -07:00
Zomatree 21daf3aec6 fix: allow removing channel slowmode (#836)
Signed-off-by: Zomatree <me@zomatree.live>
2026-07-02 19:23:52 -07:00
Tom 59f6e012f8 feat: replace tenor with gifbox (#844)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-07-02 19:20:18 -07:00
Zomatree a22378c35c fix(docs): update react version (#842)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-27 04:24:00 +01:00
Zomatree 164be6a2da Revert "chore(deps): update dependency gh to v2.95.0" (#840)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-25 18:57:12 +01:00
11 changed files with 196 additions and 216 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
node = "25.4.0"
pnpm = "10.28.1"
gh = "2.95.0"
gh = "2.25.0"
rust = "1.92.0"
"cargo:cargo-nextest" = "0.9.122"
Generated
+23 -80
View File
@@ -185,9 +185,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arbitrary"
@@ -995,7 +995,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
"http 1.4.2",
@@ -1004,7 +1005,7 @@ dependencies = [
"hyper 1.10.1",
"hyper-util",
"itoa",
"matchit 0.7.3",
"matchit",
"memchr",
"mime",
"multer",
@@ -1023,41 +1024,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core 0.5.6",
"axum-macros",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper 1.10.1",
"hyper-util",
"itoa",
"matchit 0.8.4",
"memchr",
"mime",
"multer",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.4.5"
@@ -1079,52 +1045,35 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.12.6"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
dependencies = [
"axum 0.8.9",
"axum-core 0.5.6",
"axum",
"axum-core",
"bytes",
"futures-core",
"fastrand 2.4.1",
"futures-util",
"headers",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"mime",
"multer",
"pin-project-lite",
"serde",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.5.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
dependencies = [
"proc-macro2",
"quote 1.0.46",
@@ -1138,7 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05679dcdfeccf4cc6a8f45b80318fff8a9908337da1cfeb6acca00562a345b4d"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"axum_typed_multipart_macros",
"bytes",
"chrono",
@@ -5010,12 +4959,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
@@ -7223,7 +7166,7 @@ dependencies = [
name = "revolt-autumn"
version = "0.13.7"
dependencies = [
"axum 0.8.9",
"axum",
"axum-macros",
"axum_typed_multipart",
"ffprobe",
@@ -7344,7 +7287,7 @@ dependencies = [
"async-lock 2.8.0",
"async-recursion",
"async-trait",
"axum 0.8.9",
"axum",
"base32",
"base64 0.21.7",
"bson",
@@ -7474,7 +7417,7 @@ dependencies = [
name = "revolt-gifbox"
version = "0.13.7"
dependencies = [
"axum 0.8.9",
"axum",
"axum-extra",
"lru_time_cache",
"reqwest 0.13.4",
@@ -7498,7 +7441,7 @@ name = "revolt-january"
version = "0.13.7"
dependencies = [
"async-recursion",
"axum 0.8.9",
"axum",
"axum-extra",
"encoding_rs",
"lazy_static",
@@ -7612,7 +7555,7 @@ name = "revolt-ratelimits"
version = "0.13.7"
dependencies = [
"async-trait",
"axum 0.8.9",
"axum",
"dashmap",
"log",
"revolt-config",
@@ -7627,7 +7570,7 @@ dependencies = [
name = "revolt-result"
version = "0.13.7"
dependencies = [
"axum 0.8.9",
"axum",
"log",
"revolt_okapi",
"revolt_rocket_okapi",
@@ -10129,7 +10072,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3ab4b7269d14d93626b0bfedf212f1b0995cb7d13d35daba21d579511e7fae8"
dependencies = [
"axum 0.7.9",
"axum",
"serde",
"serde_json",
"utoipa",
+3 -3
View File
@@ -108,10 +108,10 @@ aws-config = "1.5.5"
aws-sdk-s3 = "1.46.0"
# Axum (HTTP server)
axum-macros = "0.5.0"
axum-macros = "0.4.1"
axum_typed_multipart = "0.12.1"
axum = "0.8.0"
axum-extra = "0.12"
axum = "0.7.5"
axum-extra = "0.9"
tower-http = "0.5.2"
# Rocket (HTTP server)
@@ -161,6 +161,7 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
);
@@ -554,6 +555,12 @@ impl Channel {
}
_ => {}
},
FieldsChannel::Slowmode => match self {
Self::TextChannel { slowmode, .. } => {
slowmode.take();
}
_ => {}
}
}
}
@@ -777,6 +784,7 @@ impl IntoDocumentPath for FieldsChannel {
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Voice => "voice",
FieldsChannel::Slowmode => "slowmode",
})
}
}
@@ -319,6 +319,7 @@ impl From<FieldsChannel> for crate::FieldsChannel {
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
FieldsChannel::Voice => crate::FieldsChannel::Voice,
FieldsChannel::Slowmode => crate::FieldsChannel::Slowmode,
}
}
}
@@ -330,6 +331,7 @@ impl From<crate::FieldsChannel> for FieldsChannel {
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
crate::FieldsChannel::Voice => FieldsChannel::Voice,
crate::FieldsChannel::Slowmode => FieldsChannel::Slowmode,
}
}
}
@@ -159,4 +159,17 @@ impl VoiceClient {
.await
.to_internal_error()
}
pub async fn get_room_participants(
&self,
node: &str,
channel_id: &str,
) -> Result<Vec<ParticipantInfo>> {
let room = self.get_node(node)?;
room.client
.list_participants(channel_id)
.await
.to_internal_error()
}
}
+1
View File
@@ -164,6 +164,7 @@ auto_derived!(
Icon,
DefaultPermissions,
Voice,
Slowmode,
}
/// New webhook information
+91 -92
View File
@@ -1,19 +1,15 @@
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType;
use revolt_database::{
events::client::EventV1,
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
create_voice_state, delete_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,
AMQP, Database, PartialMessage, SystemMessage, events::client::EventV1, iso8601_timestamp::{Duration, Timestamp}, util::reference::Reference, voice::{
RoomMetadata, UserVoiceChannel, VoiceClient, create_voice_state, delete_channel_voice_state, delete_voice_state, get_call_notification_recipients, get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members, set_channel_call_started_system_message, take_channel_call_started_system_message, update_voice_state_tracks
}
};
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError};
use rocket::{post, State};
use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader;
@@ -21,12 +17,12 @@ use crate::guard::AuthHeader;
pub async fn ingress(
db: &State<Database>,
voice_client: &State<VoiceClient>,
_amqp: &State<AMQP>,
amqp: &State<AMQP>,
node: &str,
auth_header: AuthHeader<'_>,
body: &str,
) -> Result<EmptyResponse> {
log::debug!("received event: {body:?}");
log::debug!("received event: {body}");
let config = revolt_config::config().await;
@@ -63,16 +59,18 @@ pub async fn ingress(
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 {
let voice_channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
let joined_at = Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap();
let voice_state = create_voice_state(&channel, user_id, joined_at).await?;
let voice_state = create_voice_state(&voice_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? {
@@ -93,63 +91,66 @@ pub async fn ingress(
.await;
};
// TODO: fix `num_participants` being incorrect sometimes see (#457)
// First user who joined - send call started system message.
// if event.room.as_ref().unwrap().num_participants == 1 {
// let user = Reference::from_unchecked(user_id).as_user(db).await?;
let participants = voice_client.get_room_participants(node, channel_id).await?;
// let message_id =
// Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
// .to_string();
if participants.len() == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?;
let message_id = Ulid::from_datetime(
Timestamp::UNIX_EPOCH
.checked_add(Duration::seconds(event.created_at))
.unwrap()
.into(),
)
.to_string();
// let mut call_started_message = SystemMessage::CallStarted {
// by: user_id.to_string(),
// finished_at: None,
// }
// .into_message(channel.id().to_string());
let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(),
finished_at: None,
}
.into_message(channel_id.clone());
// call_started_message.id = message_id;
call_started_message.id = message_id;
// set_channel_call_started_system_message(channel.id(), &call_started_message.id)
// .await?;
set_channel_call_started_system_message(channel_id, &call_started_message.id)
.await?;
// call_started_message
// .send(
// db,
// Some(amqp),
// v0::MessageAuthor::System {
// username: &user.username,
// avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
// },
// None,
// None,
// &channel,
// false,
// )
// .await?;
call_started_message
.send(
db,
Some(amqp),
v0::MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false,
)
.await?;
// let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
// let now = joined_at.format_short().to_string();
let recipients = get_call_notification_recipients(channel_id, user_id).await?;
let now = joined_at.format_short().to_string();
// if let Err(e) = amqp
// .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
// .await
// {
// revolt_config::capture_error(&e);
// }
// }
if let Err(e) = amqp
.dm_call_updated(&user.id, channel_id, Some(&now), false, recipients)
.await
{
revolt_config::capture_error(&e);
}
}
}
// User left a channel
"participant_left" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let server_id = room_metadata.to_internal_error()?.server;
let channel = UserVoiceChannel {
let voice_channel = UserVoiceChannel {
id: channel_id.clone(),
server_id: server_id.clone(),
};
delete_voice_state(&channel, user_id).await?;
delete_voice_state(&voice_channel, user_id).await?;
// Dont send leave event when a user is moved
if get_user_moved_from_voice(channel_id, user_id)
@@ -164,49 +165,47 @@ pub async fn ingress(
.await;
};
// See above for why this is commented out
// // Update CallStarted system message if everyone has left with the end time
// let members = get_voice_channel_members(channel_id).await?;
let members = get_voice_channel_members(&voice_channel).await?;
// if members.is_none_or(|m| m.is_empty()) {
// // The channel is empty so send out an "end" message for ringing
// if let Err(e) = amqp
// .dm_call_updated(user_id, channel_id, None, true, None)
// .await
// {
// revolt_config::capture_internal_error!(&e);
// }
if members.is_none_or(|m| m.is_empty()) {
// The channel is empty so send out an "end" message for ringing
if let Err(e) = amqp
.dm_call_updated(user_id, channel_id, None, true, None)
.await
{
revolt_config::capture_internal_error!(&e);
}
// if let Some(system_message_id) =
// take_channel_call_started_system_message(channel_id).await?
// {
// // Could have been deleted
// if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
// .as_message(db)
// .await
// {
// if let Some(SystemMessage::CallStarted { finished_at, .. }) =
// &mut message.system
// {
// *finished_at = Some(Timestamp::now_utc());
if let Some(system_message_id) =
take_channel_call_started_system_message(channel_id).await?
{
// Could have been deleted
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
.as_message(db)
.await
{
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*finished_at = Some(Timestamp::now_utc());
// message
// .update(
// db,
// PartialMessage {
// system: message.system.clone(),
// ..Default::default()
// },
// Vec::new(),
// )
// .await?;
// } else {
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
// }
// };
// };
// }
message
.update(
db,
PartialMessage {
system: message.system.clone(),
..Default::default()
},
Vec::new(),
)
.await?;
} else {
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
}
};
};
}
}
// Audio/video track was started/stopped/unmuted/muted
"track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
@@ -221,6 +221,9 @@ pub async fn edit(
v0::FieldsChannel::Voice => {
voice.take();
}
v0::FieldsChannel::Slowmode => {
slowmode.take();
}
_ => {}
}
}
+49 -38
View File
@@ -10,7 +10,7 @@ use tokio::sync::RwLock;
pub mod types;
const TENOR_API_BASE_URL: &str = "https://tenor.googleapis.com/v2";
const TENOR_API_BASE_URL: &str = "https://compat.gifbox.me/v2";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TenorError {
@@ -59,7 +59,11 @@ impl Tenor {
}
}
pub async fn request<T: DeserializeOwned>(&self, path: &str, query: &[Option<(&str, &str)>]) -> Result<Arc<T>, TenorError> {
pub async fn request<T: DeserializeOwned>(
&self,
path: &str,
query: &[Option<(&str, &str)>],
) -> Result<Arc<T>, TenorError> {
let response = self
.client
.get(format!("{TENOR_API_BASE_URL}{path}"))
@@ -95,24 +99,27 @@ impl Tenor {
}
}
let res = self.coalescion.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/search",
&[
Some(("key", &self.key)),
Some(("q", query)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
is_category.then_some(("component", "categories"))
]
).await
})
.await
.unwrap();
let res = self
.coalescion
.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/search",
&[
Some(("key", &self.key)),
Some(("q", query)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
is_category.then_some(("component", "categories")),
],
)
.await
})
.await
.unwrap();
if let Ok(resp) = &*res {
self.cache.write().await.insert(unique_key, resp.clone());
@@ -143,8 +150,9 @@ impl Tenor {
Some(("client_key", "Gifbox")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
]
).await
],
)
.await
})
.await
.unwrap();
@@ -173,22 +181,25 @@ impl Tenor {
}
}
let res = self.coalescion.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/featured",
&[
Some(("key", &self.key)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
]
).await
})
.await
.unwrap();
let res = self
.coalescion
.execute(unique_key.clone(), || async move {
self.request::<types::PaginatedMediaResponse>(
"/featured",
&[
Some(("key", &self.key)),
Some(("client_key", "Gifbox")),
Some(("media_filter", "webm,tinywebm")),
Some(("locale", locale)),
Some(("contentfilter", "high")),
Some(("limit", &limit.to_string())),
position.is_empty().then_some(("pos", position)),
],
)
.await
})
.await
.unwrap();
if let Ok(resp) = &*res {
self.featured.write().await.insert(unique_key, resp.clone());
+2 -2
View File
@@ -22,8 +22,8 @@
"@scalar/docusaurus": "^0.7.21",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2",