Skip to content
Clone

feat(chat): phase3c room roles (kick/ban/transfer), invites, drafts delete, webhooks/reminders patch, scheduled send_at

ZhenYi committed
parent 5cb60f0 commit b8f89bc Browse files

20 files changed

+959 −18

File tree 20
Modified lib/chat/src/handlers/drafts.rs View lib/chat/src/handlers/drafts.rs
@@ -73,3 +73,24 @@
7373 let drafts = service.list_my_drafts(uid).await?;
7474 Ok(AppResponseData::ok(drafts))
7575}
76
77#[utoipa::path(
78 delete,
79 path = "/chat/rooms/{room_id}/draft",
80 tag = "chat",
81 params(("room_id" = Uuid, Path), ("thread_id" = Option<Uuid>, Query, description = "thread scope")),
82 responses(
83 (status = 200, description = "draft deleted", body = AppResponseData<NoData>),
84 (status = 404, description = "no draft", body = AppResponseData<NoData>)
85 )
86)]
87pub async fn delete_draft(
88 State(service): State<ChatService>,
89 session: Session,
90 Path(room_id): Path<Uuid>,
91 Query(q): Query<DraftQuery>,
92) -> Result<AppResponseData<NoData>, ApiError> {
93 let uid = require_uid(&session)?;
94 service.delete_draft(uid, room_id, q.thread_id).await?;
95 Ok(AppResponseData::empty())
96}
Modified lib/chat/src/handlers/emojis.rs View lib/chat/src/handlers/emojis.rs
@@ -100,3 +100,23 @@
100100 let bytes = service.emoji_image(&space, emoji_id).await?;
101101 Ok((StatusCode::OK, bytes).into_response())
102102}
103
104#[utoipa::path(
105 get,
106 path = "/chat/spaces/{space_name}/emojis/{emoji_id}",
107 tag = "chat",
108 params(("space_name" = String, Path), ("emoji_id" = Uuid, Path)),
109 responses(
110 (status = 200, description = "emoji", body = AppResponseData<EmojiDto>),
111 (status = 404, description = "not found", body = AppResponseData<NoData>)
112 )
113)]
114pub async fn get_emoji(
115 State(service): State<ChatService>,
116 session: Session,
117 Path((space_name, emoji_id)): Path<(String, Uuid)>,
118) -> Result<AppResponseData<EmojiDto>, ApiError> {
119 let uid = require_uid(&session)?;
120 let dto = service.get_emoji(uid, &space_name, emoji_id).await?;
121 Ok(AppResponseData::ok(dto))
122}
Modified lib/chat/src/handlers/messages.rs View lib/chat/src/handlers/messages.rs
@@ -1,4 +1,5 @@
11use axum::extract::{Json, Path, Query, State};
2use axum::response::IntoResponse;
23use export::rest::{AppResponseData, NoData};
34use session::Session;
45use session::share::session::require_uid;
@@ -7,7 +8,7 @@
78use crate::errors::ApiError;
89use crate::services::messages::{
910 BulkDeleteCmd, ForwardMessageCmd, ListMessageQuery, MessageDto, MessageEditDto, MessagePage,
10 MessagePath, PinCmd, SendMessageCmd, UpdateMessageCmd,
11 MessagePath, PinCmd, ScheduledMessageDto, SendMessageCmd, UpdateMessageCmd,
1112};
1213use crate::services::service::ChatService;
1314
@@ -19,6 +20,7 @@
1920 request_body = SendMessageCmd,
2021 responses(
2122 (status = 200, description = "message sent", body = AppResponseData<MessageDto>),
23 (status = 202, description = "message scheduled", body = AppResponseData<ScheduledMessageDto>),
2224 (status = 400, description = "bad request", body = AppResponseData<NoData>),
2325 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
2426 )
@@ -28,10 +30,17 @@
2830 session: Session,
2931 Path(room_id): Path<Uuid>,
3032 Json(cmd): Json<SendMessageCmd>,
31) -> Result<AppResponseData<MessageDto>, ApiError> {
33) -> Result<axum::response::Response, ApiError> {
3234 let uid = require_uid(&session)?;
35 if cmd.send_at.is_some() {
36 let dto = service.schedule_message(uid, room_id, cmd).await?;
37 let mut envelope = AppResponseData::ok(dto);
38 envelope.code = 202;
39 envelope.message = "scheduled".to_string();
40 return Ok(envelope.into_response());
41 }
3342 let dto = service.send_message(uid, room_id, cmd).await?;
34 Ok(AppResponseData::ok(dto))
43 Ok(AppResponseData::ok(dto).into_response())
3544}
3645
3746#[utoipa::path(
Modified lib/chat/src/handlers/mod.rs View lib/chat/src/handlers/mod.rs
@@ -8,6 +8,7 @@
88pub mod reactions;
99pub mod read_state;
1010pub mod reminders;
11pub mod room_admin;
1112pub mod rooms;
1213pub mod search;
1314pub mod settings;
Modified lib/chat/src/handlers/reminders.rs View lib/chat/src/handlers/reminders.rs
@@ -5,7 +5,7 @@
55use uuid::Uuid;
66
77use crate::errors::ApiError;
8use crate::services::reminders::{CreateReminderCmd, ReminderDto};
8use crate::services::reminders::{CreateReminderCmd, ReminderDto, UpdateReminderCmd};
99use crate::services::service::ChatService;
1010
1111#[utoipa::path(
@@ -72,3 +72,25 @@
7272 service.cancel_reminder(uid, reminder_id).await?;
7373 Ok(AppResponseData::empty())
7474}
75
76#[utoipa::path(
77 patch,
78 path = "/chat/reminders/{reminder_id}",
79 tag = "chat",
80 params(("reminder_id" = Uuid, Path)),
81 request_body = UpdateReminderCmd,
82 responses(
83 (status = 200, description = "reminder updated", body = AppResponseData<ReminderDto>),
84 (status = 404, description = "not found", body = AppResponseData<NoData>)
85 )
86)]
87pub async fn update_reminder(
88 State(service): State<ChatService>,
89 session: Session,
90 Path(reminder_id): Path<Uuid>,
91 Json(cmd): Json<UpdateReminderCmd>,
92) -> Result<AppResponseData<ReminderDto>, ApiError> {
93 let uid = require_uid(&session)?;
94 let dto = service.update_reminder(uid, reminder_id, cmd).await?;
95 Ok(AppResponseData::ok(dto))
96}
Added lib/chat/src/handlers/room_admin.rs View lib/chat/src/handlers/room_admin.rs
@@ -0,0 +1,162 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5use uuid::Uuid;
6
7use crate::errors::ApiError;
8use crate::services::room_admin::{
9 BanRoomMemberCmd, RoomBanDto, UpdateRoomMemberCmd,
10};
11use crate::services::rooms::RoomMemberDto;
12use crate::services::service::ChatService;
13
14#[utoipa::path(
15 get,
16 path = "/chat/rooms/{room_id}/members/{user_id}",
17 tag = "chat",
18 params(("room_id" = Uuid, Path), ("user_id" = Uuid, Path)),
19 responses(
20 (status = 200, description = "room member", body = AppResponseData<RoomMemberDto>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn get_room_member(
25 State(service): State<ChatService>,
26 session: Session,
27 Path((room_id, user_id)): Path<(Uuid, Uuid)>,
28) -> Result<AppResponseData<RoomMemberDto>, ApiError> {
29 let uid = require_uid(&session)?;
30 let member = service.get_room_member(uid, room_id, user_id).await?;
31 Ok(AppResponseData::ok(member))
32}
33
34#[utoipa::path(
35 patch,
36 path = "/chat/rooms/{room_id}/members/{user_id}",
37 tag = "chat",
38 params(("room_id" = Uuid, Path), ("user_id" = Uuid, Path)),
39 request_body = UpdateRoomMemberCmd,
40 responses(
41 (status = 200, description = "room member updated", body = AppResponseData<RoomMemberDto>),
42 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
43 )
44)]
45pub async fn update_room_member(
46 State(service): State<ChatService>,
47 session: Session,
48 Path((room_id, user_id)): Path<(Uuid, Uuid)>,
49 Json(cmd): Json<UpdateRoomMemberCmd>,
50) -> Result<AppResponseData<RoomMemberDto>, ApiError> {
51 let uid = require_uid(&session)?;
52 let member = service.update_room_member(uid, room_id, user_id, cmd).await?;
53 Ok(AppResponseData::ok(member))
54}
55
56#[utoipa::path(
57 delete,
58 path = "/chat/rooms/{room_id}/members/{user_id}",
59 tag = "chat",
60 params(("room_id" = Uuid, Path), ("user_id" = Uuid, Path)),
61 responses(
62 (status = 200, description = "member removed", body = AppResponseData<NoData>),
63 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
64 )
65)]
66pub async fn kick_room_member(
67 State(service): State<ChatService>,
68 session: Session,
69 Path((room_id, user_id)): Path<(Uuid, Uuid)>,
70) -> Result<AppResponseData<NoData>, ApiError> {
71 let uid = require_uid(&session)?;
72 service.kick_room_member(uid, room_id, user_id).await?;
73 Ok(AppResponseData::empty())
74}
75
76#[utoipa::path(
77 get,
78 path = "/chat/rooms/{room_id}/bans",
79 tag = "chat",
80 params(("room_id" = Uuid, Path)),
81 responses(
82 (status = 200, description = "room bans", body = AppResponseData<Vec<RoomBanDto>>)
83 )
84)]
85pub async fn list_room_bans(
86 State(service): State<ChatService>,
87 session: Session,
88 Path(room_id): Path<Uuid>,
89) -> Result<AppResponseData<Vec<RoomBanDto>>, ApiError> {
90 let uid = require_uid(&session)?;
91 let bans = service.list_room_bans(uid, room_id).await?;
92 Ok(AppResponseData::ok(bans))
93}
94
95#[utoipa::path(
96 post,
97 path = "/chat/rooms/{room_id}/bans",
98 tag = "chat",
99 params(("room_id" = Uuid, Path)),
100 request_body = BanRoomMemberCmd,
101 responses(
102 (status = 200, description = "member banned", body = AppResponseData<RoomBanDto>),
103 (status = 409, description = "already banned", body = AppResponseData<NoData>)
104 )
105)]
106pub async fn ban_room_member(
107 State(service): State<ChatService>,
108 session: Session,
109 Path(room_id): Path<Uuid>,
110 Json(cmd): Json<BanRoomMemberCmd>,
111) -> Result<AppResponseData<RoomBanDto>, ApiError> {
112 let uid = require_uid(&session)?;
113 let ban = service.ban_room_member(uid, room_id, cmd).await?;
114 Ok(AppResponseData::ok(ban))
115}
116
117#[utoipa::path(
118 delete,
119 path = "/chat/rooms/{room_id}/bans/{user_id}",
120 tag = "chat",
121 params(("room_id" = Uuid, Path), ("user_id" = Uuid, Path)),
122 responses(
123 (status = 200, description = "member unbanned", body = AppResponseData<NoData>),
124 (status = 404, description = "not banned", body = AppResponseData<NoData>)
125 )
126)]
127pub async fn unban_room_member(
128 State(service): State<ChatService>,
129 session: Session,
130 Path((room_id, user_id)): Path<(Uuid, Uuid)>,
131) -> Result<AppResponseData<NoData>, ApiError> {
132 let uid = require_uid(&session)?;
133 service.unban_room_member(uid, room_id, user_id).await?;
134 Ok(AppResponseData::empty())
135}
136
137#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
138pub struct TransferOwnerCmd {
139 pub user_id: Uuid,
140}
141
142#[utoipa::path(
143 put,
144 path = "/chat/rooms/{room_id}/owner",
145 tag = "chat",
146 params(("room_id" = Uuid, Path)),
147 request_body = TransferOwnerCmd,
148 responses(
149 (status = 200, description = "ownership transferred", body = AppResponseData<NoData>),
150 (status = 403, description = "forbidden", body = AppResponseData<NoData>)
151 )
152)]
153pub async fn transfer_room_owner(
154 State(service): State<ChatService>,
155 session: Session,
156 Path(room_id): Path<Uuid>,
157 Json(cmd): Json<TransferOwnerCmd>,
158) -> Result<AppResponseData<NoData>, ApiError> {
159 let uid = require_uid(&session)?;
160 service.transfer_room_owner(uid, room_id, cmd.user_id).await?;
161 Ok(AppResponseData::empty())
162}
Modified lib/chat/src/handlers/rooms.rs View lib/chat/src/handlers/rooms.rs
@@ -7,7 +7,7 @@
77use crate::errors::ApiError;
88use crate::services::rooms::{
99 AcceptRoomInviteCmd, AddMembersCmd, CreateRoomInviteCmd, RoomBriefDto, RoomInviteDto,
10 RoomMemberDto, RoomMemberQuery, UpdateRoomCmd,
10 RoomInviteListItemDto, RoomMemberDto, RoomMemberQuery, UpdateRoomCmd,
1111};
1212use crate::services::service::ChatService;
1313
@@ -199,3 +199,42 @@
199199 let room = service.accept_room_invite(uid, &cmd.code).await?;
200200 Ok(AppResponseData::ok(room))
201201}
202
203#[utoipa::path(
204 get,
205 path = "/chat/rooms/{room_id}/invites",
206 tag = "chat",
207 params(("room_id" = Uuid, Path)),
208 responses(
209 (status = 200, description = "room invites", body = AppResponseData<Vec<RoomInviteListItemDto>>)
210 )
211)]
212pub async fn list_room_invites(
213 State(service): State<ChatService>,
214 session: Session,
215 Path(room_id): Path<Uuid>,
216) -> Result<AppResponseData<Vec<RoomInviteListItemDto>>, ApiError> {
217 let uid = require_uid(&session)?;
218 let invites = service.list_room_invites(uid, room_id).await?;
219 Ok(AppResponseData::ok(invites))
220}
221
222#[utoipa::path(
223 delete,
224 path = "/chat/rooms/{room_id}/invites/{invite_id}",
225 tag = "chat",
226 params(("room_id" = Uuid, Path), ("invite_id" = Uuid, Path)),
227 responses(
228 (status = 200, description = "invite revoked", body = AppResponseData<NoData>),
229 (status = 404, description = "not found", body = AppResponseData<NoData>)
230 )
231)]
232pub async fn revoke_room_invite(
233 State(service): State<ChatService>,
234 session: Session,
235 Path((room_id, invite_id)): Path<(Uuid, Uuid)>,
236) -> Result<AppResponseData<NoData>, ApiError> {
237 let uid = require_uid(&session)?;
238 service.revoke_room_invite(uid, room_id, invite_id).await?;
239 Ok(AppResponseData::empty())
240}
Modified lib/chat/src/handlers/webhooks.rs View lib/chat/src/handlers/webhooks.rs
@@ -7,7 +7,7 @@
77use crate::errors::ApiError;
88use crate::services::messages::MessageDto;
99use crate::services::service::ChatService;
10use crate::services::webhooks::{CreateWebhookCmd, WebhookDto, WebhookPostCmd, WebhookSecretDto};
10use crate::services::webhooks::{CreateWebhookCmd, UpdateWebhookCmd, WebhookDto, WebhookPostCmd, WebhookSecretDto};
1111
1212#[utoipa::path(
1313 post,
@@ -95,3 +95,25 @@
9595 let dto: MessageDto = serde_json::from_value(value)?;
9696 Ok(AppResponseData::ok(dto))
9797}
98
99#[utoipa::path(
100 patch,
101 path = "/chat/rooms/{room_id}/webhooks/{webhook_id}",
102 tag = "chat",
103 params(("room_id" = Uuid, Path), ("webhook_id" = Uuid, Path)),
104 request_body = UpdateWebhookCmd,
105 responses(
106 (status = 200, description = "webhook updated", body = AppResponseData<WebhookDto>),
107 (status = 404, description = "not found", body = AppResponseData<NoData>)
108 )
109)]
110pub async fn update_webhook(
111 State(service): State<ChatService>,
112 session: Session,
113 Path((room_id, webhook_id)): Path<(Uuid, Uuid)>,
114 Json(cmd): Json<UpdateWebhookCmd>,
115) -> Result<AppResponseData<WebhookDto>, ApiError> {
116 let uid = require_uid(&session)?;
117 let dto = service.update_webhook(uid, room_id, webhook_id, cmd).await?;
118 Ok(AppResponseData::ok(dto))
119}
Modified lib/chat/src/services/drafts.rs View lib/chat/src/services/drafts.rs
@@ -116,6 +116,26 @@
116116 }))
117117 }
118118
119 pub async fn delete_draft(
120 &self,
121 caller: Uuid,
122 room_id: Uuid,
123 thread_id: Option<Uuid>,
124 ) -> Result<(), ChatError> {
125 let _ctx = require_room_readable(self, room_id, caller).await?;
126 let mut q = user_drafts::Entity::delete_many()
127 .filter(user_drafts::Column::UserId.eq(caller))
128 .filter(user_drafts::Column::RoomId.eq(room_id));
129 if let Some(tid) = thread_id {
130 q = q.filter(user_drafts::Column::ThreadId.eq(tid));
131 }
132 let deleted = q.exec(self.db().write()).await?;
133 if deleted.rows_affected == 0 {
134 return Err(ChatError::NotFound);
135 }
136 Ok(())
137 }
138
119139 pub async fn list_my_drafts(&self, caller: Uuid) -> Result<Vec<DraftDto>, ChatError> {
120140 let rows = user_drafts::Entity::find()
121141 .filter(user_drafts::Column::UserId.eq(caller))
Modified lib/chat/src/services/emojis.rs View lib/chat/src/services/emojis.rs
@@ -35,6 +35,29 @@
3535const MAX_EMOJI_BYTES: usize = 512 * 1024;
3636
3737impl ChatService {
38 pub async fn get_emoji(
39 &self,
40 caller: Uuid,
41 space_name: &str,
42 emoji_id: Uuid,
43 ) -> Result<EmojiDto, ChatError> {
44 let space = lookup_space_by_name(self, space_name).await?;
45 require_space_member(self, &space, caller).await?;
46 let e = emojis::Entity::find_by_id(emoji_id)
47 .filter(emojis::Column::SpaceId.eq(space.id))
48 .one(self.db().read())
49 .await?
50 .ok_or(ChatError::NotFound)?;
51 Ok(EmojiDto {
52 id: e.id,
53 space_id: e.space_id,
54 name: e.name,
55 animated: e.animated,
56 disabled: e.disabled,
57 created_at: e.created_at,
58 })
59 }
60
3861 pub async fn list_emojis(
3962 &self,
4063 caller: Uuid,
Modified lib/chat/src/services/messages.rs View lib/chat/src/services/messages.rs
@@ -70,7 +70,7 @@
7070 pub has_more: bool,
7171}
7272
73#[derive(Debug, Clone, Deserialize, ToSchema)]
73#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
7474pub struct SendMessageCmd {
7575 #[serde(default)]
7676 pub content: String,
@@ -90,8 +90,18 @@
9090 pub urgent: bool,
9191 #[serde(default)]
9292 pub forwarded_from: Option<Uuid>,
93 #[serde(default)]
94 pub send_at: Option<OffsetDateTime>,
9395}
9496
97#[derive(Debug, Clone, Serialize, ToSchema)]
98pub struct ScheduledMessageDto {
99 pub id: Uuid,
100 pub room_id: Uuid,
101 pub scheduled_at: OffsetDateTime,
102 pub status: models::users::user_scheduled_messages::ScheduledMessageStatus,
103}
104
95105#[derive(Debug, Clone, Deserialize, ToSchema)]
96106pub struct UpdateMessageCmd {
97107 pub content: String,
@@ -256,6 +266,42 @@
256266 Ok(())
257267 }
258268
269 pub async fn schedule_message(
270 &self,
271 caller: Uuid,
272 room_id: Uuid,
273 mut cmd: SendMessageCmd,
274 ) -> Result<ScheduledMessageDto, ChatError> {
275 let Some(send_at) = cmd.send_at else {
276 return Err(ChatError::BadRequest("send_at is required".to_string()));
277 };
278 if send_at <= OffsetDateTime::now_utc() {
279 return Err(ChatError::BadRequest("send_at must be in the future".to_string()));
280 }
281 require_room_writable(self, room_id, caller).await?;
282 cmd.send_at = None;
283 let payload = serde_json::to_value(&cmd)?;
284 let id = Uuid::now_v7();
285 let now = OffsetDateTime::now_utc();
286 let row = models::users::user_scheduled_messages::ActiveModel {
287 id: Set(id),
288 user_id: Set(caller),
289 target_channel_id: Set(room_id),
290 payload: Set(payload),
291 scheduled_at: Set(send_at),
292 status: Set(models::users::user_scheduled_messages::ScheduledMessageStatus::Pending),
293 created_at: Set(now),
294 }
295 .insert(self.db().write())
296 .await?;
297 Ok(ScheduledMessageDto {
298 id: row.id,
299 room_id,
300 scheduled_at: row.scheduled_at,
301 status: row.status,
302 })
303 }
304
259305 pub async fn send_message(
260306 &self,
261307 caller: Uuid,
@@ -970,6 +1016,7 @@
9701016 nonce: None,
9711017 urgent: false,
9721018 forwarded_from: Some(original.id),
1019 send_at: None,
9731020 };
9741021 let dto = self
9751022 .send_message(caller, cmd.target_room_id, send_cmd)
Modified lib/chat/src/services/mod.rs View lib/chat/src/services/mod.rs
@@ -8,6 +8,7 @@
88pub mod reactions;
99pub mod read_state;
1010pub mod reminders;
11pub mod room_admin;
1112pub mod rooms;
1213pub mod search;
1314pub mod service;
Modified lib/chat/src/services/reminders.rs View lib/chat/src/services/reminders.rs
@@ -33,6 +33,14 @@
3333 pub note: Option<String>,
3434}
3535
36#[derive(Debug, Clone, Deserialize, ToSchema)]
37pub struct UpdateReminderCmd {
38 #[serde(default)]
39 pub remind_at: Option<OffsetDateTime>,
40 #[serde(default)]
41 pub note: Option<Option<String>>,
42}
43
3644impl From<message_reminders::Model> for ReminderDto {
3745 fn from(r: message_reminders::Model) -> Self {
3846 Self {
@@ -92,6 +100,36 @@
92100 Ok(rows.into_iter().map(ReminderDto::from).collect())
93101 }
94102
103 pub async fn update_reminder(
104 &self,
105 caller: Uuid,
106 reminder_id: Uuid,
107 cmd: UpdateReminderCmd,
108 ) -> Result<ReminderDto, ChatError> {
109 let row = message_reminders::Entity::find_by_id(reminder_id)
110 .filter(message_reminders::Column::UserId.eq(caller))
111 .one(self.db().write())
112 .await?
113 .ok_or(ChatError::NotFound)?;
114 if row.status != ReminderStatus::Pending {
115 return Err(ChatError::Conflict("reminder not pending".to_string()));
116 }
117 if let Some(remind_at) = cmd.remind_at
118 && remind_at <= OffsetDateTime::now_utc()
119 {
120 return Err(ChatError::BadRequest("remind_at must be in the future".to_string()));
121 }
122 let mut am: message_reminders::ActiveModel = row.into();
123 if let Some(remind_at) = cmd.remind_at {
124 am.remind_at = Set(remind_at);
125 }
126 if let Some(note) = cmd.note {
127 am.note = Set(note.filter(|n| !n.trim().is_empty()));
128 }
129 let updated = am.update(self.db().write()).await?;
130 Ok(ReminderDto::from(updated))
131 }
132
95133 pub async fn cancel_reminder(&self, caller: Uuid, reminder_id: Uuid) -> Result<(), ChatError> {
96134 let row = message_reminders::Entity::find_by_id(reminder_id)
97135 .filter(message_reminders::Column::UserId.eq(caller))
Added lib/chat/src/services/room_admin.rs View lib/chat/src/services/room_admin.rs
@@ -0,0 +1,312 @@
1use models::room::room_bans;
2use models::room::room_members::{self, RoomMemberRole};
3use models::room::rooms;
4use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set};
5use serde::{Deserialize, Serialize};
6use space::services::spaces::UserBriefDto;
7use time::OffsetDateTime;
8use utoipa::ToSchema;
9use uuid::Uuid;
10
11use crate::errors::ChatError;
12use crate::services::rooms::RoomMemberDto;
13use crate::services::service::ChatService;
14use crate::share::lookup::lookup_room;
15use crate::share::realtime::RoomEvent;
16
17#[derive(Debug, Clone, Serialize, ToSchema)]
18pub struct RoomBanDto {
19 pub room_id: Uuid,
20 pub user: Option<UserBriefDto>,
21 pub reason: Option<String>,
22 pub banned_by: Option<UserBriefDto>,
23 pub expires_at: Option<OffsetDateTime>,
24 pub created_at: OffsetDateTime,
25}
26
27#[derive(Debug, Clone, Deserialize, ToSchema)]
28pub struct BanRoomMemberCmd {
29 pub user_id: Uuid,
30 #[serde(default)]
31 pub reason: Option<String>,
32 #[serde(default)]
33 pub expires_at: Option<OffsetDateTime>,
34}
35
36#[derive(Debug, Clone, Deserialize, ToSchema)]
37pub struct UpdateRoomMemberCmd {
38 #[serde(default)]
39 pub nick: Option<Option<String>>,
40 #[serde(default)]
41 pub role: Option<RoomMemberRole>,
42}
43
44pub async fn active_room_role(
45 service: &ChatService,
46 room_id: Uuid,
47 uid: Uuid,
48) -> Result<Option<RoomMemberRole>, ChatError> {
49 Ok(room_members::Entity::find_by_id((room_id, uid))
50 .one(service.db().read())
51 .await?
52 .filter(|m| m.left_at.is_none())
53 .map(|m| m.role))
54}
55
56async fn require_room_role(
57 service: &ChatService,
58 room_id: Uuid,
59 uid: Uuid,
60 min: RoomMemberRole,
61) -> Result<RoomMemberRole, ChatError> {
62 let Some(role) = active_room_role(service, room_id, uid).await? else {
63 return Err(ChatError::Forbidden);
64 };
65 if role < min {
66 return Err(ChatError::Forbidden);
67 }
68 Ok(role)
69}
70
71async fn set_left_at(service: &ChatService, room_id: Uuid, uid: Uuid) -> Result<(), ChatError> {
72 let Some(row) = room_members::Entity::find_by_id((room_id, uid))
73 .one(service.db().write())
74 .await?
75 else {
76 return Ok(());
77 };
78 if row.left_at.is_none() {
79 let mut am: room_members::ActiveModel = row.into();
80 am.left_at = Set(Some(OffsetDateTime::now_utc()));
81 am.updated_at = Set(Some(OffsetDateTime::now_utc()));
82 am.update(service.db().write()).await?;
83 }
84 Ok(())
85}
86
87impl ChatService {
88 async fn build_room_ban_dto(&self, row: room_bans::Model) -> Result<RoomBanDto, ChatError> {
89 let user_map = self.load_user_briefs(&[row.user_id, row.banned_by]).await?;
90 Ok(RoomBanDto {
91 room_id: row.room_id,
92 user: user_map.get(&row.user_id).cloned(),
93 reason: row.reason,
94 banned_by: user_map.get(&row.banned_by).cloned(),
95 expires_at: row.expires_at,
96 created_at: row.created_at,
97 })
98 }
99
100 pub async fn get_room_member(
101 &self,
102 caller: Uuid,
103 room_id: Uuid,
104 uid: Uuid,
105 ) -> Result<RoomMemberDto, ChatError> {
106 crate::share::lookup::require_room_readable(self, room_id, caller).await?;
107 let Some(row) = room_members::Entity::find_by_id((room_id, uid))
108 .one(self.db().read())
109 .await?
110 else {
111 return Err(ChatError::NotFound);
112 };
113 let user_map = self.load_user_briefs(&[row.user_id]).await?;
114 let user = user_map.get(&row.user_id).cloned().ok_or(ChatError::NotFound)?;
115 Ok(RoomMemberDto {
116 user,
117 role: row.role,
118 nick: row.nick,
119 joined_at: row.joined_at,
120 })
121 }
122
123 pub async fn kick_room_member(
124 &self,
125 caller: Uuid,
126 room_id: Uuid,
127 uid: Uuid,
128 ) -> Result<(), ChatError> {
129 let room = lookup_room(self, room_id).await?;
130 if caller == uid {
131 return Err(ChatError::BadRequest("use leave endpoint instead".to_string()));
132 }
133 let caller_role = require_room_role(self, room_id, caller, RoomMemberRole::Moderator).await?;
134 let target_role = active_room_role(self, room_id, uid)
135 .await?
136 .ok_or(ChatError::NotFound)?;
137 if uid == room.owner_id || target_role >= caller_role {
138 return Err(ChatError::Forbidden);
139 }
140 set_left_at(self, room_id, uid).await?;
141 self.realtime
142 .publish_room(&RoomEvent::MemberLeft { room_id, user_id: uid })
143 .await;
144 Ok(())
145 }
146
147 pub async fn ban_room_member(
148 &self,
149 caller: Uuid,
150 room_id: Uuid,
151 cmd: BanRoomMemberCmd,
152 ) -> Result<RoomBanDto, ChatError> {
153 let room = lookup_room(self, room_id).await?;
154 require_room_role(self, room_id, caller, RoomMemberRole::Moderator).await?;
155 if cmd.user_id == room.owner_id {
156 return Err(ChatError::Forbidden);
157 }
158 if let Some(target_role) = active_room_role(self, room_id, cmd.user_id).await?
159 && target_role >= RoomMemberRole::Moderator
160 {
161 return Err(ChatError::Forbidden);
162 }
163 let existing = room_bans::Entity::find_by_id((room_id, cmd.user_id))
164 .one(self.db().read())
165 .await?;
166 if existing.is_some() {
167 return Err(ChatError::Conflict("user already banned".to_string()));
168 }
169 let now = OffsetDateTime::now_utc();
170 let row = room_bans::ActiveModel {
171 room_id: Set(room_id),
172 user_id: Set(cmd.user_id),
173 reason: Set(cmd.reason),
174 banned_by: Set(caller),
175 expires_at: Set(cmd.expires_at),
176 created_at: Set(now),
177 updated_at: Set(Some(now)),
178 }
179 .insert(self.db().write())
180 .await?;
181 set_left_at(self, room_id, cmd.user_id).await?;
182 self.realtime
183 .publish_room(&RoomEvent::MemberLeft { room_id, user_id: cmd.user_id })
184 .await;
185 self.build_room_ban_dto(row).await
186 }
187
188 pub async fn unban_room_member(
189 &self,
190 caller: Uuid,
191 room_id: Uuid,
192 uid: Uuid,
193 ) -> Result<(), ChatError> {
194 require_room_role(self, room_id, caller, RoomMemberRole::Moderator).await?;
195 let deleted = room_bans::Entity::delete_by_id((room_id, uid))
196 .exec(self.db().write())
197 .await?;
198 if deleted.rows_affected == 0 {
199 return Err(ChatError::NotFound);
200 }
201 Ok(())
202 }
203
204 pub async fn list_room_bans(&self, caller: Uuid, room_id: Uuid) -> Result<Vec<RoomBanDto>, ChatError> {
205 require_room_role(self, room_id, caller, RoomMemberRole::Moderator).await?;
206 let rows = room_bans::Entity::find()
207 .filter(room_bans::Column::RoomId.eq(room_id))
208 .order_by_desc(room_bans::Column::CreatedAt)
209 .all(self.db().read())
210 .await?;
211 let mut out = Vec::with_capacity(rows.len());
212 for row in rows {
213 out.push(self.build_room_ban_dto(row).await?);
214 }
215 Ok(out)
216 }
217
218 pub async fn transfer_room_owner(
219 &self,
220 caller: Uuid,
221 room_id: Uuid,
222 new_owner: Uuid,
223 ) -> Result<(), ChatError> {
224 let room = lookup_room(self, room_id).await?;
225 if room.owner_id != caller {
226 return Err(ChatError::Forbidden);
227 }
228 if new_owner == caller {
229 return Err(ChatError::BadRequest("cannot transfer ownership to yourself".to_string()));
230 }
231 let target = room_members::Entity::find_by_id((room_id, new_owner))
232 .one(self.db().read())
233 .await?
234 .filter(|m| m.left_at.is_none())
235 .ok_or(ChatError::NotFound)?;
236 let now = OffsetDateTime::now_utc();
237 let mut owner_am = rooms::ActiveModel::from(room);
238 owner_am.owner_id = Set(new_owner);
239 owner_am.updated_at = Set(now);
240 owner_am.update(self.db().write()).await?;
241
242 let mut new_owner_am: room_members::ActiveModel = target.into();
243 new_owner_am.role = Set(RoomMemberRole::Owner);
244 new_owner_am.updated_at = Set(Some(now));
245 new_owner_am.update(self.db().write()).await?;
246
247 if let Some(old) = room_members::Entity::find_by_id((room_id, caller))
248 .one(self.db().write())
249 .await?
250 && old.left_at.is_none()
251 {
252 let mut am: room_members::ActiveModel = old.into();
253 am.role = Set(RoomMemberRole::Admin);
254 am.updated_at = Set(Some(now));
255 am.update(self.db().write()).await?;
256 }
257 Ok(())
258 }
259
260 pub async fn update_room_member(
261 &self,
262 caller: Uuid,
263 room_id: Uuid,
264 uid: Uuid,
265 cmd: UpdateRoomMemberCmd,
266 ) -> Result<RoomMemberDto, ChatError> {
267 let row = room_members::Entity::find_by_id((room_id, uid))
268 .one(self.db().write())
269 .await?
270 .ok_or(ChatError::NotFound)?;
271 let is_self = caller == uid;
272 if !is_self {
273 require_room_role(self, room_id, caller, RoomMemberRole::Admin).await?;
274 }
275 if let Some(new_role) = cmd.role {
276 if is_self || new_role >= RoomMemberRole::Owner || row.role == RoomMemberRole::Owner {
277 return Err(ChatError::Forbidden);
278 }
279 if let Some(caller_role) = active_room_role(self, room_id, caller).await?
280 && new_role >= caller_role
281 {
282 return Err(ChatError::Forbidden);
283 }
284 }
285 let mut am: room_members::ActiveModel = row.into();
286 if let Some(nick) = cmd.nick {
287 let nick = nick.map(|n| n.trim().to_string()).filter(|n| !n.is_empty());
288 if let Some(n) = &nick
289 && n.chars().count() > 64
290 {
291 return Err(ChatError::BadRequest("nick too long".to_string()));
292 }
293 am.nick = Set(nick);
294 }
295 if let Some(role) = cmd.role {
296 am.role = Set(role);
297 }
298 am.updated_at = Set(Some(OffsetDateTime::now_utc()));
299 let updated = am.update(self.db().write()).await?;
300 let user_map = self.load_user_briefs(&[updated.user_id]).await?;
301 let user = user_map
302 .get(&updated.user_id)
303 .cloned()
304 .ok_or(ChatError::NotFound)?;
305 Ok(RoomMemberDto {
306 user,
307 role: updated.role,
308 nick: updated.nick,
309 joined_at: updated.joined_at,
310 })
311 }
312}
Modified lib/chat/src/services/rooms.rs View lib/chat/src/services/rooms.rs
@@ -109,6 +109,18 @@
109109 pub expires_at: Option<OffsetDateTime>,
110110}
111111
112#[derive(Debug, Clone, Serialize, ToSchema)]
113pub struct RoomInviteListItemDto {
114 pub id: Uuid,
115 pub room_id: Uuid,
116 pub inviter_id: Uuid,
117 pub invitee_user_id: Option<Uuid>,
118 pub created_at: OffsetDateTime,
119 pub expires_at: Option<OffsetDateTime>,
120 pub accepted_at: Option<OffsetDateTime>,
121 pub revoked_at: Option<OffsetDateTime>,
122}
123
112124fn random_accept_code() -> String {
113125 const CHARSET: &[u8] = b"abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789";
114126 let mut rng = rand::thread_rng();
@@ -492,10 +504,35 @@
492504 if room.space_id.is_none() {
493505 return Err(ChatError::BadRequest("use DM close instead".to_string()));
494506 }
507 let now = OffsetDateTime::now_utc();
495508 if room.owner_id == caller {
496 return Err(ChatError::Forbidden);
509 let successor = room_members::Entity::find()
510 .filter(room_members::Column::RoomId.eq(room_id))
511 .filter(room_members::Column::UserId.ne(caller))
512 .filter(room_members::Column::LeftAt.is_null())
513 .order_by_asc(room_members::Column::JoinedAt)
514 .one(self.db().write())
515 .await?;
516 match successor {
517 Some(next) => {
518 let next_uid = next.user_id;
519 let mut am: room_members::ActiveModel = next.into();
520 am.role = Set(RoomMemberRole::Owner);
521 am.updated_at = Set(Some(now));
522 am.update(self.db().write()).await?;
523 let mut room_am: rooms::ActiveModel = room.clone().into();
524 room_am.owner_id = Set(next_uid);
525 room_am.updated_at = Set(now);
526 room_am.update(self.db().write()).await?;
527 }
528 None => {
529 let mut room_am: rooms::ActiveModel = room.clone().into();
530 room_am.is_active = Set(false);
531 room_am.updated_at = Set(now);
532 room_am.update(self.db().write()).await?;
533 }
534 }
497535 }
498 let now = OffsetDateTime::now_utc();
499536 let row = room_members::Entity::find_by_id((room_id, caller))
500537 .one(self.db().write())
501538 .await?
@@ -716,6 +753,65 @@
716753 })
717754 }
718755
756 pub async fn list_room_invites(
757 &self,
758 caller: Uuid,
759 room_id: Uuid,
760 ) -> Result<Vec<RoomInviteListItemDto>, ChatError> {
761 require_room_readable(self, room_id, caller).await?;
762 let rows = room_invites::Entity::find()
763 .filter(room_invites::Column::RoomId.eq(room_id))
764 .order_by_desc(room_invites::Column::CreatedAt)
765 .all(self.db().read())
766 .await?;
767 Ok(rows
768 .into_iter()
769 .map(|r| RoomInviteListItemDto {
770 id: r.id,
771 room_id: r.room_id,
772 inviter_id: r.inviter_id,
773 invitee_user_id: r.invitee_user_id,
774 created_at: r.created_at,
775 expires_at: r.expires_at,
776 accepted_at: r.accepted_at,
777 revoked_at: r.revoked_at,
778 })
779 .collect())
780 }
781
782 pub async fn revoke_room_invite(
783 &self,
784 caller: Uuid,
785 room_id: Uuid,
786 invite_id: Uuid,
787 ) -> Result<(), ChatError> {
788 let ctx = require_room_readable(self, room_id, caller).await?;
789 let row = room_invites::Entity::find_by_id(invite_id)
790 .filter(room_invites::Column::RoomId.eq(room_id))
791 .one(self.db().write())
792 .await?
793 .ok_or(ChatError::NotFound)?;
794 if row.accepted_at.is_some() {
795 return Err(ChatError::Conflict("invite already accepted".to_string()));
796 }
797 if row.inviter_id != caller {
798 match &ctx.space {
799 Some(space) => {
800 if !can_manage_space(self, space, caller).await? {
801 return Err(ChatError::Forbidden);
802 }
803 }
804 None => return Err(ChatError::Forbidden),
805 }
806 }
807 if row.revoked_at.is_none() {
808 let mut am: room_invites::ActiveModel = row.into();
809 am.revoked_at = Set(Some(OffsetDateTime::now_utc()));
810 am.update(self.db().write()).await?;
811 }
812 Ok(())
813 }
814
719815 pub async fn accept_room_invite(
720816 &self,
721817 caller: Uuid,
Modified lib/chat/src/services/webhooks.rs View lib/chat/src/services/webhooks.rs
@@ -11,7 +11,7 @@
1111use crate::services::messages::SendMessageCmd;
1212use crate::services::service::ChatService;
1313use crate::share::authz::can_manage_space;
14use crate::share::lookup::{lookup_room, require_room_readable};
14use crate::share::lookup::{is_room_member, lookup_room, require_room_readable};
1515
1616#[derive(Debug, Clone, Serialize, ToSchema)]
1717pub struct WebhookDto {
@@ -39,6 +39,14 @@
3939}
4040
4141#[derive(Debug, Clone, Deserialize, ToSchema)]
42pub struct UpdateWebhookCmd {
43 #[serde(default)]
44 pub name: Option<String>,
45 #[serde(default)]
46 pub disabled: Option<bool>,
47}
48
49#[derive(Debug, Clone, Deserialize, ToSchema)]
4250pub struct WebhookPostCmd {
4351 #[serde(default)]
4452 pub content: String,
@@ -162,6 +170,46 @@
162170 Ok(rows.into_iter().map(WebhookDto::from).collect())
163171 }
164172
173 pub async fn update_webhook(
174 &self,
175 caller: Uuid,
176 room_id: Uuid,
177 webhook_id: Uuid,
178 cmd: UpdateWebhookCmd,
179 ) -> Result<WebhookDto, ChatError> {
180 let ctx = require_room_readable(self, room_id, caller).await?;
181 match &ctx.space {
182 Some(space) => {
183 if !can_manage_space(self, space, caller).await? {
184 return Err(ChatError::Forbidden);
185 }
186 }
187 None => {
188 if !is_room_member(self, room_id, caller).await? {
189 return Err(ChatError::Forbidden);
190 }
191 }
192 }
193 let row = room_webhooks::Entity::find_by_id(webhook_id)
194 .one(self.db().write())
195 .await?
196 .filter(|w| w.room_id == room_id)
197 .ok_or(ChatError::NotFound)?;
198 let mut am: room_webhooks::ActiveModel = row.into();
199 if let Some(name) = cmd.name {
200 let name = name.trim();
201 if name.is_empty() || name.chars().count() > self.config().chat.max_webhook_name_len {
202 return Err(ChatError::BadRequest("invalid webhook name".to_string()));
203 }
204 am.name = Set(name.to_string());
205 }
206 if let Some(disabled) = cmd.disabled {
207 am.disabled = Set(disabled);
208 }
209 let updated = am.update(self.db().write()).await?;
210 Ok(WebhookDto::from(updated))
211 }
212
165213 pub async fn delete_webhook(
166214 &self,
167215 caller: Uuid,
@@ -239,6 +287,7 @@
239287 nonce: cmd.nonce,
240288 urgent: false,
241289 forwarded_from: None,
290 send_at: None,
242291 },
243292 )
244293 .await?;
Modified lib/chat/src/share/lookup.rs View lib/chat/src/share/lookup.rs
@@ -1,3 +1,4 @@
1use models::room::room_bans;
12use models::room::room_members;
23use models::room::rooms::{self, RoomKind};
34use models::space::space_members::{self, MemberStatus};
@@ -137,6 +138,15 @@
137138 uid: Uuid,
138139) -> Result<RoomContext, ChatError> {
139140 let ctx = lookup_room_context(service, room_id).await?;
141 if let Some(ban) = room_bans::Entity::find_by_id((room_id, uid))
142 .one(service.db().read())
143 .await?
144 {
145 let now = time::OffsetDateTime::now_utc();
146 if ban.expires_at.is_none() || ban.expires_at > Some(now) {
147 return Err(ChatError::Forbidden);
148 }
149 }
140150 match &ctx.space {
141151 Some(space) => {
142152 require_space_member(service, space, uid).await?;
Modified lib/chat/src/openapi.rs View lib/chat/src/openapi.rs
@@ -14,6 +14,15 @@
1414 crate::handlers::rooms::add_room_members,
1515 crate::handlers::rooms::list_room_members,
1616 crate::handlers::rooms::create_room_invite,
17 crate::handlers::rooms::list_room_invites,
18 crate::handlers::rooms::revoke_room_invite,
19 crate::handlers::room_admin::get_room_member,
20 crate::handlers::room_admin::update_room_member,
21 crate::handlers::room_admin::kick_room_member,
22 crate::handlers::room_admin::list_room_bans,
23 crate::handlers::room_admin::ban_room_member,
24 crate::handlers::room_admin::unban_room_member,
25 crate::handlers::room_admin::transfer_room_owner,
1726 crate::handlers::rooms::accept_room_invite,
1827 crate::handlers::messages::send_message,
1928 crate::handlers::messages::list_messages,
@@ -49,6 +58,7 @@
4958 crate::handlers::emojis::list_emojis,
5059 crate::handlers::emojis::upload_emoji,
5160 crate::handlers::emojis::delete_emoji,
61 crate::handlers::emojis::get_emoji,
5262 crate::handlers::emojis::emoji_image,
5363 crate::handlers::bookmarks::list_bookmarks,
5464 crate::handlers::bookmarks::create_bookmark,
@@ -57,7 +67,9 @@
5767 crate::handlers::reminders::create_reminder,
5868 crate::handlers::reminders::list_reminders,
5969 crate::handlers::reminders::cancel_reminder,
70 crate::handlers::reminders::update_reminder,
6071 crate::handlers::drafts::upsert_draft,
72 crate::handlers::drafts::delete_draft,
6173 crate::handlers::drafts::get_draft,
6274 crate::handlers::drafts::list_my_drafts,
6375 crate::handlers::presence::set_presence,
@@ -69,6 +81,7 @@
6981 crate::handlers::webhooks::create_webhook,
7082 crate::handlers::webhooks::list_webhooks,
7183 crate::handlers::webhooks::delete_webhook,
84 crate::handlers::webhooks::update_webhook,
7285 crate::handlers::webhooks::post_via_webhook,
7386 crate::handlers::ws::issue_ws_ticket,
7487 ),
@@ -84,10 +97,13 @@
8497 models::chat::presence_states::PresenceStatus,
8598 models::chat::message_reminders::ReminderStatus,
8699 models::room::rooms::RoomKind,
100 models::room::room_members::RoomMemberRole,
101 models::users::user_scheduled_messages::ScheduledMessageStatus,
87102 crate::services::messages::MessageDto,
88103 crate::services::messages::MessagePage,
89104 crate::services::messages::MessageEditDto,
90105 crate::services::messages::SendMessageCmd,
106 crate::services::messages::ScheduledMessageDto,
91107 crate::services::messages::UpdateMessageCmd,
92108 crate::services::messages::BulkDeleteCmd,
93109 crate::services::messages::PinCmd,
@@ -98,6 +114,10 @@
98114 crate::services::rooms::UpdateRoomCmd,
99115 crate::services::rooms::AddMembersCmd,
100116 crate::services::rooms::RoomMemberDto,
117 crate::services::rooms::RoomInviteListItemDto,
118 crate::services::room_admin::RoomBanDto,
119 crate::services::room_admin::BanRoomMemberCmd,
120 crate::services::room_admin::UpdateRoomMemberCmd,
101121 crate::services::rooms::RoomMemberQuery,
102122 crate::services::rooms::CreateRoomInviteCmd,
103123 crate::services::rooms::RoomInviteDto,
@@ -127,6 +147,7 @@
127147 crate::services::bookmarks::UpdateBookmarkCmd,
128148 crate::services::reminders::ReminderDto,
129149 crate::services::reminders::CreateReminderCmd,
150 crate::services::reminders::UpdateReminderCmd,
130151 crate::services::drafts::DraftDto,
131152 crate::services::drafts::UpsertDraftCmd,
132153 crate::handlers::drafts::DraftQuery,
@@ -137,6 +158,7 @@
137158 crate::services::search::SearchQuery,
138159 crate::services::settings::RoomSettingDto,
139160 crate::services::settings::UpdateRoomSettingCmd,
161 crate::services::webhooks::UpdateWebhookCmd,
140162 crate::services::webhooks::WebhookDto,
141163 crate::services::webhooks::WebhookSecretDto,
142164 crate::services::webhooks::CreateWebhookCmd,
Modified lib/chat/src/routes.rs View lib/chat/src/routes.rs
@@ -1,5 +1,5 @@
11use axum::Router;
2use axum::routing::{get, post};
2use axum::routing::{delete, get, post, put};
33
44use crate::handlers::attachments::{
55 create_attachment, download_attachment, finalize_attachment, get_attachment,
@@ -16,6 +16,7 @@
1616};
1717use crate::handlers::presence::{get_presence, list_space_presences, set_presence};
1818use crate::handlers::reactions::{add_reaction, list_reactions, remove_reaction};
19use crate::handlers::room_admin;
1920use crate::handlers::read_state::{
2021 get_read_state, list_space_read_states, mark_read, mark_thread_read,
2122};
@@ -47,7 +48,7 @@
4748 )
4849 .route(
4950 "/chat/rooms/{room_id}",
50 axum::routing::delete(space::handlers::delete_room::delete_room),
51 delete(space::handlers::delete_room::delete_room),
5152 )
5253 .with_state(space_service)
5354 .route_layer(axum::middleware::from_fn_with_state(
@@ -66,7 +67,29 @@
6667 "/chat/rooms/{room_id}/members",
6768 get(list_room_members).post(add_room_members),
6869 )
69 .route("/chat/rooms/{room_id}/invites", post(create_room_invite))
70 .route(
71 "/chat/rooms/{room_id}/members/{user_id}",
72 get(crate::handlers::room_admin::get_room_member)
73 .patch(crate::handlers::room_admin::update_room_member)
74 .delete(crate::handlers::room_admin::kick_room_member),
75 )
76 .route(
77 "/chat/rooms/{room_id}/bans",
78 get(crate::handlers::room_admin::list_room_bans).post(crate::handlers::room_admin::ban_room_member),
79 )
80 .route(
81 "/chat/rooms/{room_id}/bans/{user_id}",
82 delete(crate::handlers::room_admin::unban_room_member),
83 )
84 .route("/chat/rooms/{room_id}/owner", put(crate::handlers::room_admin::transfer_room_owner))
85 .route(
86 "/chat/rooms/{room_id}/invites",
87 post(create_room_invite).get(crate::handlers::rooms::list_room_invites),
88 )
89 .route(
90 "/chat/rooms/{room_id}/invites/{invite_id}",
91 delete(crate::handlers::rooms::revoke_room_invite),
92 )
7093 .route("/chat/invites/accept", post(accept_room_invite))
7194 .route(
7295 "/chat/rooms/{room_id}/messages",
@@ -128,7 +151,7 @@
128151 .route("/chat/reminders", get(list_reminders))
129152 .route(
130153 "/chat/reminders/{reminder_id}",
131 axum::routing::delete(cancel_reminder),
154 axum::routing::delete(cancel_reminder).patch(crate::handlers::reminders::update_reminder),
132155 )
133156 .route(
134157 "/chat/rooms/{room_id}/read-state",
@@ -156,7 +179,7 @@
156179 )
157180 .route(
158181 "/chat/spaces/{space_name}/emojis/{emoji_id}",
159 axum::routing::delete(delete_emoji),
182 get(crate::handlers::emojis::get_emoji).delete(delete_emoji),
160183 )
161184 .route(
162185 "/chat/spaces/{space_name}/emojis/{emoji_id}/image",
@@ -172,7 +195,7 @@
172195 )
173196 .route(
174197 "/chat/rooms/{room_id}/draft",
175 get(get_draft).put(upsert_draft),
198 get(get_draft).put(upsert_draft).delete(crate::handlers::drafts::delete_draft),
176199 )
177200 .route("/chat/drafts", get(list_my_drafts))
178201 .route("/chat/presence", axum::routing::put(set_presence))
@@ -195,7 +218,7 @@
195218 )
196219 .route(
197220 "/chat/rooms/{room_id}/webhooks/{webhook_id}",
198 axum::routing::delete(delete_webhook),
221 axum::routing::delete(delete_webhook).patch(crate::handlers::webhooks::update_webhook),
199222 )
200223 .route("/chat/ws/ticket", post(issue_ws_ticket))
201224 .route_layer(axum::middleware::from_fn_with_state(
Modified lib/space/src/services/rooms.rs View lib/space/src/services/rooms.rs
@@ -195,7 +195,11 @@
195195 let r = room_row.insert(txn).await?;
196196 for &uid in &member_uids_clone {
197197 let rm = room_members::ActiveModel {
198 role: Set(room_members::RoomMemberRole::Member),
198 role: Set(if uid == caller {
199 room_members::RoomMemberRole::Owner
200 } else {
201 room_members::RoomMemberRole::Member
202 }),
199203 room_id: Set(room_id),
200204 user_id: Set(uid),
201205 nick: Set(None),