diff --git a/migrations/2025-05-26-181536_add_channel_ordering/down.sql b/migrations/2025-05-26-181536_add_channel_ordering/down.sql new file mode 100644 index 0000000..0a70d35 --- /dev/null +++ b/migrations/2025-05-26-181536_add_channel_ordering/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE channels DROP COLUMN is_above; diff --git a/migrations/2025-05-26-181536_add_channel_ordering/up.sql b/migrations/2025-05-26-181536_add_channel_ordering/up.sql new file mode 100644 index 0000000..e18e5e2 --- /dev/null +++ b/migrations/2025-05-26-181536_add_channel_ordering/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE channels ADD COLUMN is_above UUID UNIQUE REFERENCES channels(uuid) DEFAULT NULL; diff --git a/src/api/v1/servers/uuid/channels/mod.rs b/src/api/v1/servers/uuid/channels/mod.rs index 0c515ef..3021ded 100644 --- a/src/api/v1/servers/uuid/channels/mod.rs +++ b/src/api/v1/servers/uuid/channels/mod.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - structs::{Channel, Member}, - utils::get_auth_header, + api::v1::auth::check_access_token, error::Error, structs::{Channel, Member}, utils::{get_auth_header, order_channels}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -43,10 +39,12 @@ pub async fn get( let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; - data.set_cache_key(format!("{}_channels", guild_uuid), channels.clone(), 1800) + let channels_ordered = order_channels(channels).await?; + + data.set_cache_key(format!("{}_channels", guild_uuid), channels_ordered.clone(), 1800) .await?; - Ok(HttpResponse::Ok().json(channels)) + Ok(HttpResponse::Ok().json(channels_ordered)) } #[post("{uuid}/channels")] @@ -76,7 +74,7 @@ pub async fn create( channel_info.name.clone(), channel_info.description.clone(), ) - .await; + .await?; - Ok(HttpResponse::Ok().json(channel.unwrap())) + Ok(HttpResponse::Ok().json(channel)) } diff --git a/src/error.rs b/src/error.rs index fa9524f..3907f7c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,6 +60,8 @@ pub enum Error { BadRequest(String), #[error("{0}")] Unauthorized(String), + #[error("{0}")] + InternalServerError(String), } impl ResponseError for Error { diff --git a/src/schema.rs b/src/schema.rs index 33935f7..8a85a2e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -27,6 +27,7 @@ diesel::table! { name -> Varchar, #[max_length = 500] description -> Nullable, + is_above -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index c86bc5b..b423b20 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,12 +5,18 @@ use diesel::{ update, }; use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use log::debug; use serde::{Deserialize, Serialize}; use tokio::task; use url::Url; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::*, utils::image_check}; +use crate::{ + Conn, Data, + error::Error, + schema::*, + utils::{image_check, order_channels}, +}; fn load_or_empty( query_result: Result, diesel::result::Error>, @@ -22,7 +28,7 @@ fn load_or_empty( } } -#[derive(Queryable, Selectable, Insertable, Clone)] +#[derive(Queryable, Selectable, Insertable, Clone, Debug)] #[diesel(table_name = channels)] #[diesel(check_for_backend(diesel::pg::Pg))] struct ChannelBuilder { @@ -30,6 +36,7 @@ struct ChannelBuilder { guild_uuid: Uuid, name: String, description: Option, + is_above: Option, } impl ChannelBuilder { @@ -48,21 +55,23 @@ impl ChannelBuilder { guild_uuid: self.guild_uuid, name: self.name, description: self.description, + is_above: self.is_above, permissions: channel_permission, }) } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct Channel { pub uuid: Uuid, pub guild_uuid: Uuid, name: String, description: Option, + pub is_above: Option, pub permissions: Vec, } -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] #[diesel(table_name = channel_permissions)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct ChannelPermission { @@ -118,24 +127,47 @@ impl Channel { let channel_uuid = Uuid::now_v7(); + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + + debug!("{:?}", channels); + + let channels_ordered = order_channels(channels).await?; + + debug!("{:?}", channels_ordered); + + let last_channel = channels_ordered.last(); + let new_channel = ChannelBuilder { uuid: channel_uuid, guild_uuid, name: name.clone(), description: description.clone(), + is_above: None, }; + debug!("New Channel: {:?}", new_channel); + insert_into(channels::table) - .values(new_channel) + .values(new_channel.clone()) .execute(&mut conn) .await?; + if let Some(old_last_channel) = last_channel { + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(old_last_channel.uuid)) + .set(dsl::is_above.eq(new_channel.uuid)) + .execute(&mut conn) + .await?; + } + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) let channel = Self { uuid: channel_uuid, guild_uuid, name, description, + is_above: None, permissions: vec![], }; diff --git a/src/utils.rs b/src/utils.rs index 631b003..e68f38a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use hex::encode; use redis::RedisError; use serde::Serialize; -use crate::{Data, error::Error}; +use crate::{Data, error::Error, structs::Channel}; pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); @@ -119,6 +119,28 @@ pub fn image_check(icon: BytesMut) -> Result { )) } +pub async fn order_channels(mut channels: Vec) -> Result, Error> { + let mut ordered = Vec::new(); + + // Find head + let head_pos = channels + .iter() + .position(|channel| !channels.iter().any(|i| i.is_above == Some(channel.uuid))); + + if let Some(pos) = head_pos { + ordered.push(channels.swap_remove(pos)); + + while let Some(next_pos) = channels + .iter() + .position(|channel| Some(channel.uuid) == ordered.last().unwrap().is_above) + { + ordered.push(channels.swap_remove(next_pos)); + } + } + + Ok(ordered) +} + impl Data { pub async fn set_cache_key( &self,