Compare commits

..

5 commits

Author SHA1 Message Date
4cb89645fe Merge pull request 'Member Improvements' (#44) from wip/member-improvements into main
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #44
2025-08-05 00:09:19 +02:00
ac1678bfa8 fix: use dedicated function for member count
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-08-05 00:02:30 +02:00
642dbe5270 fix: remove order_by on single fetches 2025-08-05 00:02:21 +02:00
8d91ec78a6 refactor: rename fetch_one_with_member
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
Renamed to fetch_one_with_uuid
2025-08-04 22:55:48 +02:00
e9cc2a3f0e feat: faster member fetching and pagination 2025-08-04 22:55:22 +02:00
6 changed files with 176 additions and 37 deletions

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use ::uuid::Uuid; use ::uuid::Uuid;
use axum::{ use axum::{
Extension, Json, Extension, Json,
extract::{Path, State}, extract::{Path, Query, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
}; };
@ -12,13 +12,14 @@ use crate::{
AppState, AppState,
api::v1::auth::CurrentUser, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Me, Member}, objects::{Me, Member, PaginationRequest},
utils::global_checks, utils::global_checks,
}; };
pub async fn get( pub async fn get(
State(app_state): State<Arc<AppState>>, State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>, Path(guild_uuid): Path<Uuid>,
Query(pagination): Query<PaginationRequest>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?; let mut conn = app_state.pool.get().await?;
@ -29,7 +30,14 @@ pub async fn get(
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let members = Member::fetch_all(&mut conn, &app_state.cache_pool, &me, guild_uuid).await?; let members = Member::fetch_page(
&mut conn,
&app_state.cache_pool,
&me,
guild_uuid,
pagination,
)
.await?;
Ok((StatusCode::OK, Json(members))) Ok((StatusCode::OK, Json(members)))
} }

View file

@ -34,7 +34,7 @@ pub async fn post(
global_checks(&mut conn, &app_state.config, uuid).await?; global_checks(&mut conn, &app_state.config, uuid).await?;
let member = let member =
Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, None, member_uuid).await?; Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, None, member_uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;

View file

@ -32,7 +32,7 @@ pub async fn get(
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let member = let member =
Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid)
.await?; .await?;
Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
@ -51,7 +51,7 @@ pub async fn delete(
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let member = let member =
Member::fetch_one_with_member(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid)
.await?; .await?;
let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;

View file

@ -1,6 +1,7 @@
use diesel::{ use diesel::{
ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl,
delete, insert_into, QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into,
sql_types::{Nullable, VarChar},
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -9,14 +10,21 @@ use uuid::Uuid;
use crate::{ use crate::{
Conn, Conn,
error::Error, error::Error,
objects::{GuildBan, Me, Permissions, Role}, objects::PaginationRequest,
schema::{guild_bans, guild_members}, schema::{friends, guild_bans, guild_members, users},
}; };
use super::{User, load_or_empty}; use super::{
Friend, Guild, GuildBan, Me, Pagination, Permissions, Role, User, load_or_empty,
user::UserBuilder,
};
#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable)] define_sql_function! { fn coalesce(x: Nullable<VarChar>, y: Nullable<VarChar>, z: VarChar) -> Text; }
#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)]
#[diesel(table_name = guild_members)] #[diesel(table_name = guild_members)]
#[diesel(belongs_to(UserBuilder, foreign_key = user_uuid))]
#[diesel(belongs_to(Guild, foreign_key = guild_uuid))]
#[diesel(primary_key(uuid))] #[diesel(primary_key(uuid))]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct MemberBuilder { pub struct MemberBuilder {
@ -55,6 +63,32 @@ impl MemberBuilder {
}) })
} }
async fn build_with_parts(
&self,
conn: &mut Conn,
cache_pool: &redis::Client,
user_builder: UserBuilder,
friend: Option<Friend>,
) -> Result<Member, Error> {
let mut user = user_builder.build();
if let Some(friend) = friend {
user.friends_since = Some(friend.accepted_at);
}
let roles = Role::fetch_from_member(conn, cache_pool, self).await?;
Ok(Member {
uuid: self.uuid,
nickname: self.nickname.clone(),
user_uuid: self.user_uuid,
guild_uuid: self.guild_uuid,
is_owner: self.is_owner,
user,
roles,
})
}
pub async fn check_permission( pub async fn check_permission(
&self, &self,
conn: &mut Conn, conn: &mut Conn,
@ -120,52 +154,135 @@ impl Member {
user_uuid: Uuid, user_uuid: Uuid,
guild_uuid: Uuid, guild_uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use friends::dsl as fdsl;
use guild_members::dsl; use guild_members::dsl;
let member: MemberBuilder = dsl::guild_members let (member, user, friend): (MemberBuilder, UserBuilder, Option<Friend>) =
.filter(dsl::user_uuid.eq(user_uuid)) dsl::guild_members
.filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid))
.select(MemberBuilder::as_select()) .filter(dsl::user_uuid.eq(user_uuid))
.get_result(conn) .inner_join(users::table)
.await?; .left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.get_result(conn)
.await?;
member.build(conn, cache_pool, Some(me)).await member
.build_with_parts(conn, cache_pool, user, friend)
.await
} }
pub async fn fetch_one_with_member( pub async fn fetch_one_with_uuid(
conn: &mut Conn, conn: &mut Conn,
cache_pool: &redis::Client, cache_pool: &redis::Client,
me: Option<&Me>, me: Option<&Me>,
uuid: Uuid, uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let member: MemberBuilder;
let user: UserBuilder;
let friend: Option<Friend>;
use friends::dsl as fdsl;
use guild_members::dsl; use guild_members::dsl;
let member: MemberBuilder = dsl::guild_members if let Some(me) = me {
.filter(dsl::uuid.eq(uuid)) (member, user, friend) = dsl::guild_members
.select(MemberBuilder::as_select()) .filter(dsl::uuid.eq(uuid))
.get_result(conn) .inner_join(users::table)
.await?; .left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.get_result(conn)
.await?;
} else {
(member, user) = dsl::guild_members
.filter(dsl::uuid.eq(uuid))
.inner_join(users::table)
.select((MemberBuilder::as_select(), UserBuilder::as_select()))
.get_result(conn)
.await?;
member.build(conn, cache_pool, me).await friend = None;
}
member
.build_with_parts(conn, cache_pool, user, friend)
.await
} }
pub async fn fetch_all( pub async fn fetch_page(
conn: &mut Conn, conn: &mut Conn,
cache_pool: &redis::Client, cache_pool: &redis::Client,
me: &Me, me: &Me,
guild_uuid: Uuid, guild_uuid: Uuid,
) -> Result<Vec<Self>, Error> { pagination: PaginationRequest,
) -> Result<Pagination<Self>, Error> {
let per_page = pagination.per_page.unwrap_or(50);
let page_multiplier: i64 = ((pagination.page - 1) * per_page).into();
if !(10..=100).contains(&per_page) {
return Err(Error::BadRequest(
"Invalid amount per page requested".to_string(),
));
}
use friends::dsl as fdsl;
use guild_members::dsl; use guild_members::dsl;
let member_builders: Vec<MemberBuilder> = load_or_empty( let member_builders: Vec<(MemberBuilder, UserBuilder, Option<Friend>)> = load_or_empty(
dsl::guild_members dsl::guild_members
.filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid))
.select(MemberBuilder::as_select()) .inner_join(users::table)
.left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.limit(per_page.into())
.offset(page_multiplier)
.order_by(coalesce(
dsl::nickname,
users::display_name,
users::username,
))
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.load(conn) .load(conn)
.await, .await,
)?; )?;
let mut members = vec![]; let pages = Member::count(conn, guild_uuid).await? as f32 / per_page as f32;
for builder in member_builders { let mut members = Pagination::<Member> {
members.push(builder.build(conn, cache_pool, Some(me)).await?); objects: Vec::with_capacity(member_builders.len()),
amount: member_builders.len() as i32,
pages: pages.ceil() as i32,
page: pagination.page,
};
for (member, user, friend) in member_builders {
members.objects.push(
member
.build_with_parts(conn, cache_pool, user, friend)
.await?,
);
} }
Ok(members) Ok(members)

