Skip to content
Clone
194c44c
194 lines 6.1 KB Rust
use models::chat::room_bookmarks;
use sea_orm::sea_query::Expr;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, ExprTrait, QueryFilter, QueryOrder, QuerySelect,
Set,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::ChatError;
use crate::services::service::ChatService;
use crate::share::lookup::require_room_readable;
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct BookmarkDto {
pub id: Uuid,
pub room_id: Uuid,
pub title: String,
pub url: Option<String>,
pub emoji: Option<String>,
pub position: i64,
pub created_by_id: i64,
pub created_at: OffsetDateTime,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct CreateBookmarkCmd {
pub title: String,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub emoji: Option<String>,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct UpdateBookmarkCmd {
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub emoji: Option<String>,
#[serde(default)]
pub position: Option<i64>,
}
impl From<room_bookmarks::Model> for BookmarkDto {
fn from(b: room_bookmarks::Model) -> Self {
Self {
id: b.id,
room_id: b.room_id,
title: b.title,
url: b.url,
emoji: b.emoji,
position: b.position,
created_by_id: b.user_id,
created_at: b.created_at,
}
}
}
impl ChatService {
pub async fn list_bookmarks(
&self,
caller: i64,
room_id: Uuid,
) -> Result<Vec<BookmarkDto>, ChatError> {
let _ctx = require_room_readable(self, room_id, caller).await?;
let rows = room_bookmarks::Entity::find()
.filter(room_bookmarks::Column::RoomId.eq(room_id))
.order_by_asc(room_bookmarks::Column::Position)
.order_by_asc(room_bookmarks::Column::CreatedAt)
.all(self.db().read())
.await?;
Ok(rows.into_iter().map(BookmarkDto::from).collect())
}
pub async fn create_bookmark(
&self,
caller: i64,
room_id: Uuid,
cmd: CreateBookmarkCmd,
) -> Result<BookmarkDto, ChatError> {
let _ctx = require_room_readable(self, room_id, caller).await?;
let title = cmd.title.trim().to_string();
if title.is_empty() || title.chars().count() > self.config().chat.max_bookmark_title_len {
return Err(ChatError::BadRequest("invalid bookmark title".to_string()));
}
if cmd.url.is_none() && cmd.emoji.is_none() {
return Err(ChatError::BadRequest(
"bookmark requires url or emoji".to_string(),
));
}
let max_pos: i64 = room_bookmarks::Entity::find()
.filter(room_bookmarks::Column::RoomId.eq(room_id))
.select_only()
.column_as(Expr::col(room_bookmarks::Column::Position).max(), "max_pos")
.into_tuple::<Option<i64>>()
.one(self.db().read())
.await?
.flatten()
.unwrap_or(-1);
let now = OffsetDateTime::now_utc();
let row = room_bookmarks::ActiveModel {
id: Set(Uuid::now_v7()),
room_id: Set(room_id),
user_id: Set(caller),
title: Set(title),
url: Set(cmd.url),
emoji: Set(cmd.emoji),
position: Set(max_pos + 1),
created_at: Set(now),
updated_at: Set(now),
}
.insert(self.db().write())
.await?;
Ok(BookmarkDto::from(row))
}
pub async fn update_bookmark(
&self,
caller: i64,
room_id: Uuid,
bookmark_id: Uuid,
cmd: UpdateBookmarkCmd,
) -> Result<BookmarkDto, ChatError> {
let ctx = require_room_readable(self, room_id, caller).await?;
let row = room_bookmarks::Entity::find_by_id(bookmark_id)
.filter(room_bookmarks::Column::RoomId.eq(room_id))
.one(self.db().write())
.await?
.ok_or(ChatError::NotFound)?;
if row.user_id != caller && !self.bookmark_manager(&ctx, caller).await? {
return Err(ChatError::Forbidden);
}
let mut am: room_bookmarks::ActiveModel = row.into();
if let Some(title) = cmd.title {
let title = title.trim().to_string();
if title.is_empty() || title.chars().count() > self.config().chat.max_bookmark_title_len
{
return Err(ChatError::BadRequest("invalid bookmark title".to_string()));
}
am.title = Set(title);
}
if let Some(url) = cmd.url {
am.url = Set(Some(url));
}
if let Some(emoji) = cmd.emoji {
am.emoji = Set(Some(emoji));
}
if let Some(position) = cmd.position {
am.position = Set(position);
}
am.updated_at = Set(OffsetDateTime::now_utc());
let updated = am.update(self.db().write()).await?;
Ok(BookmarkDto::from(updated))
}
pub async fn delete_bookmark(
&self,
caller: i64,
room_id: Uuid,
bookmark_id: Uuid,
) -> Result<(), ChatError> {
let ctx = require_room_readable(self, room_id, caller).await?;
let row = room_bookmarks::Entity::find_by_id(bookmark_id)
.filter(room_bookmarks::Column::RoomId.eq(room_id))
.one(self.db().write())
.await?
.ok_or(ChatError::NotFound)?;
if row.user_id != caller && !self.bookmark_manager(&ctx, caller).await? {
return Err(ChatError::Forbidden);
}
room_bookmarks::Entity::delete_by_id(row.id)
.exec(self.db().write())
.await?;
Ok(())
}
async fn bookmark_manager(
&self,
ctx: &crate::share::lookup::RoomContext,
caller: i64,
) -> Result<bool, ChatError> {
match &ctx.space {
Some(space) => crate::share::authz::can_manage_space(self, space, caller).await,
None => Ok(false),
}
}
}