Compare commits
2 commits
main
...
wip/catego
Author | SHA1 | Date | |
---|---|---|---|
45bca0bd20 | |||
b7b07141f9 |
11 changed files with 512 additions and 0 deletions
2
migrations/2025-07-12-124819_message_is_edited/down.sql
Normal file
2
migrations/2025-07-12-124819_message_is_edited/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
ALTER TABLE messages DROP COLUMN is_edited;
|
2
migrations/2025-07-12-124819_message_is_edited/up.sql
Normal file
2
migrations/2025-07-12-124819_message_is_edited/up.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- Your SQL goes here
|
||||
ALTER TABLE messages ADD COLUMN is_edited BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,4 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
ALTER TABLE channels DROP COLUMN in_category;
|
||||
|
||||
DROP TABLE categories;
|
10
migrations/2025-07-12-135941_add_channel_categories/up.sql
Normal file
10
migrations/2025-07-12-135941_add_channel_categories/up.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
-- Your SQL goes here
|
||||
CREATE TABLE categories (
|
||||
uuid UUID PRIMARY KEY NOT NULL,
|
||||
guild_uuid UUID NOT NULL REFERENCES guilds(uuid),
|
||||
name VARCHAR(32) NOT NULL,
|
||||
description VARCHAR(500) DEFAULT NULL,
|
||||
is_above UUID UNIQUE REFERENCES categories(uuid) DEFAULT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE channels ADD COLUMN in_category UUID REFERENCES categories(uuid) DEFAULT NULL;
|
92
src/api/v1/guilds/uuid/categories.rs
Normal file
92
src/api/v1/guilds/uuid/categories.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use crate::{
|
||||
Data,
|
||||
api::v1::auth::check_access_token,
|
||||
error::Error,
|
||||
objects::{Channel, Member, Permissions},
|
||||
utils::{get_auth_header, global_checks, order_by_is_above},
|
||||
};
|
||||
use ::uuid::Uuid;
|
||||
use actix_web::{HttpRequest, HttpResponse, get, post, web};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChannelInfo {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[get("{uuid}/categories")]
|
||||
pub async fn get(
|
||||
req: HttpRequest,
|
||||
path: web::Path<(Uuid,)>,
|
||||
data: web::Data<Data>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let headers = req.headers();
|
||||
|
||||
let auth_header = get_auth_header(headers)?;
|
||||
|
||||
let guild_uuid = path.into_inner().0;
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
let uuid = check_access_token(auth_header, &mut conn).await?;
|
||||
|
||||
global_checks(&data, uuid).await?;
|
||||
|
||||
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
|
||||
|
||||
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await {
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(cache_hit));
|
||||
}
|
||||
|
||||
let channels = Channel::fetch_all(&data.pool, guild_uuid).await?;
|
||||
|
||||
let channels_ordered = order_by_is_above(channels).await?;
|
||||
|
||||
data.set_cache_key(
|
||||
format!("{guild_uuid}_channels"),
|
||||
channels_ordered.clone(),
|
||||
1800,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(channels_ordered))
|
||||
}
|
||||
|
||||
#[post("{uuid}/categories")]
|
||||
pub async fn create(
|
||||
req: HttpRequest,
|
||||
channel_info: web::Json<ChannelInfo>,
|
||||
path: web::Path<(Uuid,)>,
|
||||
data: web::Data<Data>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let headers = req.headers();
|
||||
|
||||
let auth_header = get_auth_header(headers)?;
|
||||
|
||||
let guild_uuid = path.into_inner().0;
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
let uuid = check_access_token(auth_header, &mut conn).await?;
|
||||
|
||||
global_checks(&data, uuid).await?;
|
||||
|
||||
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
|
||||
|
||||
member
|
||||
.check_permission(&data, Permissions::CreateChannel)
|
||||
.await?;
|
||||
|
||||
let channel = Channel::new(
|
||||
data.clone(),
|
||||
guild_uuid,
|
||||
channel_info.name.clone(),
|
||||
channel_info.description.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(channel))
|
||||
}
|
|
@ -8,6 +8,7 @@ mod icon;
|
|||
mod invites;
|
||||
mod members;
|
||||
mod roles;
|
||||
mod categories;
|
||||
|
||||
use crate::{
|
||||
Data,
|
||||
|
@ -21,6 +22,9 @@ pub fn web() -> Scope {
|
|||
web::scope("")
|
||||
// Servers
|
||||
.service(get)
|
||||
// Categories
|
||||
.service(categories::get)
|
||||
.service(categories::create)
|
||||
// Channels
|
||||
.service(channels::get)
|
||||
.service(channels::create)
|
||||
|
|
361
src/objects/categories.rs
Normal file
361
src/objects/categories.rs
Normal file
|
@ -0,0 +1,361 @@
|
|||
use diesel::{
|
||||
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete,
|
||||
insert_into, update,
|
||||
};
|
||||
use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
Conn, Data,
|
||||
error::Error,
|
||||
schema::categories,
|
||||
utils::{CHANNEL_REGEX, order_by_is_above},
|
||||
};
|
||||
|
||||
use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder};
|
||||
|
||||
#[derive(Serialize, Deserialize, Queryable, Selectable, Insertable, Clone, Debug)]
|
||||
#[diesel(table_name = categories)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
struct Category {
|
||||
uuid: Uuid,
|
||||
guild_uuid: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
is_above: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl HasUuid for Category {
|
||||
fn uuid(&self) -> &Uuid {
|
||||
self.uuid.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasIsAbove for Category {
|
||||
fn is_above(&self) -> Option<&Uuid> {
|
||||
self.is_above.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub async fn fetch_all(
|
||||
pool: &deadpool::managed::Pool<
|
||||
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
|
||||
Conn,
|
||||
>,
|
||||
guild_uuid: Uuid,
|
||||
) -> Result<Vec<Self>, Error> {
|
||||
let mut conn = pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
let channel_builders: Vec<ChannelBuilder> = load_or_empty(
|
||||
dsl::channels
|
||||
.filter(dsl::guild_uuid.eq(guild_uuid))
|
||||
.select(ChannelBuilder::as_select())
|
||||
.load(&mut conn)
|
||||
.await,
|
||||
)?;
|
||||
|
||||
let channel_futures = channel_builders.iter().map(async move |c| {
|
||||
let mut conn = pool.get().await?;
|
||||
c.clone().build(&mut conn).await
|
||||
});
|
||||
|
||||
futures::future::try_join_all(channel_futures).await
|
||||
}
|
||||
|
||||
pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result<Self, Error> {
|
||||
if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await {
|
||||
return Ok(serde_json::from_str(&cache_hit)?);
|
||||
}
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
let channel_builder: ChannelBuilder = dsl::channels
|
||||
.filter(dsl::uuid.eq(channel_uuid))
|
||||
.select(ChannelBuilder::as_select())
|
||||
.get_result(&mut conn)
|
||||
.await?;
|
||||
|
||||
let channel = channel_builder.build(&mut conn).await?;
|
||||
|
||||
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60)
|
||||
.await?;
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
data: actix_web::web::Data<Data>,
|
||||
guild_uuid: Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
) -> Result<Self, Error> {
|
||||
if !CHANNEL_REGEX.is_match(&name) {
|
||||
return Err(Error::BadRequest("Channel name is invalid".to_string()));
|
||||
}
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
let channel_uuid = Uuid::now_v7();
|
||||
|
||||
let channels = Self::fetch_all(&data.pool, guild_uuid).await?;
|
||||
|
||||
let channels_ordered = order_by_is_above(channels).await?;
|
||||
|
||||
let last_channel = channels_ordered.last();
|
||||
|
||||
let new_channel = ChannelBuilder {
|
||||
uuid: channel_uuid,
|
||||
guild_uuid,
|
||||
name: name.clone(),
|
||||
description: description.clone(),
|
||||
is_above: None,
|
||||
};
|
||||
|
||||
insert_into(channels::table)
|
||||
.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![],
|
||||
};
|
||||
|
||||
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800)
|
||||
.await?;
|
||||
|
||||
if data
|
||||
.get_cache_key(format!("{guild_uuid}_channels"))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
data.del_cache_key(format!("{guild_uuid}_channels")).await?;
|
||||
}
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
pub async fn delete(self, data: &Data) -> Result<(), Error> {
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
match update(channels::table)
|
||||
.filter(dsl::is_above.eq(self.uuid))
|
||||
.set(dsl::is_above.eq(None::<Uuid>))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(r),
|
||||
Err(diesel::result::Error::NotFound) => Ok(0),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
|
||||
delete(channels::table)
|
||||
.filter(dsl::uuid.eq(self.uuid))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
match update(channels::table)
|
||||
.filter(dsl::is_above.eq(self.uuid))
|
||||
.set(dsl::is_above.eq(self.is_above))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(r),
|
||||
Err(diesel::result::Error::NotFound) => Ok(0),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
|
||||
if data.get_cache_key(self.uuid.to_string()).await.is_ok() {
|
||||
data.del_cache_key(self.uuid.to_string()).await?;
|
||||
}
|
||||
|
||||
if data
|
||||
.get_cache_key(format!("{}_channels", self.guild_uuid))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
data.del_cache_key(format!("{}_channels", self.guild_uuid))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_messages(
|
||||
&self,
|
||||
data: &Data,
|
||||
amount: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<Message>, Error> {
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use messages::dsl;
|
||||
let messages: Vec<MessageBuilder> = load_or_empty(
|
||||
dsl::messages
|
||||
.filter(dsl::channel_uuid.eq(self.uuid))
|
||||
.select(MessageBuilder::as_select())
|
||||
.order(dsl::uuid.desc())
|
||||
.limit(amount)
|
||||
.offset(offset)
|
||||
.load(&mut conn)
|
||||
.await,
|
||||
)?;
|
||||
|
||||
let message_futures = messages.iter().map(async move |b| b.build(data).await);
|
||||
|
||||
futures::future::try_join_all(message_futures).await
|
||||
}
|
||||
|
||||
pub async fn new_message(
|
||||
&self,
|
||||
data: &Data,
|
||||
user_uuid: Uuid,
|
||||
message: String,
|
||||
reply_to: Option<Uuid>,
|
||||
) -> Result<Message, Error> {
|
||||
let message_uuid = Uuid::now_v7();
|
||||
|
||||
let message = MessageBuilder {
|
||||
uuid: message_uuid,
|
||||
channel_uuid: self.uuid,
|
||||
user_uuid,
|
||||
message,
|
||||
reply_to,
|
||||
is_edited: false,
|
||||
};
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
insert_into(messages::table)
|
||||
.values(message.clone())
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
message.build(data).await
|
||||
}
|
||||
|
||||
/*pub async fn edit_message(&self, data: &Data, user_uuid: Uuid, message_uuid: Uuid, message: String) -> Result<Message, Error> {
|
||||
use messages::dsl;
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
update(messages::table)
|
||||
.filter(dsl::user_uuid.eq(user_uuid))
|
||||
.filter(dsl::uuid.eq(message_uuid))
|
||||
.set((dsl::is_edited.eq(true), dsl::message.eq(message)))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}*/
|
||||
|
||||
pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> {
|
||||
if !CHANNEL_REGEX.is_match(&new_name) {
|
||||
return Err(Error::BadRequest("Channel name is invalid".to_string()));
|
||||
}
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
update(channels::table)
|
||||
.filter(dsl::uuid.eq(self.uuid))
|
||||
.set(dsl::name.eq(&new_name))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
self.name = new_name;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_description(
|
||||
&mut self,
|
||||
data: &Data,
|
||||
new_description: String,
|
||||
) -> Result<(), Error> {
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
update(channels::table)
|
||||
.filter(dsl::uuid.eq(self.uuid))
|
||||
.set(dsl::description.eq(&new_description))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
self.description = Some(new_description);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> {
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
use channels::dsl;
|
||||
let old_above_uuid: Option<Uuid> = match dsl::channels
|
||||
.filter(dsl::is_above.eq(self.uuid))
|
||||
.select(dsl::uuid)
|
||||
.get_result(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(Some(r)),
|
||||
Err(diesel::result::Error::NotFound) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
|
||||
if let Some(uuid) = old_above_uuid {
|
||||
update(channels::table)
|
||||
.filter(dsl::uuid.eq(uuid))
|
||||
.set(dsl::is_above.eq(None::<Uuid>))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
match update(channels::table)
|
||||
.filter(dsl::is_above.eq(new_is_above))
|
||||
.set(dsl::is_above.eq(self.uuid))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(r) => Ok(r),
|
||||
Err(diesel::result::Error::NotFound) => Ok(0),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
|
||||
update(channels::table)
|
||||
.filter(dsl::uuid.eq(self.uuid))
|
||||
.set(dsl::is_above.eq(new_is_above))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
if let Some(uuid) = old_above_uuid {
|
||||
update(channels::table)
|
||||
.filter(dsl::uuid.eq(uuid))
|
||||
.set(dsl::is_above.eq(self.is_above))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.is_above = Some(new_is_above);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -280,6 +280,7 @@ impl Channel {
|
|||
user_uuid,
|
||||
message,
|
||||
reply_to,
|
||||
is_edited: false,
|
||||
};
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
@ -292,6 +293,21 @@ impl Channel {
|
|||
message.build(data).await
|
||||
}
|
||||
|
||||
/*pub async fn edit_message(&self, data: &Data, user_uuid: Uuid, message_uuid: Uuid, message: String) -> Result<Message, Error> {
|
||||
use messages::dsl;
|
||||
|
||||
let mut conn = data.pool.get().await?;
|
||||
|
||||
update(messages::table)
|
||||
.filter(dsl::user_uuid.eq(user_uuid))
|
||||
.filter(dsl::uuid.eq(message_uuid))
|
||||
.set((dsl::is_edited.eq(true), dsl::message.eq(message)))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}*/
|
||||
|
||||
pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> {
|
||||
if !CHANNEL_REGEX.is_match(&new_name) {
|
||||
return Err(Error::BadRequest("Channel name is invalid".to_string()));
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct MessageBuilder {
|
|||
pub user_uuid: Uuid,
|
||||
pub message: String,
|
||||
pub reply_to: Option<Uuid>,
|
||||
pub is_edited: bool,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
|
@ -27,6 +28,7 @@ impl MessageBuilder {
|
|||
user_uuid: self.user_uuid,
|
||||
message: self.message.clone(),
|
||||
reply_to: self.reply_to,
|
||||
is_edited: self.is_edited,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
@ -39,5 +41,6 @@ pub struct Message {
|
|||
user_uuid: Uuid,
|
||||
message: String,
|
||||
reply_to: Option<Uuid>,
|
||||
is_edited: bool,
|
||||
user: User,
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use log::debug;
|
|||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod categories;
|
||||
mod channel;
|
||||
mod email_token;
|
||||
mod friends;
|
||||
|
|
|
@ -11,6 +11,18 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
categories (uuid) {
|
||||
uuid -> Uuid,
|
||||
guild_uuid -> Uuid,
|
||||
#[max_length = 32]
|
||||
name -> Varchar,
|
||||
#[max_length = 500]
|
||||
description -> Nullable<Varchar>,
|
||||
is_above -> Nullable<Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
channel_permissions (channel_uuid, role_uuid) {
|
||||
channel_uuid -> Uuid,
|
||||
|
@ -28,6 +40,7 @@ diesel::table! {
|
|||
#[max_length = 500]
|
||||
description -> Nullable<Varchar>,
|
||||
is_above -> Nullable<Uuid>,
|
||||
in_category -> Nullable<Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +107,7 @@ diesel::table! {
|
|||
#[max_length = 4000]
|
||||
message -> Varchar,
|
||||
reply_to -> Nullable<Uuid>,
|
||||
is_edited -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +166,9 @@ diesel::table! {
|
|||
|
||||
diesel::joinable!(access_tokens -> refresh_tokens (refresh_token));
|
||||
diesel::joinable!(access_tokens -> users (uuid));
|
||||
diesel::joinable!(categories -> guilds (guild_uuid));
|
||||
diesel::joinable!(channel_permissions -> channels (channel_uuid));
|
||||
diesel::joinable!(channels -> categories (in_category));
|
||||
diesel::joinable!(channels -> guilds (guild_uuid));
|
||||
diesel::joinable!(guild_members -> guilds (guild_uuid));
|
||||
diesel::joinable!(guild_members -> users (user_uuid));
|
||||
|
@ -167,6 +183,7 @@ diesel::joinable!(roles -> guilds (guild_uuid));
|
|||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
access_tokens,
|
||||
categories,
|
||||
channel_permissions,
|
||||
channels,
|
||||
friend_requests,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue