feat: add an approximate member count to servers (#884)

* feat: add an approximate member count to servers

Signed-off-by: Zomatree <me@zomatree.live>

* fix: use approx member count on invites

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
This commit is contained in:
Zomatree
2026-08-05 08:14:05 +01:00
committed by GitHub
parent fa74826eea
commit ccfb5f1a62
13 changed files with 77 additions and 37 deletions

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

@@ -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,5 +1,10 @@
use std::collections::{HashMap, HashSet};
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;
@@ -322,6 +327,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

@@ -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

@@ -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,
)))
}
}