Merge remote-tracking branch 'origin/main' into feat/elasticsearch

Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
Zomatree
2026-08-08 03:53:41 +01:00
40 changed files with 210 additions and 88 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.14.3"
".": "0.15.0"
}

View File

@@ -1,5 +1,18 @@
# Changelog
## [0.15.0](https://github.com/stoatchat/stoatchat/compare/v0.14.3...v0.15.0) (2026-08-05)
### Features
* add an approximate member count to servers ([#884](https://github.com/stoatchat/stoatchat/issues/884)) ([ccfb5f1](https://github.com/stoatchat/stoatchat/commit/ccfb5f1a62f1be9177ac9c1c09fd18ea8e9c7f18))
* add call event ([#873](https://github.com/stoatchat/stoatchat/issues/873)) ([457c770](https://github.com/stoatchat/stoatchat/commit/457c7709c75060cd8519cc45df3badcfc1b629ea))
### Bug Fixes
* rewrite youtube embedder to use youtube oembed ([#878](https://github.com/stoatchat/stoatchat/issues/878)) ([0369451](https://github.com/stoatchat/stoatchat/commit/03694512b90be90367299c3ebfb072ebbc8a681d))
## [0.14.3](https://github.com/stoatchat/stoatchat/compare/v0.14.2...v0.14.3) (2026-07-22)

36
Cargo.lock generated
View File

@@ -7257,7 +7257,7 @@ dependencies = [
[[package]]
name = "revolt-autumn"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"axum",
"axum-macros",
@@ -7298,7 +7298,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"async-channel",
"async-tungstenite",
@@ -7329,7 +7329,7 @@ dependencies = [
[[package]]
name = "revolt-coalesced"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"indexmap 2.14.0",
"lru",
@@ -7338,7 +7338,7 @@ dependencies = [
[[package]]
name = "revolt-config"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"cached",
"config",
@@ -7354,7 +7354,7 @@ dependencies = [
[[package]]
name = "revolt-crond"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"futures",
"futures-lite",
@@ -7375,7 +7375,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"anyhow",
"async-lock 2.8.0",
@@ -7434,7 +7434,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"async-channel",
"bitfield",
@@ -7482,7 +7482,7 @@ dependencies = [
[[package]]
name = "revolt-files"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -7510,7 +7510,7 @@ dependencies = [
[[package]]
name = "revolt-gifbox"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"axum",
"axum-extra",
@@ -7533,7 +7533,7 @@ dependencies = [
[[package]]
name = "revolt-january"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"async-recursion",
"axum",
@@ -7563,7 +7563,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"indexmap 2.14.0",
"iso8601-timestamp",
@@ -7583,14 +7583,14 @@ dependencies = [
[[package]]
name = "revolt-parser"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"logos",
]
[[package]]
name = "revolt-permissions"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"async-trait",
"auto_ops",
@@ -7605,7 +7605,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"log",
"once_cell",
@@ -7617,7 +7617,7 @@ dependencies = [
[[package]]
name = "revolt-pushd"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"anyhow",
"async-trait",
@@ -7647,7 +7647,7 @@ dependencies = [
[[package]]
name = "revolt-ratelimits"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"async-trait",
"axum",
@@ -7663,7 +7663,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"axum",
"log",
@@ -7711,7 +7711,7 @@ dependencies = [
[[package]]
name = "revolt-voice-ingress"
version = "0.14.3"
version = "0.15.0"
dependencies = [
"chrono",
"futures",

View File

@@ -197,14 +197,14 @@ futures-lite = "2.6.1"
vergen = "7.5.0"
# Local packages
revolt-coalesced = { version = "0.14.3", path = "crates/core/coalesced" }
revolt-config = { version = "0.14.3", path = "crates/core/config" }
revolt-database = { version = "0.14.3", path = "crates/core/database" }
revolt-files = { version = "0.14.3", path = "crates/core/files" }
revolt-models = { version = "0.14.3", path = "crates/core/models" }
revolt-parser = { version = "0.14.3", path = "crates/core/parser" }
revolt-permissions = { version = "0.14.3", path = "crates/core/permissions" }
revolt-presence = { version = "0.14.3", path = "crates/core/presence" }
revolt-ratelimits = { version = "0.14.3", path = "crates/core/ratelimits" }
revolt-result = { version = "0.14.3", path = "crates/core/result" }
revolt-search = { version = "0.13.7", path = "crates/core/search" }
revolt-coalesced = { version = "0.15.0", path = "crates/core/coalesced" }
revolt-config = { version = "0.15.0", path = "crates/core/config" }
revolt-database = { version = "0.15.0", path = "crates/core/database" }
revolt-files = { version = "0.15.0", path = "crates/core/files" }
revolt-models = { version = "0.15.0", path = "crates/core/models" }
revolt-parser = { version = "0.15.0", path = "crates/core/parser" }
revolt-permissions = { version = "0.15.0", path = "crates/core/permissions" }
revolt-presence = { version = "0.15.0", path = "crates/core/presence" }
revolt-ratelimits = { version = "0.15.0", path = "crates/core/ratelimits" }
revolt-result = { version = "0.15.0", path = "crates/core/result" }
revolt-search = { version = "0.15.0", path = "crates/core/search" }

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.14.3"
version = "0.15.0"
license = "AGPL-3.0-or-later"
edition = "2021"
publish = false

View File

@@ -307,7 +307,7 @@ impl State {
Ok(EventV1::Ready {
users: if fields.users { Some(users) } else { None },
servers: if fields.servers {
Some(servers.into_iter().map(Into::into).collect())
Some(join_all(servers.into_iter().map(|server| server.into(db))).await)
} else {
None
},

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-coalesced"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -70,6 +70,8 @@ trust_cloudflare = false
easypwned = ""
# Tenor API Key
tenor_key = ""
# Admin keys for future admin api and for bonfire reported image fetching
admin_keys = []
[api.security.captcha]
# hCaptcha configuration

View File

@@ -228,6 +228,7 @@ pub struct ApiSecurity {
pub trust_cloudflare: bool,
pub easypwned: String,
pub tenor_key: String,
pub admin_keys: Vec<String>,
}
#[derive(Deserialize, Debug, Clone)]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -176,7 +176,7 @@ impl Member {
EventV1::ServerCreate {
id: server.id.clone(),
server: server.clone().into(),
server: server.clone().into(db).await,
channels: channels
.clone()
.into_iter()
@@ -317,9 +317,15 @@ impl Member {
})
{
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user.clone() },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user.clone() },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user.clone() },
RemovalIntention::Leave => SystemMessage::UserLeft {
id: self.id.user.clone(),
},
RemovalIntention::Kick => SystemMessage::UserKicked {
id: self.id.user.clone(),
},
RemovalIntention::Ban => SystemMessage::UserBanned {
id: self.id.user.clone(),
},
}
.into_message(id.to_string())
// TODO: support notifications here in the future?

View File

@@ -1,6 +1,11 @@
use std::collections::{HashMap, HashSet};
use revolt_config::capture_error;
use redis_kiss::{
get_connection,
redis::{SetExpiry, SetOptions},
AsyncCommands,
};
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
use revolt_result::Result;
@@ -332,6 +337,30 @@ impl Server {
Ok(())
}
/// Gets a approximate count of the members in this server
///
/// this value is cached for one hour
pub async fn get_approximate_member_count(&self, db: &Database) -> usize {
let Ok(mut redis) = get_connection().await else {
return 0;
};
let key = format!("member_count:{}", &self.id);
if let Some(count) = redis.get::<_, Option<usize>>(&key).await.ok().flatten() {
count
} else {
let count = db.fetch_member_count(&self.id).await.unwrap_or(0);
let _ = redis
.set_options::<_, _, ()>(
&key,
count,
SetOptions::default().with_expiration(SetExpiry::EX(60 * 60)),
)
.await;
count
}
}
}
impl Role {

View File

@@ -751,30 +751,29 @@ impl From<crate::RemovalIntention> for RemovalIntention {
}
}
impl From<crate::Server> for Server {
fn from(value: crate::Server) -> Self {
impl crate::Server {
pub async fn into(self, db: &Database) -> Server {
let approximate_member_count = self.get_approximate_member_count(db).await;
Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
id: self.id,
owner: self.owner,
name: self.name,
description: self.description,
channels: self.channels,
categories: self
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.unwrap_or_default() as u32,
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
system_messages: self.system_messages.map(|v| v.into()),
roles: self.roles.into_iter().map(|(k, v)| (k, v.into())).collect(),
default_permissions: self.default_permissions,
icon: self.icon.map(|f| f.into()),
banner: self.banner.map(|f| f.into()),
flags: self.flags.unwrap_or_default() as u32,
nsfw: self.nsfw,
analytics: self.analytics,
discoverable: self.discoverable,
approximate_member_count,
}
}
}
@@ -829,6 +828,7 @@ impl From<crate::PartialServer> for PartialServer {
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
approximate_member_count: None,
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-files"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -78,6 +78,9 @@ auto_derived_partial!(
serde(skip_serializing_if = "crate::if_false", default)
)]
pub discoverable: bool,
/// Approximate amount of members in the server
pub approximate_member_count: usize,
},
"PartialServer"
);

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-parser"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-ratelimits"
version = "0.14.3"
version = "0.15.0"
edition = "2024"
license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-crond"
version = "0.14.3"
version = "0.15.0"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-pushd"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
publish = false

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-voice-ingress"
version = "0.14.3"
version = "0.15.0"
license = "AGPL-3.0-or-later"
edition = "2021"
publish = false

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.14.3"
version = "0.15.0"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"

View File

@@ -28,7 +28,7 @@ pub async fn fetch(db: &State<Database>, target: Reference<'_>) -> Result<Json<v
v0::InviteResponse::Server {
code: target.id.to_string(),
member_count: db.fetch_member_count(&server.id).await? as i64,
member_count: server.get_approximate_member_count(db).await as i64,
server_id: server.id,
server_name: server.name,
server_icon: server.icon.map(|f| f.into()),

View File

@@ -28,7 +28,7 @@ pub async fn join(
Ok(Json(InviteJoinResponse::Server {
channels: channels.into_iter().map(|c| c.into()).collect(),
server: server.into(),
server: server.into(db).await,
}))
}
Invite::Group {

View File

@@ -78,5 +78,5 @@ pub async fn set_role_permission(
sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?;
}
Ok(Json(server.into()))
Ok(Json(server.into(db).await))
}

View File

@@ -66,5 +66,5 @@ pub async fn set_default_server_permissions(
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
}
Ok(Json(server.into()))
Ok(Json(server.into(db).await))
}

View File

@@ -88,7 +88,7 @@ pub async fn edit_role_ranks(
sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?;
}
Ok(Json(server.into()))
Ok(Json(server.into(db).await))
}
#[cfg(test)]

View File

@@ -52,7 +52,7 @@ pub async fn create_server(
let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?;
Ok(Json(v0::CreateServerLegacyResponse {
server: server.into(),
server: server.into(db).await,
channels: channels.into_iter().map(|channel| channel.into()).collect(),
}))
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
AuditLogEntryAction, Database, FieldsServer, File, PartialServer, User, ValidatedTicket
AuditLogEntryAction, Database, FieldsServer, File, PartialServer, User, ValidatedTicket,
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -50,7 +50,7 @@ pub async fn edit(
&& data.owner.is_none()
&& data.remove.is_empty()
{
return Ok(Json(server.into()));
return Ok(Json(server.into(db).await));
} else if data.name.is_some()
|| data.description.is_some()
|| data.icon.is_some()
@@ -195,5 +195,5 @@ pub async fn edit(
.insert(db, server.id.clone(), reason, user.id, None)
.await;
Ok(Json(server.into()))
Ok(Json(server.into(db).await))
}

View File

@@ -39,10 +39,12 @@ pub async fn fetch(
}
Ok(Json(v0::FetchServerResponse::ServerWithChannels {
server: server.into(),
server: server.into(db).await,
channels: visible_channels,
}))
} else {
Ok(Json(v0::FetchServerResponse::JustServer(server.into())))
Ok(Json(v0::FetchServerResponse::JustServer(
server.into(db).await,
)))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-autumn"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
publish = false

View File

@@ -5,7 +5,7 @@ use std::{
use axum::{
extract::{DefaultBodyLimit, Path, State},
http::{header, Method},
http::{header, HeaderMap, Method},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Json, Router,
@@ -58,6 +58,7 @@ pub async fn router() -> Router<AppState> {
)
.route("/:tag/:file_id", get(fetch_preview))
.route("/:tag/:file_id/:file_name", get(fetch_file))
.route("/mod/:tag/:file_id/:file_name", get(fetch_file_mod))
.layer(cors)
}
@@ -503,3 +504,67 @@ async fn fetch_file(
.into_response()
})
}
/// Fetch original file (Moderation)
///
/// This is intentionally left out of the OpenApi docs.
/// It uses the config key api.security.admin_keys to determine access. This is intended for server to server communication.
async fn fetch_file_mod(
State(db): State<Database>,
headers: HeaderMap,
Path((tag, file_id, file_name)): Path<(Tag, String, String)>,
) -> Result<Response> {
let config = revolt_config::config().await;
let token = headers
.get("X-Admin-Token")
.and_then(|value| value.to_str().ok())
.ok_or_else(|| create_error!(NotAuthenticated))?;
if !config
.api
.security
.admin_keys
.iter()
.any(|tok| tok == token)
{
return Err(create_error!(NotAuthenticated));
}
let tag: &'static str = tag.clone().into();
let file = db.fetch_attachment(tag, &file_id).await?;
// Ignore files that haven't been attached
if file.used_for.is_none() {
return Err(create_error!(NotFound));
}
// Ensure filename is correct
if file_name != file.filename {
if file_name == "original" {
let safe_filename = encode_component(&file.filename);
return Ok(
Redirect::permanent(&format!("/{tag}/{file_id}/{}", safe_filename)).into_response(),
);
}
return Err(create_error!(NotFound));
}
let hash = file.as_hash(&db).await?;
retrieve_file_by_hash(&hash).await.map(|data| {
(
[
(header::CONTENT_TYPE, hash.content_type),
(header::CONTENT_DISPOSITION, "attachment".to_owned()),
(
header::CACHE_CONTROL,
"private, max-age=300, must-revalidate".to_owned(),
),
],
data,
)
.into_response()
})
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-gifbox"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
publish = false

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-january"
version = "0.14.3"
version = "0.15.0"
edition = "2021"
license = "AGPL-3.0-or-later"
publish = false

View File

@@ -25,7 +25,8 @@ pub async fn create_website_embed(original_url: &str, document: &str) -> Option<
node.attr("property").or_else(|| node.attr("name")),
node.attr("content"),
) {
meta.insert(property.to_string(), content.to_string());
meta.entry(property.to_string())
.or_insert(content.to_string());
}
}

View File

@@ -1 +1 @@
0.14.3
0.15.0