diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 56710af..0e1d2bc 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use ::uuid::Uuid; use axum::{ Extension, Json, - extract::{Path, State}, + extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, }; @@ -12,13 +12,14 @@ use crate::{ AppState, api::v1::auth::CurrentUser, error::Error, - objects::{Me, Member}, + objects::{Me, Member, PaginationRequest}, utils::global_checks, }; pub async fn get( State(app_state): State>, Path(guild_uuid): Path, + Query(pagination): Query, Extension(CurrentUser(uuid)): Extension>, ) -> Result { 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 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))) } diff --git a/src/objects/member.rs b/src/objects/member.rs index a25bf66..59929cd 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,6 +1,7 @@ use diesel::{ - ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, - delete, insert_into, + Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl, + QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into, + sql_types::{Nullable, VarChar}, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -9,14 +10,21 @@ use uuid::Uuid; use crate::{ Conn, error::Error, - objects::{GuildBan, Me, Permissions, Role}, - schema::{guild_bans, guild_members}, + objects::PaginationRequest, + 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, y: Nullable, z: VarChar) -> Text; } + +#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)] #[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(check_for_backend(diesel::pg::Pg))] 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, + ) -> Result { + 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( &self, conn: &mut Conn, @@ -120,15 +154,35 @@ impl Member { user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { + use friends::dsl as fdsl; use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; + let (member, user, friend): (MemberBuilder, UserBuilder, Option) = + dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .filter(dsl::user_uuid.eq(user_uuid)) + .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)))), + ) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select(( + MemberBuilder::as_select(), + UserBuilder::as_select(), + Option::::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( @@ -137,35 +191,119 @@ impl Member { me: Option<&Me>, uuid: Uuid, ) -> Result { + let member: MemberBuilder; + let user: UserBuilder; + let friend: Option; + use friends::dsl as fdsl; use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; + if let Some(me) = me { + (member, user, friend) = dsl::guild_members + .filter(dsl::uuid.eq(uuid)) + .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)))), + ) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .select(( + MemberBuilder::as_select(), + UserBuilder::as_select(), + Option::::as_select(), + )) + .get_result(conn) + .await?; + } else { + (member, user) = dsl::guild_members + .filter(dsl::uuid.eq(uuid)) + .inner_join(users::table) + .order_by(coalesce( + dsl::nickname, + users::display_name, + users::username, + )) + .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, cache_pool: &redis::Client, me: &Me, guild_uuid: Uuid, - ) -> Result, Error> { + pagination: PaginationRequest, + ) -> Result, 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; - let member_builders: Vec = load_or_empty( + let member_builders: Vec<(MemberBuilder, UserBuilder, Option)> = load_or_empty( dsl::guild_members .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::::as_select(), + )) .load(conn) .await, )?; - let mut members = vec![]; + let member_count: i64 = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .count() + .get_result(conn) + .await?; - for builder in member_builders { - members.push(builder.build(conn, cache_pool, Some(me)).await?); + let pages = member_count as f32 / per_page as f32; + + let mut members = Pagination:: { + 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) diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 3bcce9c..5a013ca 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -4,7 +4,7 @@ use lettre::{ transport::smtp::authentication::Credentials, }; use log::debug; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; mod bans; @@ -76,6 +76,20 @@ impl Cookies for Request { } */ +#[derive(Serialize)] +pub struct Pagination { + objects: Vec, + amount: i32, + pages: i32, + page: i32, +} + +#[derive(Deserialize)] +pub struct PaginationRequest { + pub page: i32, + pub per_page: Option, +} + fn load_or_empty( query_result: Result, diesel::result::Error>, ) -> Result, diesel::result::Error> { diff --git a/src/objects/user.rs b/src/objects/user.rs index a686c39..596a785 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -21,7 +21,7 @@ pub struct UserBuilder { } impl UserBuilder { - fn build(self) -> User { + pub fn build(self) -> User { User { uuid: self.uuid, username: self.username, @@ -36,13 +36,13 @@ impl UserBuilder { #[derive(Deserialize, Serialize, Clone)] pub struct User { - uuid: Uuid, + pub uuid: Uuid, username: String, display_name: Option, avatar: Option, pronouns: Option, about: Option, - friends_since: Option>, + pub friends_since: Option>, } impl User {