View file

@ -4,7 +4,7 @@ use lettre::{
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
}; };
use log::debug; use log::debug;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
mod bans; mod bans;
@ -76,6 +76,20 @@ impl Cookies for Request<Body> {
} }
*/ */
#[derive(Serialize)]
pub struct Pagination<T> {
objects: Vec<T>,
amount: i32,
pages: i32,
page: i32,
}
#[derive(Deserialize)]
pub struct PaginationRequest {
pub page: i32,
pub per_page: Option<i32>,
}
fn load_or_empty<T>( fn load_or_empty<T>(
query_result: Result<Vec<T>, diesel::result::Error>, query_result: Result<Vec<T>, diesel::result::Error>,
) -> Result<Vec<T>, diesel::result::Error> { ) -> Result<Vec<T>, diesel::result::Error> {

View file

@ -22,7 +22,7 @@ pub struct UserBuilder {
} }
impl UserBuilder { impl UserBuilder {
fn build(self) -> User { pub fn build(self) -> User {
User { User {
uuid: self.uuid, uuid: self.uuid,
username: self.username, username: self.username,
@ -38,14 +38,14 @@ impl UserBuilder {
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct User { pub struct User {
uuid: Uuid, pub uuid: Uuid,
username: String, username: String,
display_name: Option<String>, display_name: Option<String>,
avatar: Option<String>, avatar: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
online_status: i16, online_status: i16,
friends_since: Option<DateTime<Utc>>, pub friends_since: Option<DateTime<Utc>>,
} }
impl User { impl User {