From 0a28ffb9c479dade491a1f2cc6da8b7922bda9de Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 00:29:44 -0400 Subject: [PATCH 01/10] Private message delete and read extracted. --- docs/src/contributing_websocket_http_api.md | 131 ++++++++++ server/lemmy_db/src/private_message.rs | 50 ++++ server/src/api/user.rs | 271 +++++++++++++------- server/src/routes/api.rs | 10 +- server/src/websocket/mod.rs | 2 + server/src/websocket/server.rs | 12 +- ui/src/api_tests/api.spec.ts | 15 +- ui/src/components/inbox.tsx | 46 +++- ui/src/components/private-message-form.tsx | 6 +- ui/src/components/private-message.tsx | 11 +- ui/src/interfaces.ts | 21 +- ui/src/services/WebSocketService.ts | 14 + 12 files changed, 457 insertions(+), 132 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 5af89431..5628c328 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -489,6 +489,137 @@ Only the first user will be able to be the admin. `PUT /user/mention` +#### Get Private Messages +##### Request +```rust +{ + op: "GetPrivateMessages", + data: { + unread_only: bool, + page: Option, + limit: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "GetPrivateMessages", + data: { + messages: Vec, + } +} +``` + +##### HTTP + +`GET /private_message/list` + +#### Create Private Message +##### Request +```rust +{ + op: "CreatePrivateMessage", + data: { + content: String, + recipient_id: i32, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "CreatePrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message` + +#### Edit Private Message +##### Request +```rust +{ + op: "EditPrivateMessage", + data: { + edit_id: i32, + content: String, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "EditPrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`PUT /private_message` + +#### Delete Private Message +##### Request +```rust +{ + op: "DeletePrivateMessage", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeletePrivateMessage", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message/delete` + +#### Mark Private Message as Read +##### Request +```rust +{ + op: "MarkPrivateMessageAsRead", + data: { + edit_id: i32, + read: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "MarkPrivateMessageAsRead", + data: { + message: PrivateMessageView, + } +} +``` + +##### HTTP + +`POST /private_message/mark_as_read` + #### Mark All As Read Marks all user replies and mentions as read. diff --git a/server/lemmy_db/src/private_message.rs b/server/lemmy_db/src/private_message.rs index 3492be2f..30f40e6b 100644 --- a/server/lemmy_db/src/private_message.rs +++ b/server/lemmy_db/src/private_message.rs @@ -80,6 +80,50 @@ impl PrivateMessage { .filter(ap_id.eq(object_id)) .first::(conn) } + + pub fn update_content( + conn: &PgConnection, + private_message_id: i32, + new_content: &str, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(content.eq(new_content)) + .get_result::(conn) + } + + pub fn update_deleted( + conn: &PgConnection, + private_message_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_read( + conn: &PgConnection, + private_message_id: i32, + new_read: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result, Error> { + use crate::schema::private_message::dsl::*; + diesel::update( + private_message + .filter(recipient_id.eq(for_recipient_id)) + .filter(read.eq(false)), + ) + .set(read.eq(true)) + .get_results::(conn) + } } #[cfg(test)] @@ -180,6 +224,10 @@ mod tests { let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap(); let updated_private_message = PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap(); + let deleted_private_message = + PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap(); + let marked_read_private_message = + PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap(); let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap(); User_::delete(&conn, inserted_creator.id).unwrap(); User_::delete(&conn, inserted_recipient.id).unwrap(); @@ -187,6 +235,8 @@ mod tests { assert_eq!(expected_private_message, read_private_message); assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, inserted_private_message); + assert!(deleted_private_message.deleted); + assert!(marked_read_private_message.read); assert_eq!(1, num_deleted); } } diff --git a/server/src/api/user.rs b/server/src/api/user.rs index fa1a9766..a83b794a 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -110,7 +110,7 @@ pub struct GetUserDetailsResponse { moderates: Vec, comments: Vec, posts: Vec, - admins: Vec, + admins: Vec, // TODO why is this necessary, just use GetSite } #[derive(Serialize, Deserialize)] @@ -216,9 +216,21 @@ pub struct CreatePrivateMessage { #[derive(Serialize, Deserialize)] pub struct EditPrivateMessage { edit_id: i32, - content: Option, - deleted: Option, - read: Option, + content: String, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeletePrivateMessage { + edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MarkPrivateMessageAsRead { + edit_id: i32, + read: bool, auth: String, } @@ -974,36 +986,10 @@ impl Perform for Oper { } } - // messages - let messages = blocking(pool, move |conn| { - PrivateMessageQueryBuilder::create(conn, user_id) - .page(1) - .limit(999) - .unread_only(true) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - for message in &messages { - let private_message_form = PrivateMessageForm { - content: message.to_owned().content, - creator_id: message.to_owned().creator_id, - recipient_id: message.to_owned().recipient_id, - deleted: None, - read: Some(true), - updated: None, - ap_id: message.to_owned().ap_id, - local: message.local, - published: None, - }; - - let message_id = message.id; - let update_pm = - move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form); - if blocking(pool, update_pm).await?.is_err() { - return Err(APIError::err("couldnt_update_private_message").into()); - } + // Mark all private_messages as read + let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id); + if blocking(pool, update_pm).await?.is_err() { + return Err(APIError::err("couldnt_update_private_message").into()); } Ok(GetRepliesResponse { replies: vec![] }) @@ -1293,59 +1279,25 @@ impl Perform for Oper { let user_id = claims.id; - let edit_id = data.edit_id; - let orig_private_message = - blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; - // Check for a site ban let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } - // Check to make sure they are the creator (or the recipient marking as read - if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id) - || orig_private_message.creator_id.eq(&user_id)) - { + // Checking permissions + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.creator_id { return Err(APIError::err("no_private_message_edit_allowed").into()); } - let content_slurs_removed = match &data.content { - Some(content) => remove_slurs(content), - None => orig_private_message.content.clone(), - }; - - let private_message_form = { - if data.read.is_some() { - PrivateMessageForm { - content: orig_private_message.content.to_owned(), - creator_id: orig_private_message.creator_id, - recipient_id: orig_private_message.recipient_id, - read: data.read.to_owned(), - updated: orig_private_message.updated, - deleted: Some(orig_private_message.deleted), - ap_id: orig_private_message.ap_id, - local: orig_private_message.local, - published: None, - } - } else { - PrivateMessageForm { - content: content_slurs_removed, - creator_id: orig_private_message.creator_id, - recipient_id: orig_private_message.recipient_id, - deleted: data.deleted.to_owned(), - read: Some(orig_private_message.read), - updated: Some(naive_now()), - ap_id: orig_private_message.ap_id, - local: orig_private_message.local, - published: None, - } - } - }; - + // Doing the update + let content_slurs_removed = remove_slurs(&data.content); let edit_id = data.edit_id; let updated_private_message = match blocking(pool, move |conn| { - PrivateMessage::update(conn, edit_id, &private_message_form) + PrivateMessage::update_content(conn, edit_id, &content_slurs_removed) }) .await? { @@ -1353,30 +1305,14 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), }; - if data.read.is_none() { - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_private_message - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_private_message - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else { - updated_private_message - .send_update(&user, &self.client, pool) - .await?; - } - } else { - updated_private_message - .send_update(&user, &self.client, pool) - .await?; - } + // Send the apub update + updated_private_message + .send_update(&user, &self.client, pool) + .await?; let edit_id = data.edit_id; let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; let res = PrivateMessageResponse { message }; @@ -1384,7 +1320,146 @@ impl Perform for Oper { ws.chatserver.do_send(SendUserRoomMessage { op: UserOperation::EditPrivateMessage, response: res.clone(), - recipient_id: orig_private_message.recipient_id, + recipient_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeletePrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Checking permissions + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.creator_id { + return Err(APIError::err("no_private_message_edit_allowed").into()); + } + + // Doing the update + let edit_id = data.edit_id; + let deleted = data.deleted; + let updated_private_message = match blocking(pool, move |conn| { + PrivateMessage::update_deleted(conn, edit_id, deleted) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; + + // Send the apub update + if data.deleted { + updated_private_message + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_private_message + .send_undo_delete(&user, &self.client, pool) + .await?; + } + + let edit_id = data.edit_id; + let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; + + let res = PrivateMessageResponse { message }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendUserRoomMessage { + op: UserOperation::DeletePrivateMessage, + response: res.clone(), + recipient_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &MarkPrivateMessageAsRead = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Checking permissions + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; + if user_id != orig_private_message.recipient_id { + return Err(APIError::err("couldnt_update_private_message").into()); + } + + // Doing the update + let edit_id = data.edit_id; + let read = data.read; + match blocking(pool, move |conn| { + PrivateMessage::update_read(conn, edit_id, read) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; + + // No need to send an apub update + + let edit_id = data.edit_id; + let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; + let recipient_id = message.recipient_id; + + let res = PrivateMessageResponse { message }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendUserRoomMessage { + op: UserOperation::MarkPrivateMessageAsRead, + response: res.clone(), + recipient_id, my_id: ws.id, }); } diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 35e495fa..69ecbc8f 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -90,7 +90,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("/list", web::get().to(route_get::)) .route("", web::post().to(route_post::)) - .route("", web::put().to(route_post::)), + .route("", web::put().to(route_post::)) + .route( + "/delete", + web::post().to(route_post::), + ) + .route( + "/mark_as_read", + web::post().to(route_post::), + ), ) // User .service( diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index cdaf4f30..0e938c7e 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -59,6 +59,8 @@ pub enum UserOperation { PasswordChange, CreatePrivateMessage, EditPrivateMessage, + DeletePrivateMessage, + MarkPrivateMessageAsRead, GetPrivateMessages, UserJoin, GetComments, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index aef0abb8..4543781a 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -448,13 +448,21 @@ impl ChatServer { UserOperation::DeleteAccount => do_user_operation::(args).await, UserOperation::PasswordReset => do_user_operation::(args).await, UserOperation::PasswordChange => do_user_operation::(args).await, + UserOperation::UserJoin => do_user_operation::(args).await, + UserOperation::SaveUserSettings => do_user_operation::(args).await, + + // Private Message ops UserOperation::CreatePrivateMessage => { do_user_operation::(args).await } UserOperation::EditPrivateMessage => do_user_operation::(args).await, + UserOperation::DeletePrivateMessage => { + do_user_operation::(args).await + } + UserOperation::MarkPrivateMessageAsRead => { + do_user_operation::(args).await + } UserOperation::GetPrivateMessages => do_user_operation::(args).await, - UserOperation::UserJoin => do_user_operation::(args).await, - UserOperation::SaveUserSettings => do_user_operation::(args).await, // Site ops UserOperation::GetModlog => do_user_operation::(args).await, diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 41710e11..09454b15 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -9,17 +9,16 @@ import { FollowCommunityForm, CommunityResponse, GetFollowedCommunitiesResponse, - GetPostForm, GetPostResponse, CommentForm, CommentResponse, CommunityForm, - GetCommunityForm, GetCommunityResponse, CommentLikeForm, CreatePostLikeForm, PrivateMessageForm, EditPrivateMessageForm, + DeletePrivateMessageForm, PrivateMessageResponse, PrivateMessagesResponse, GetUserMentionsResponse, @@ -1149,16 +1148,16 @@ describe('main', () => { ); // lemmy alpha deletes the private message - let deletePrivateMessageForm: EditPrivateMessageForm = { + let deletePrivateMessageForm: DeletePrivateMessageForm = { deleted: true, edit_id: createRes.message.id, auth: lemmyAlphaAuth, }; let deleteRes: PrivateMessageResponse = await fetch( - `${lemmyAlphaApiUrl}/private_message`, + `${lemmyAlphaApiUrl}/private_message/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -1182,16 +1181,16 @@ describe('main', () => { expect(getPrivateMessagesDeletedRes.messages.length).toBe(0); // lemmy alpha undeletes the private message - let undeletePrivateMessageForm: EditPrivateMessageForm = { + let undeletePrivateMessageForm: DeletePrivateMessageForm = { deleted: false, edit_id: createRes.message.id, auth: lemmyAlphaAuth, }; let undeleteRes: PrivateMessageResponse = await fetch( - `${lemmyAlphaApiUrl}/private_message`, + `${lemmyAlphaApiUrl}/private_message/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 8e148921..faedb4b1 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -446,22 +446,42 @@ export class Inbox extends Component { let found: PrivateMessageI = this.state.messages.find( m => m.id === data.message.id ); - found.content = data.message.content; - found.updated = data.message.updated; - found.deleted = data.message.deleted; - // If youre in the unread view, just remove it from the list - if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) { - this.state.messages = this.state.messages.filter( - r => r.id !== data.message.id - ); - } else { - let found = this.state.messages.find(c => c.id == data.message.id); - found.read = data.message.read; + if (found) { + found.content = data.message.content; + found.updated = data.message.updated; + } + this.setState(this.state); + } else if (res.op == UserOperation.DeletePrivateMessage) { + let data = res.data as PrivateMessageResponse; + let found: PrivateMessageI = this.state.messages.find( + m => m.id === data.message.id + ); + if (found) { + found.deleted = data.message.deleted; + found.updated = data.message.updated; + } + this.setState(this.state); + } else if (res.op == UserOperation.MarkPrivateMessageAsRead) { + let data = res.data as PrivateMessageResponse; + let found: PrivateMessageI = this.state.messages.find( + m => m.id === data.message.id + ); + + if (found) { + found.updated = data.message.updated; + + // If youre in the unread view, just remove it from the list + if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) { + this.state.messages = this.state.messages.filter( + r => r.id !== data.message.id + ); + } else { + let found = this.state.messages.find(c => c.id == data.message.id); + found.read = data.message.read; + } } this.sendUnreadCount(); - window.scrollTo(0, 0); this.setState(this.state); - setupTippy(); } else if (res.op == UserOperation.MarkAllAsRead) { // Moved to be instant } else if (res.op == UserOperation.EditComment) { diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx index b8dc8853..eb4d49a3 100644 --- a/ui/src/components/private-message-form.tsx +++ b/ui/src/components/private-message-form.tsx @@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component< this.state.loading = false; this.setState(this.state); return; - } else if (res.op == UserOperation.EditPrivateMessage) { + } else if ( + res.op == UserOperation.EditPrivateMessage || + res.op == UserOperation.DeletePrivateMessage || + res.op == UserOperation.MarkPrivateMessageAsRead + ) { let data = res.data as PrivateMessageResponse; this.state.loading = false; this.props.onEdit(data.message); diff --git a/ui/src/components/private-message.tsx b/ui/src/components/private-message.tsx index 71924f0c..ac707930 100644 --- a/ui/src/components/private-message.tsx +++ b/ui/src/components/private-message.tsx @@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { PrivateMessage as PrivateMessageI, - EditPrivateMessageForm, + DeletePrivateMessageForm, + MarkPrivateMessageAsReadForm, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils'; @@ -243,11 +244,11 @@ export class PrivateMessage extends Component< } handleDeleteClick(i: PrivateMessage) { - let form: EditPrivateMessageForm = { + let form: DeletePrivateMessageForm = { edit_id: i.props.privateMessage.id, deleted: !i.props.privateMessage.deleted, }; - WebSocketService.Instance.editPrivateMessage(form); + WebSocketService.Instance.deletePrivateMessage(form); } handleReplyCancel() { @@ -257,11 +258,11 @@ export class PrivateMessage extends Component< } handleMarkRead(i: PrivateMessage) { - let form: EditPrivateMessageForm = { + let form: MarkPrivateMessageAsReadForm = { edit_id: i.props.privateMessage.id, read: !i.props.privateMessage.read, }; - WebSocketService.Instance.editPrivateMessage(form); + WebSocketService.Instance.markPrivateMessageAsRead(form); } handleMessageCollapse(i: PrivateMessage) { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index dc860e06..17b5f694 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -40,6 +40,8 @@ export enum UserOperation { PasswordChange, CreatePrivateMessage, EditPrivateMessage, + DeletePrivateMessage, + MarkPrivateMessageAsRead, GetPrivateMessages, UserJoin, GetComments, @@ -834,9 +836,19 @@ export interface PrivateMessageFormParams { export interface EditPrivateMessageForm { edit_id: number; - content?: string; - deleted?: boolean; - read?: boolean; + content: string; + auth?: string; +} + +export interface DeletePrivateMessageForm { + edit_id: number; + deleted: boolean; + auth?: string; +} + +export interface MarkPrivateMessageAsReadForm { + edit_id: number; + read: boolean; auth?: string; } @@ -864,7 +876,6 @@ export interface UserJoinResponse { } export type MessageType = - | EditPrivateMessageForm | LoginForm | RegisterForm | CommunityForm @@ -900,6 +911,8 @@ export type MessageType = | PasswordChangeForm | PrivateMessageForm | EditPrivateMessageForm + | DeletePrivateMessageForm + | MarkPrivateMessageAsReadForm | GetPrivateMessagesForm | SiteConfigForm; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 8e4364d2..f0fb6fc7 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -36,6 +36,8 @@ import { PasswordChangeForm, PrivateMessageForm, EditPrivateMessageForm, + DeletePrivateMessageForm, + MarkPrivateMessageAsReadForm, GetPrivateMessagesForm, GetCommentsForm, UserJoinForm, @@ -315,6 +317,18 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form)); } + public deletePrivateMessage(form: DeletePrivateMessageForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form)); + } + + public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) { + this.setAuth(form); + this.ws.send( + this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form) + ); + } + public getPrivateMessages(form: GetPrivateMessagesForm) { this.setAuth(form); this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form)); From a67f46bec5c67b9a6e8b442e8c451635418e2180 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 10:56:40 -0400 Subject: [PATCH 02/10] EditUserMention changed to MarkUserMentionAsRead. --- docs/src/contributing_websocket_http_api.md | 10 ++--- server/lemmy_db/src/user_mention.rs | 24 +++++++++++ server/src/api/user.rs | 46 +++++---------------- server/src/routes/api.rs | 5 ++- server/src/websocket/mod.rs | 2 +- server/src/websocket/server.rs | 4 +- ui/src/components/comment-node.tsx | 6 +-- ui/src/components/inbox.tsx | 2 +- ui/src/interfaces.ts | 8 ++-- ui/src/services/WebSocketService.ts | 6 +-- 10 files changed, 58 insertions(+), 55 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 5628c328..63d8f994 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -464,14 +464,14 @@ Only the first user will be able to be the admin. `GET /user/mentions` -#### Edit User Mention +#### Mark User Mention as read ##### Request ```rust { - op: "EditUserMention", + op: "MarkUserMentionAsRead", data: { user_mention_id: i32, - read: Option, + read: bool, auth: String, } } @@ -479,7 +479,7 @@ Only the first user will be able to be the admin. ##### Response ```rust { - op: "EditUserMention", + op: "MarkUserMentionAsRead", data: { mention: UserMentionView, } @@ -487,7 +487,7 @@ Only the first user will be able to be the admin. ``` ##### HTTP -`PUT /user/mention` +`POST /user/mention/mark_as_read` #### Get Private Messages ##### Request diff --git a/server/lemmy_db/src/user_mention.rs b/server/lemmy_db/src/user_mention.rs index 5dc899b2..f32318e0 100644 --- a/server/lemmy_db/src/user_mention.rs +++ b/server/lemmy_db/src/user_mention.rs @@ -52,6 +52,30 @@ impl Crud for UserMention { } } +impl UserMention { + pub fn update_read( + conn: &PgConnection, + user_mention_id: i32, + new_read: bool, + ) -> Result { + use crate::schema::user_mention::dsl::*; + diesel::update(user_mention.find(user_mention_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result, Error> { + use crate::schema::user_mention::dsl::*; + diesel::update( + user_mention + .filter(recipient_id.eq(for_recipient_id)) + .filter(read.eq(false)), + ) + .set(read.eq(true)) + .get_results::(conn) + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/server/src/api/user.rs b/server/src/api/user.rs index a83b794a..2e621823 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -174,9 +174,9 @@ pub struct GetUserMentions { } #[derive(Serialize, Deserialize)] -pub struct EditUserMention { +pub struct MarkUserMentionAsRead { user_mention_id: i32, - read: Option, + read: bool, auth: String, } @@ -874,7 +874,7 @@ impl Perform for Oper { } #[async_trait::async_trait(?Send)] -impl Perform for Oper { +impl Perform for Oper { type Response = UserMentionResponse; async fn perform( @@ -882,7 +882,7 @@ impl Perform for Oper { pool: &DbPool, _websocket_info: Option, ) -> Result { - let data: &EditUserMention = &self.data; + let data: &MarkUserMentionAsRead = &self.data; let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, @@ -899,15 +899,9 @@ impl Perform for Oper { return Err(APIError::err("couldnt_update_comment").into()); } - let user_mention_form = UserMentionForm { - recipient_id: read_user_mention.recipient_id, - comment_id: read_user_mention.comment_id, - read: data.read.to_owned(), - }; - let user_mention_id = read_user_mention.id; - let update_mention = - move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form); + let read = data.read; + let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read); if blocking(pool, update_mention).await?.is_err() { return Err(APIError::err("couldnt_update_comment").into()); }; @@ -960,30 +954,10 @@ impl Perform for Oper { } } - // Mentions - let mentions = blocking(pool, move |conn| { - UserMentionQueryBuilder::create(conn, user_id) - .unread_only(true) - .page(1) - .limit(999) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - for mention in &mentions { - let mention_form = UserMentionForm { - recipient_id: mention.to_owned().recipient_id, - comment_id: mention.to_owned().id, - read: Some(true), - }; - - let user_mention_id = mention.user_mention_id; - let update_mention = - move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form); - if blocking(pool, update_mention).await?.is_err() { - return Err(APIError::err("couldnt_update_comment").into()); - } + // Mark all user mentions as read + let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id); + if blocking(pool, update_user_mentions).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); } // Mark all private_messages as read diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 69ecbc8f..a02f1d3b 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -115,7 +115,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("", web::get().to(route_get::)) .route("/mention", web::get().to(route_get::)) - .route("/mention", web::put().to(route_post::)) + .route( + "/mention/mark_as_read", + web::post().to(route_post::), + ) .route("/replies", web::get().to(route_get::)) .route( "/followed_communities", diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index 0e938c7e..431273d2 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -40,7 +40,7 @@ pub enum UserOperation { GetUserDetails, GetReplies, GetUserMentions, - EditUserMention, + MarkUserMentionAsRead, GetModlog, BanFromCommunity, AddModToCommunity, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 4543781a..49099084 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -443,7 +443,9 @@ impl ChatServer { UserOperation::AddAdmin => do_user_operation::(args).await, UserOperation::BanUser => do_user_operation::(args).await, UserOperation::GetUserMentions => do_user_operation::(args).await, - UserOperation::EditUserMention => do_user_operation::(args).await, + UserOperation::MarkUserMentionAsRead => { + do_user_operation::(args).await + } UserOperation::MarkAllAsRead => do_user_operation::(args).await, UserOperation::DeleteAccount => do_user_operation::(args).await, UserOperation::PasswordReset => do_user_operation::(args).await, diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index a6b9b7ba..dfe52ec1 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -4,7 +4,7 @@ import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, - EditUserMentionForm, + MarkUserMentionAsReadForm, SaveCommentForm, BanFromCommunityForm, BanUserForm, @@ -969,11 +969,11 @@ export class CommentNode extends Component { handleMarkRead(i: CommentNode) { // if it has a user_mention_id field, then its a mention if (i.props.node.comment.user_mention_id) { - let form: EditUserMentionForm = { + let form: MarkUserMentionAsReadForm = { user_mention_id: i.props.node.comment.user_mention_id, read: !i.props.node.comment.read, }; - WebSocketService.Instance.editUserMention(form); + WebSocketService.Instance.markUserMentionAsRead(form); } else { let form: CommentFormI = { content: i.props.node.comment.content, diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index faedb4b1..5609879c 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -500,7 +500,7 @@ export class Inbox extends Component { this.sendUnreadCount(); this.setState(this.state); setupTippy(); - } else if (res.op == UserOperation.EditUserMention) { + } else if (res.op == UserOperation.MarkUserMentionAsRead) { let data = res.data as UserMentionResponse; let found = this.state.mentions.find(c => c.id == data.mention.id); diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 17b5f694..0beb0ff9 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -21,7 +21,7 @@ export enum UserOperation { GetUserDetails, GetReplies, GetUserMentions, - EditUserMention, + MarkUserMentionAsRead, GetModlog, BanFromCommunity, AddModToCommunity, @@ -357,9 +357,9 @@ export interface GetUserMentionsResponse { mentions: Array; } -export interface EditUserMentionForm { +export interface MarkUserMentionAsReadForm { user_mention_id: number; - read?: boolean; + read: boolean; auth?: string; } @@ -901,7 +901,7 @@ export type MessageType = | GetUserDetailsForm | GetRepliesForm | GetUserMentionsForm - | EditUserMentionForm + | MarkUserMentionAsReadForm | GetModlogForm | SiteForm | SearchForm diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index f0fb6fc7..54dc9635 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -28,7 +28,7 @@ import { UserView, GetRepliesForm, GetUserMentionsForm, - EditUserMentionForm, + MarkUserMentionAsReadForm, SearchForm, UserSettingsForm, DeleteAccountForm, @@ -247,9 +247,9 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form)); } - public editUserMention(form: EditUserMentionForm) { + public markUserMentionAsRead(form: MarkUserMentionAsReadForm) { this.setAuth(form); - this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form)); + this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form)); } public getModlog(form: GetModlogForm) { From 9bc6698f5841e3c16a84a0fd35f8383a9310b012 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 13:37:39 -0400 Subject: [PATCH 03/10] Added community delete and remove. --- docs/src/contributing_websocket_http_api.md | 62 +++- server/lemmy_db/src/community.rs | 33 +++ server/lemmy_db/src/community_view.rs | 8 +- server/src/api/community.rs | 303 +++++++++++++++----- server/src/routes/api.rs | 2 + server/src/websocket/mod.rs | 2 + server/src/websocket/server.rs | 5 + ui/src/api_tests/api.spec.ts | 46 ++- ui/src/components/community.tsx | 6 +- ui/src/components/post.tsx | 6 +- ui/src/components/sidebar.tsx | 21 +- ui/src/interfaces.ts | 20 +- ui/src/services/WebSocketService.ts | 28 +- ui/translations/en.json | 1 + 14 files changed, 395 insertions(+), 148 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 63d8f994..4931ed58 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1102,7 +1102,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url` `POST /community/mod` #### Edit Community -Mods and admins can remove and lock a community, creators can delete it. +Only mods can edit a community. ##### Request ```rust @@ -1113,10 +1113,6 @@ Mods and admins can remove and lock a community, creators can delete it. title: String, description: Option, category_id: i32, - removed: Option, - deleted: Option, - reason: Option, - expires: Option, auth: String } } @@ -1134,6 +1130,62 @@ Mods and admins can remove and lock a community, creators can delete it. `PUT /community` +#### Delete Community +Only a creator can delete a community + +##### Request +```rust +{ + op: "DeleteCommunity", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeleteCommunity", + data: { + community: CommunityView + } +} +``` +##### HTTP + +`POST /community/delete` + +#### Remove Community +Only admins can remove a community. + +##### Request +```rust +{ + op: "RemoveCommunity", + data: { + edit_id: i32, + removed: bool, + reason: Option, + expires: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "RemoveCommunity", + data: { + community: CommunityView + } +} +``` +##### HTTP + +`POST /community/remove` + #### Follow Community ##### Request ```rust diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index e1f24391..4fe507f7 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -99,6 +99,39 @@ impl Community { use crate::schema::community::dsl::*; community.filter(local.eq(true)).load::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + community_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + community_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_creator( + conn: &PgConnection, + community_id: i32, + new_creator_id: i32, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set(creator_id.eq(new_creator_id)) + .get_result::(conn) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/lemmy_db/src/community_view.rs b/server/lemmy_db/src/community_view.rs index 5c6bd81a..880c9455 100644 --- a/server/lemmy_db/src/community_view.rs +++ b/server/lemmy_db/src/community_view.rs @@ -295,18 +295,18 @@ pub struct CommunityModeratorView { } impl CommunityModeratorView { - pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result, Error> { + pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result, Error> { use super::community_view::community_moderator_view::dsl::*; community_moderator_view - .filter(community_id.eq(from_community_id)) + .filter(community_id.eq(for_community_id)) .order_by(published) .load::(conn) } - pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result, Error> { + pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result, Error> { use super::community_view::community_moderator_view::dsl::*; community_moderator_view - .filter(user_id.eq(from_user_id)) + .filter(user_id.eq(for_user_id)) .order_by(published) .load::(conn) } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index fc5cb0e6..1f46c596 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -10,6 +10,7 @@ use crate::{ }, DbPool, }; +use diesel::PgConnection; use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType}; use lemmy_utils::{ generate_actor_keypair, @@ -34,7 +35,7 @@ pub struct GetCommunity { pub struct GetCommunityResponse { pub community: CommunityView, pub moderators: Vec, - pub admins: Vec, + pub admins: Vec, // TODO this should be from GetSite, shouldn't need this pub online: usize, } @@ -101,9 +102,21 @@ pub struct EditCommunity { title: String, description: Option, category_id: i32, - removed: Option, - deleted: Option, nsfw: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeleteCommunity { + pub edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RemoveCommunity { + pub edit_id: i32, + removed: bool, reason: Option, expires: Option, auth: String, @@ -366,24 +379,15 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } - // Verify its a mod + // Verify its a mod (only mods can edit it) let edit_id = data.edit_id; - let mut editors: Vec = Vec::new(); - editors.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(conn, edit_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - editors.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - if !editors.contains(&user_id) { - return Err(APIError::err("no_community_edit_allowed").into()); + let mods: Vec = blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, edit_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??; + if !mods.contains(&user_id) { + return Err(APIError::err("not_a_moderator").into()); } let edit_id = data.edit_id; @@ -395,8 +399,8 @@ impl Perform for Oper { description: data.description.to_owned(), category_id: data.category_id.to_owned(), creator_id: read_community.creator_id, - removed: data.removed.to_owned(), - deleted: data.deleted.to_owned(), + removed: Some(read_community.removed), + deleted: Some(read_community.deleted), nsfw: data.nsfw, updated: Some(naive_now()), actor_id: read_community.actor_id, @@ -408,7 +412,7 @@ impl Perform for Oper { }; let edit_id = data.edit_id; - let updated_community = match blocking(pool, move |conn| { + match blocking(pool, move |conn| { Community::update(conn, edit_id, &community_form) }) .await? @@ -417,43 +421,8 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; - // Mod tables - if let Some(removed) = data.removed.to_owned() { - let expires = match data.expires { - Some(time) => Some(naive_from_unix(time)), - None => None, - }; - let form = ModRemoveCommunityForm { - mod_user_id: user_id, - community_id: data.edit_id, - removed: Some(removed), - reason: data.reason.to_owned(), - expires, - }; - blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??; - } - - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_community - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_community - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else if let Some(removed) = data.removed.to_owned() { - if removed { - updated_community - .send_remove(&user, &self.client, pool) - .await?; - } else { - updated_community - .send_undo_remove(&user, &self.client, pool) - .await?; - } - } + // TODO there needs to be some kind of an apub update + // process for communities and users let edit_id = data.edit_id; let community_view = blocking(pool, move |conn| { @@ -483,6 +452,186 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommunityResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeleteCommunity = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Verify its the creator (only a creator can delete the community) + let edit_id = data.edit_id; + let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??; + if read_community.creator_id != user_id { + return Err(APIError::err("no_community_edit_allowed").into()); + } + + // Do the delete + let edit_id = data.edit_id; + let deleted = data.deleted; + let updated_community = match blocking(pool, move |conn| { + Community::update_deleted(conn, edit_id, deleted) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(APIError::err("couldnt_update_community").into()), + }; + + // Send apub messages + if deleted { + updated_community + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_community + .send_undo_delete(&user, &self.client, pool) + .await?; + } + + let edit_id = data.edit_id; + let community_view = blocking(pool, move |conn| { + CommunityView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = CommunityResponse { + community: community_view, + }; + + if let Some(ws) = websocket_info { + // Strip out the user id and subscribed when sending to others + let mut res_sent = res.clone(); + res_sent.community.user_id = None; + res_sent.community.subscribed = None; + + ws.chatserver.do_send(SendCommunityRoomMessage { + op: UserOperation::DeleteCommunity, + response: res_sent, + community_id: data.edit_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommunityResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &RemoveCommunity = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Verify its an admin (only an admin can remove a community) + let admins: Vec = blocking(pool, move |conn| { + UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) + }) + .await??; + if !admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Do the remove + let edit_id = data.edit_id; + let removed = data.removed; + let updated_community = match blocking(pool, move |conn| { + Community::update_removed(conn, edit_id, removed) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(APIError::err("couldnt_update_community").into()), + }; + + // Mod tables + let expires = match data.expires { + Some(time) => Some(naive_from_unix(time)), + None => None, + }; + let form = ModRemoveCommunityForm { + mod_user_id: user_id, + community_id: data.edit_id, + removed: Some(removed), + reason: data.reason.to_owned(), + expires, + }; + blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??; + + // Apub messages + if removed { + updated_community + .send_remove(&user, &self.client, pool) + .await?; + } else { + updated_community + .send_undo_remove(&user, &self.client, pool) + .await?; + } + + let edit_id = data.edit_id; + let community_view = blocking(pool, move |conn| { + CommunityView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = CommunityResponse { + community: community_view, + }; + + if let Some(ws) = websocket_info { + // Strip out the user id and subscribed when sending to others + let mut res_sent = res.clone(); + res_sent.community.user_id = None; + res_sent.community.subscribed = None; + + ws.chatserver.do_send(SendCommunityRoomMessage { + op: UserOperation::RemoveCommunity, + response: res_sent, + community_id: data.edit_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = ListCommunitiesResponse; @@ -852,26 +1001,9 @@ impl Perform for Oper { return Err(APIError::err("not_an_admin").into()); } - let community_form = CommunityForm { - name: read_community.name, - title: read_community.title, - description: read_community.description, - category_id: read_community.category_id, - creator_id: data.user_id, // This makes the new user the community creator - removed: None, - deleted: None, - nsfw: read_community.nsfw, - updated: Some(naive_now()), - actor_id: read_community.actor_id, - local: read_community.local, - private_key: read_community.private_key, - public_key: read_community.public_key, - last_refreshed_at: None, - published: None, - }; - let community_id = data.community_id; - let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form); + let new_creator = data.user_id; + let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator); if blocking(pool, update).await?.is_err() { return Err(APIError::err("couldnt_update_community").into()); }; @@ -946,3 +1078,16 @@ impl Perform for Oper { }) } } + +pub fn community_mods_and_admins( + conn: &PgConnection, + community_id: i32, +) -> Result, LemmyError> { + let mut editors: Vec = Vec::new(); + editors.append( + &mut CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect())?, + ); + editors.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); + Ok(editors) +} diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index a02f1d3b..4722fb81 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -53,7 +53,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("", web::put().to(route_post::)) .route("/list", web::get().to(route_get::)) .route("/follow", web::post().to(route_post::)) + .route("/delete", web::post().to(route_post::)) // Mod Actions + .route("/remove", web::post().to(route_post::)) .route("/transfer", web::post().to(route_post::)) .route("/ban_user", web::post().to(route_post::)) .route("/mod", web::post().to(route_post::)), diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index 431273d2..c4c02146 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -35,6 +35,8 @@ pub enum UserOperation { EditPost, SavePost, EditCommunity, + DeleteCommunity, + RemoveCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 49099084..0344e1b9 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -212,6 +212,9 @@ impl ChatServer { // Also leave all communities // This avoids double messages + // TODO found a bug, whereby community messages like + // delete and remove aren't sent, because + // you left the community room for sessions in self.community_rooms.values_mut() { sessions.remove(&id); } @@ -483,6 +486,8 @@ impl ChatServer { UserOperation::ListCommunities => do_user_operation::(args).await, UserOperation::CreateCommunity => do_user_operation::(args).await, UserOperation::EditCommunity => do_user_operation::(args).await, + UserOperation::DeleteCommunity => do_user_operation::(args).await, + UserOperation::RemoveCommunity => do_user_operation::(args).await, UserOperation::FollowCommunity => do_user_operation::(args).await, UserOperation::GetFollowedCommunities => { do_user_operation::(args).await diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 09454b15..891654b2 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -13,6 +13,8 @@ import { CommentForm, CommentResponse, CommunityForm, + DeleteCommunityForm, + RemoveCommunityForm, GetCommunityResponse, CommentLikeForm, CreatePostLikeForm, @@ -731,20 +733,16 @@ describe('main', () => { expect(getPostResAgainTwo.post.deleted).toBe(false); // lemmy_beta deletes the community - let deleteCommunityForm: CommunityForm = { - name: communityName, - title: communityName, - category_id: 1, + let deleteCommunityForm: DeleteCommunityForm = { edit_id: createCommunityRes.community.id, - nsfw: false, deleted: true, auth: lemmyBetaAuth, }; let deleteResponse: CommunityResponse = await fetch( - `${lemmyBetaApiUrl}/community`, + `${lemmyBetaApiUrl}/community/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -764,20 +762,16 @@ describe('main', () => { expect(getCommunityRes.community.deleted).toBe(true); // lemmy_beta undeletes the community - let undeleteCommunityForm: CommunityForm = { - name: communityName, - title: communityName, - category_id: 1, + let undeleteCommunityForm: DeleteCommunityForm = { edit_id: createCommunityRes.community.id, - nsfw: false, deleted: false, auth: lemmyBetaAuth, }; let undeleteCommunityRes: CommunityResponse = await fetch( - `${lemmyBetaApiUrl}/community`, + `${lemmyBetaApiUrl}/community/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -1006,21 +1000,17 @@ describe('main', () => { }).then(d => d.json()); expect(getPostResAgainTwo.post.removed).toBe(false); - // lemmy_beta deletes the community - let removeCommunityForm: CommunityForm = { - name: communityName, - title: communityName, - category_id: 1, + // lemmy_beta removes the community + let removeCommunityForm: RemoveCommunityForm = { edit_id: createCommunityRes.community.id, - nsfw: false, removed: true, auth: lemmyBetaAuth, }; let removeCommunityRes: CommunityResponse = await fetch( - `${lemmyBetaApiUrl}/community`, + `${lemmyBetaApiUrl}/community/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -1028,7 +1018,7 @@ describe('main', () => { } ).then(d => d.json()); - // Make sure the delete went through + // Make sure the remove went through expect(removeCommunityRes.community.removed).toBe(true); // Re-get it from alpha, make sure its removed there too @@ -1040,20 +1030,16 @@ describe('main', () => { expect(getCommunityRes.community.removed).toBe(true); // lemmy_beta unremoves the community - let unremoveCommunityForm: CommunityForm = { - name: communityName, - title: communityName, - category_id: 1, + let unremoveCommunityForm: RemoveCommunityForm = { edit_id: createCommunityRes.community.id, - nsfw: false, removed: false, auth: lemmyBetaAuth, }; let unremoveCommunityRes: CommunityResponse = await fetch( - `${lemmyBetaApiUrl}/community`, + `${lemmyBetaApiUrl}/community/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 99b692ca..2899c2cb 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -360,7 +360,11 @@ export class Community extends Component { document.title = `/c/${this.state.community.name} - ${this.state.site.name}`; this.setState(this.state); this.fetchData(); - } else if (res.op == UserOperation.EditCommunity) { + } else if ( + res.op == UserOperation.EditCommunity || + res.op == UserOperation.DeleteCommunity || + res.op == UserOperation.RemoveCommunity + ) { let data = res.data as CommunityResponse; this.state.community = data.community; this.setState(this.state); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 9eef286c..97f80b6e 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -462,7 +462,11 @@ export class Post extends Component { this.state.post = data.post; this.setState(this.state); setupTippy(); - } else if (res.op == UserOperation.EditCommunity) { + } else if ( + res.op == UserOperation.EditCommunity || + res.op == UserOperation.DeleteCommunity || + res.op == UserOperation.RemoveCommunity + ) { let data = res.data as CommunityResponse; this.state.community = data.community; this.state.post.community_id = data.community.id; diff --git a/ui/src/components/sidebar.tsx b/ui/src/components/sidebar.tsx index 42abf65a..b600628f 100644 --- a/ui/src/components/sidebar.tsx +++ b/ui/src/components/sidebar.tsx @@ -4,7 +4,8 @@ import { Community, CommunityUser, FollowCommunityForm, - CommunityForm as CommunityFormI, + DeleteCommunityForm, + RemoveCommunityForm, UserView, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; @@ -284,16 +285,11 @@ export class Sidebar extends Component { handleDeleteClick(i: Sidebar) { event.preventDefault(); - let deleteForm: CommunityFormI = { - name: i.props.community.name, - title: i.props.community.title, - category_id: i.props.community.category_id, + let deleteForm: DeleteCommunityForm = { edit_id: i.props.community.id, deleted: !i.props.community.deleted, - nsfw: i.props.community.nsfw, - auth: null, }; - WebSocketService.Instance.editCommunity(deleteForm); + WebSocketService.Instance.deleteCommunity(deleteForm); } handleUnsubscribe(communityId: number) { @@ -350,18 +346,13 @@ export class Sidebar extends Component { handleModRemoveSubmit(i: Sidebar) { event.preventDefault(); - let deleteForm: CommunityFormI = { - name: i.props.community.name, - title: i.props.community.title, - category_id: i.props.community.category_id, + let removeForm: RemoveCommunityForm = { edit_id: i.props.community.id, removed: !i.props.community.removed, reason: i.state.removeReason, expires: getUnixTime(i.state.removeExpires), - nsfw: i.props.community.nsfw, - auth: null, }; - WebSocketService.Instance.editCommunity(deleteForm); + WebSocketService.Instance.removeCommunity(removeForm); i.state.showRemoveDialog = false; i.setState(i.state); diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 0beb0ff9..7f650f1b 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -16,6 +16,8 @@ export enum UserOperation { EditPost, SavePost, EditCommunity, + DeleteCommunity, + RemoveCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, @@ -573,13 +575,23 @@ export interface UserSettingsForm { export interface CommunityForm { name: string; + edit_id?: number; title: string; description?: string; category_id: number; - edit_id?: number; - removed?: boolean; - deleted?: boolean; nsfw: boolean; + auth?: string; +} + +export interface DeleteCommunityForm { + edit_id: number; + deleted: boolean; + auth?: string; +} + +export interface RemoveCommunityForm { + edit_id: number; + removed: boolean; reason?: string; expires?: number; auth?: string; @@ -879,6 +891,8 @@ export type MessageType = | LoginForm | RegisterForm | CommunityForm + | DeleteCommunityForm + | RemoveCommunityForm | FollowCommunityForm | ListCommunitiesForm | GetFollowedCommunitiesForm diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 54dc9635..26e58135 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -4,6 +4,8 @@ import { RegisterForm, UserOperation, CommunityForm, + DeleteCommunityForm, + RemoveCommunityForm, PostForm, SavePostForm, CommentForm, @@ -105,18 +107,24 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm)); } - public createCommunity(communityForm: CommunityForm) { - this.setAuth(communityForm); - this.ws.send( - this.wsSendWrapper(UserOperation.CreateCommunity, communityForm) - ); + public createCommunity(form: CommunityForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form)); } - public editCommunity(communityForm: CommunityForm) { - this.setAuth(communityForm); - this.ws.send( - this.wsSendWrapper(UserOperation.EditCommunity, communityForm) - ); + public editCommunity(form: CommunityForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form)); + } + + public deleteCommunity(form: DeleteCommunityForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeleteCommunity, form)); + } + + public removeCommunity(form: RemoveCommunityForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.RemoveCommunity, form)); } public followCommunity(followCommunityForm: FollowCommunityForm) { diff --git a/ui/translations/en.json b/ui/translations/en.json index e9d768f2..8ac99c26 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -254,6 +254,7 @@ "couldnt_save_post": "Couldn't save post.", "no_slurs": "No slurs.", "not_an_admin": "Not an admin.", + "not_a_moderator": "Not a moderator.", "site_already_exists": "Site already exists.", "couldnt_update_site": "Couldn't update site.", "couldnt_find_that_username_or_email": From ca7d2feedbda7033a7aec884f081a822b3506602 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 15:32:15 -0400 Subject: [PATCH 04/10] Some GetUserDetails cleanup. --- server/Cargo.lock | 2 ++ server/lemmy_db/Cargo.toml | 4 ++- server/lemmy_db/src/lib.rs | 21 +++++++++++- server/lemmy_db/src/user.rs | 14 ++++++-- server/lemmy_utils/Cargo.toml | 2 +- server/lemmy_utils/src/lib.rs | 11 ------- server/src/api/claims.rs | 14 +------- server/src/api/user.rs | 12 +------ server/src/apub/user.rs | 3 +- ui/src/components/user-details.tsx | 20 ++++------- ui/src/components/user.tsx | 53 +++++++++++++++++------------- 11 files changed, 78 insertions(+), 78 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 1e0e04f8..44fb405e 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1549,7 +1549,9 @@ dependencies = [ "bcrypt", "chrono", "diesel", + "lazy_static", "log", + "regex", "serde 1.0.114", "serde_json", "sha2", diff --git a/server/lemmy_db/Cargo.toml b/server/lemmy_db/Cargo.toml index f10f217e..6d342c1e 100644 --- a/server/lemmy_db/Cargo.toml +++ b/server/lemmy_db/Cargo.toml @@ -13,4 +13,6 @@ strum_macros = "0.18.0" log = "0.4.0" sha2 = "0.9" bcrypt = "0.8.0" -url = { version = "2.1.1", features = ["serde"] } \ No newline at end of file +url = { version = "2.1.1", features = ["serde"] } +lazy_static = "1.3.0" +regex = "1.3.5" diff --git a/server/lemmy_db/src/lib.rs b/server/lemmy_db/src/lib.rs index 2eead841..cca2994b 100644 --- a/server/lemmy_db/src/lib.rs +++ b/server/lemmy_db/src/lib.rs @@ -2,9 +2,12 @@ pub extern crate diesel; #[macro_use] pub extern crate strum_macros; +#[macro_use] +pub extern crate lazy_static; pub extern crate bcrypt; pub extern crate chrono; pub extern crate log; +pub extern crate regex; pub extern crate serde; pub extern crate serde_json; pub extern crate sha2; @@ -12,6 +15,7 @@ pub extern crate strum; use chrono::NaiveDateTime; use diesel::{dsl::*, result::Error, *}; +use regex::Regex; use serde::{Deserialize, Serialize}; use std::{env, env::VarError}; @@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime { chrono::prelude::Utc::now().naive_utc() } +pub fn is_email_regex(test: &str) -> bool { + EMAIL_REGEX.is_match(test) +} + +lazy_static! { + static ref EMAIL_REGEX: Regex = + Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); +} + #[cfg(test)] mod tests { use super::fuzzy_search; - use crate::get_database_url_from_env; + use crate::{get_database_url_from_env, is_email_regex}; use diesel::{Connection, PgConnection}; pub fn establish_unpooled_connection() -> PgConnection { @@ -194,4 +207,10 @@ mod tests { let test = "This is a fuzzy search"; assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string()); } + + #[test] + fn test_email() { + assert!(is_email_regex("gush@gmail.com")); + assert!(!is_email_regex("nada_neutho")); + } } diff --git a/server/lemmy_db/src/user.rs b/server/lemmy_db/src/user.rs index 718067f9..e5389077 100644 --- a/server/lemmy_db/src/user.rs +++ b/server/lemmy_db/src/user.rs @@ -1,4 +1,5 @@ use crate::{ + is_email_regex, naive_now, schema::{user_, user_::dsl::*}, Crud, @@ -125,9 +126,18 @@ impl User_ { use crate::schema::user_::dsl::*; user_.filter(actor_id.eq(object_id)).first::(conn) } -} -impl User_ { + pub fn find_by_email_or_username( + conn: &PgConnection, + username_or_email: &str, + ) -> Result { + if is_email_regex(username_or_email) { + Self::find_by_email(conn, username_or_email) + } else { + Self::find_by_username(conn, username_or_email) + } + } + pub fn find_by_username(conn: &PgConnection, username: &str) -> Result { user_.filter(name.eq(username)).first::(conn) } diff --git a/server/lemmy_utils/Cargo.toml b/server/lemmy_utils/Cargo.toml index fed22f58..9685c0ed 100644 --- a/server/lemmy_utils/Cargo.toml +++ b/server/lemmy_utils/Cargo.toml @@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]} comrak = "0.7" lazy_static = "1.3.0" openssl = "0.10" -url = { version = "2.1.1", features = ["serde"] } \ No newline at end of file +url = { version = "2.1.1", features = ["serde"] } diff --git a/server/lemmy_utils/src/lib.rs b/server/lemmy_utils/src/lib.rs index d88335e2..6d851b03 100644 --- a/server/lemmy_utils/src/lib.rs +++ b/server/lemmy_utils/src/lib.rs @@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime { DateTime::::from_utc(datetime, *now.offset()) } -pub fn is_email_regex(test: &str) -> bool { - EMAIL_REGEX.is_match(test) -} - pub fn remove_slurs(test: &str) -> String { SLUR_REGEX.replace_all(test, "*removed*").to_string() } @@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool { #[cfg(test)] mod tests { use crate::{ - is_email_regex, is_valid_community_name, is_valid_post_title, is_valid_username, @@ -185,12 +180,6 @@ mod tests { assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string()); } - #[test] - fn test_email() { - assert!(is_email_regex("gush@gmail.com")); - assert!(!is_email_regex("nada_neutho")); - } - #[test] fn test_valid_register_username() { assert!(is_valid_username("Hello_98")); diff --git a/server/src/api/claims.rs b/server/src/api/claims.rs index eec9d1a7..9118714b 100644 --- a/server/src/api/claims.rs +++ b/server/src/api/claims.rs @@ -1,7 +1,7 @@ use diesel::{result::Error, PgConnection}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use lemmy_db::{user::User_, Crud}; -use lemmy_utils::{is_email_regex, settings::Settings}; +use lemmy_utils::settings::Settings; use serde::{Deserialize, Serialize}; type Jwt = String; @@ -54,18 +54,6 @@ impl Claims { .unwrap() } - // TODO: move these into user? - pub fn find_by_email_or_username( - conn: &PgConnection, - username_or_email: &str, - ) -> Result { - if is_email_regex(username_or_email) { - User_::find_by_email(conn, username_or_email) - } else { - User_::find_by_username(conn, username_or_email) - } - } - pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result { let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims; User_::read(&conn, claims.id) diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 2e621823..71fecea7 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse { moderates: Vec, comments: Vec, posts: Vec, - admins: Vec, // TODO why is this necessary, just use GetSite } #[derive(Serialize, Deserialize)] @@ -276,7 +275,7 @@ impl Perform for Oper { // Fetch that username / email let username_or_email = data.username_or_email.clone(); let user = match blocking(pool, move |conn| { - Claims::find_by_email_or_username(conn, &username_or_email) + User_::find_by_email_or_username(conn, &username_or_email) }) .await? { @@ -643,14 +642,6 @@ impl Perform for Oper { }) .await??; - let site_creator_id = - blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??; - - let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??; - let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); - let creator_user = admins.remove(creator_index); - admins.insert(0, creator_user); - // If its not the same user, remove the email, and settings // TODO an if let chain would be better here, but can't figure it out // TODO separate out settings into its own thing @@ -665,7 +656,6 @@ impl Perform for Oper { moderates, comments, posts, - admins, }) } } diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index 2a98670c..403c4d4a 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -1,5 +1,4 @@ use crate::{ - api::claims::Claims, apub::{ activities::send_activity, create_apub_response, @@ -253,7 +252,7 @@ pub async fn get_apub_user_http( ) -> Result, LemmyError> { let user_name = info.into_inner().user_name; let user = blocking(&db, move |conn| { - Claims::find_by_email_or_username(conn, &user_name) + User_::find_by_email_or_username(conn, &user_name) }) .await??; let u = user.to_apub(&db).await?; diff --git a/ui/src/components/user-details.tsx b/ui/src/components/user-details.tsx index 49b9589e..5f2346a2 100644 --- a/ui/src/components/user-details.tsx +++ b/ui/src/components/user-details.tsx @@ -1,7 +1,7 @@ import { Component, linkEvent } from 'inferno'; import { WebSocketService, UserService } from '../services'; import { Subscription } from 'rxjs'; -import { retryWhen, delay, take, last } from 'rxjs/operators'; +import { retryWhen, delay, take } from 'rxjs/operators'; import { i18n } from '../i18next'; import { UserOperation, @@ -16,7 +16,6 @@ import { CommentResponse, BanUserResponse, PostResponse, - AddAdminResponse, } from '../interfaces'; import { wsJsonToRes, @@ -41,6 +40,7 @@ interface UserDetailsProps { enableNsfw: boolean; view: UserDetailsView; onPageChange(page: number): number | any; + admins: Array; } interface UserDetailsState { @@ -49,7 +49,6 @@ interface UserDetailsState { comments: Array; posts: Array; saved?: Array; - admins: Array; } export class UserDetails extends Component { @@ -63,7 +62,6 @@ export class UserDetails extends Component { comments: [], posts: [], saved: [], - admins: [], }; this.subscription = WebSocketService.Instance.subject @@ -152,7 +150,7 @@ export class UserDetails extends Component { {i.type === 'posts' ? ( { ) : ( {
{ {this.state.posts.map(post => ( { follows: data.follows, moderates: data.moderates, posts: data.posts, - admins: data.admins, }); } else if (res.op == UserOperation.CreateCommentLike) { const data = res.data as CommentResponse; @@ -298,11 +295,6 @@ export class UserDetails extends Component { posts: this.state.posts, comments: this.state.comments, }); - } else if (res.op == UserOperation.AddAdmin) { - const data = res.data as AddAdminResponse; - this.setState({ - admins: data.admins, - }); } } } diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index 468d2980..8ff5c392 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -13,9 +13,9 @@ import { DeleteAccountForm, WebSocketJsonResponse, GetSiteResponse, - Site, UserDetailsView, UserDetailsResponse, + AddAdminResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { @@ -54,7 +54,7 @@ interface UserState { deleteAccountLoading: boolean; deleteAccountShowConfirm: boolean; deleteAccountForm: DeleteAccountForm; - site: Site; + siteRes: GetSiteResponse; } interface UserProps { @@ -114,19 +114,24 @@ export class User extends Component { deleteAccountForm: { password: null, }, - site: { - id: undefined, - name: undefined, - creator_id: undefined, - published: undefined, - creator_name: undefined, - number_of_users: undefined, - number_of_posts: undefined, - number_of_comments: undefined, - number_of_communities: undefined, - enable_downvotes: undefined, - open_registration: undefined, - enable_nsfw: undefined, + siteRes: { + admins: [], + banned: [], + online: undefined, + site: { + id: undefined, + name: undefined, + creator_id: undefined, + published: undefined, + creator_name: undefined, + number_of_users: undefined, + number_of_posts: undefined, + number_of_comments: undefined, + number_of_communities: undefined, + enable_downvotes: undefined, + open_registration: undefined, + enable_nsfw: undefined, + }, }, }; @@ -201,7 +206,7 @@ export class User extends Component { // Couldnt get a refresh working. This does for now. location.reload(); } - document.title = `/u/${this.state.username} - ${this.state.site.name}`; + document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`; setupTippy(); } @@ -236,8 +241,9 @@ export class User extends Component { sort={SortType[this.state.sort]} page={this.state.page} limit={fetchLimit} - enableDownvotes={this.state.site.enable_downvotes} - enableNsfw={this.state.site.enable_nsfw} + enableDownvotes={this.state.siteRes.site.enable_downvotes} + enableNsfw={this.state.siteRes.site.enable_nsfw} + admins={this.state.siteRes.admins} view={this.state.view} onPageChange={this.handlePageChange} /> @@ -637,7 +643,7 @@ export class User extends Component { />
- {this.state.site.enable_nsfw && ( + {this.state.siteRes.site.enable_nsfw && (
{ this.context.router.history.push('/'); } else if (res.op == UserOperation.GetSite) { const data = res.data as GetSiteResponse; - this.setState({ - site: data.site, - }); + this.state.siteRes = data; + this.setState(this.state); + } else if (res.op == UserOperation.AddAdmin) { + const data = res.data as AddAdminResponse; + this.state.siteRes.admins = data.admins; + this.setState(this.state); } } } From fd96dfdb5e1f3459fa9400fe464235031663af07 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 21:37:44 -0400 Subject: [PATCH 05/10] Added comment delete, remove, read. --- docs/src/contributing_websocket_http_api.md | 96 +++- server/lemmy_db/src/comment.rs | 48 +- server/lemmy_db/src/community.rs | 19 +- server/lemmy_db/src/private_message.rs | 4 +- server/src/api/comment.rs | 532 +++++++++++++------- server/src/api/community.rs | 14 - server/src/api/user.rs | 4 +- server/src/apub/shared_inbox.rs | 4 +- server/src/routes/api.rs | 6 + server/src/websocket/mod.rs | 3 + server/src/websocket/server.rs | 3 + ui/src/api_tests/api.spec.ts | 39 +- ui/src/components/comment-node.tsx | 30 +- ui/src/components/community.tsx | 6 +- ui/src/components/inbox.tsx | 9 +- ui/src/components/main.tsx | 6 +- ui/src/components/post.tsx | 16 +- ui/src/components/user-details.tsx | 6 +- ui/src/interfaces.ts | 30 +- ui/src/services/WebSocketService.ts | 30 +- 20 files changed, 626 insertions(+), 279 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 4931ed58..390fa988 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1448,7 +1448,6 @@ Mods and admins can remove and lock a post, creators can delete it. data: { content: String, parent_id: Option, - edit_id: Option, post_id: i32, auth: String } @@ -1470,7 +1469,7 @@ Mods and admins can remove and lock a post, creators can delete it. #### Edit Comment -Mods and admins can remove a comment, creators can delete it. +Only the creator can edit the comment. ##### Request ```rust @@ -1478,15 +1477,8 @@ Mods and admins can remove a comment, creators can delete it. op: "EditComment", data: { content: String, - parent_id: Option, edit_id: i32, - creator_id: i32, - post_id: i32, - removed: Option, - deleted: Option, - reason: Option, - read: Option, - auth: String + auth: String, } } ``` @@ -1503,6 +1495,89 @@ Mods and admins can remove a comment, creators can delete it. `PUT /comment` +#### Delete Comment + +Only the creator can delete the comment. + +##### Request +```rust +{ + op: "DeleteComment", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeleteComment", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/delete` + + +#### Remove Comment + +Only a mod or admin can remove the comment. + +##### Request +```rust +{ + op: "RemoveComment", + data: { + edit_id: i32, + removed: bool, + reason: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "RemoveComment", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/remove` + +#### Mark Comment as Read +##### Request +```rust +{ + op: "MarkCommentAsRead", + data: { + edit_id: i32, + read: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "MarkCommentAsRead", + data: { + comment: CommentView + } +} +``` +##### HTTP + +`POST /comment/mark_as_read` + #### Save Comment ##### Request ```rust @@ -1538,7 +1613,6 @@ Mods and admins can remove a comment, creators can delete it. op: "CreateCommentLike", data: { comment_id: i32, - post_id: i32, score: i16, auth: String } diff --git a/server/lemmy_db/src/comment.rs b/server/lemmy_db/src/comment.rs index dc369c8b..de690413 100644 --- a/server/lemmy_db/src/comment.rs +++ b/server/lemmy_db/src/comment.rs @@ -97,14 +97,6 @@ impl Comment { comment.filter(ap_id.eq(object_id)).first::(conn) } - pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result { - use crate::schema::comment::dsl::*; - - diesel::update(comment.find(comment_id)) - .set(read.eq(true)) - .get_result::(conn) - } - pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result { use crate::schema::comment::dsl::*; @@ -116,6 +108,46 @@ impl Comment { )) .get_result::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + comment_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + comment_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn update_content( + conn: &PgConnection, + comment_id: i32, + new_content: &str, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set((content.eq(new_content), updated.eq(naive_now()))) + .get_result::(conn) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index 4fe507f7..03c47e46 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -1,4 +1,5 @@ use crate::{ + naive_now, schema::{community, community_follower, community_moderator, community_user_ban}, Bannable, Crud, @@ -29,7 +30,6 @@ pub struct Community { pub last_refreshed_at: chrono::NaiveDateTime, } -// TODO add better delete, remove, lock actions here. #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)] #[table_name = "community"] pub struct CommunityForm { @@ -129,9 +129,24 @@ impl Community { ) -> Result { use crate::schema::community::dsl::*; diesel::update(community.find(community_id)) - .set(creator_id.eq(new_creator_id)) + .set((creator_id.eq(new_creator_id), updated.eq(naive_now()))) .get_result::(conn) } + + pub fn community_mods_and_admins( + conn: &PgConnection, + community_id: i32, + ) -> Result, Error> { + use crate::{community_view::CommunityModeratorView, user_view::UserView}; + let mut mods_and_admins: Vec = Vec::new(); + mods_and_admins.append( + &mut CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect())?, + ); + mods_and_admins + .append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); + Ok(mods_and_admins) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/lemmy_db/src/private_message.rs b/server/lemmy_db/src/private_message.rs index 30f40e6b..3486cf54 100644 --- a/server/lemmy_db/src/private_message.rs +++ b/server/lemmy_db/src/private_message.rs @@ -1,4 +1,4 @@ -use crate::{schema::private_message, Crud}; +use crate::{naive_now, schema::private_message, Crud}; use diesel::{dsl::*, result::Error, *}; use serde::{Deserialize, Serialize}; @@ -88,7 +88,7 @@ impl PrivateMessage { ) -> Result { use crate::schema::private_message::dsl::*; diesel::update(private_message.find(private_message_id)) - .set(content.eq(new_content)) + .set((content.eq(new_content), updated.eq(naive_now()))) .get_result::(conn) } diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index f8bdf5d5..1a06032b 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -13,14 +13,13 @@ use crate::{ use lemmy_db::{ comment::*, comment_view::*, + community::Community, community_view::*, moderator::*, - naive_now, post::*, site_view::*, user::*, user_mention::*, - user_view::*, Crud, Likeable, ListingType, @@ -44,7 +43,6 @@ use std::str::FromStr; pub struct CreateComment { content: String, parent_id: Option, - edit_id: Option, // TODO this isn't used pub post_id: i32, auth: String, } @@ -52,14 +50,29 @@ pub struct CreateComment { #[derive(Serialize, Deserialize)] pub struct EditComment { content: String, - parent_id: Option, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change edit_id: i32, - creator_id: i32, - pub post_id: i32, - removed: Option, - deleted: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeleteComment { + edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RemoveComment { + edit_id: i32, + removed: bool, reason: Option, - read: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MarkCommentAsRead { + edit_id: i32, + read: bool, auth: String, } @@ -79,7 +92,6 @@ pub struct CommentResponse { #[derive(Serialize, Deserialize)] pub struct CreateCommentLike { comment_id: i32, - pub post_id: i32, score: i16, auth: String, } @@ -150,6 +162,7 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + // Create the comment let comment_form2 = comment_form.clone(); let inserted_comment = match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? { @@ -157,6 +170,7 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), }; + // Necessary to update the ap_id let inserted_comment_id = inserted_comment.id; let updated_comment: Comment = match blocking(pool, move |conn| { let apub_id = @@ -175,8 +189,15 @@ impl Perform for Oper { // Scan the comment for user mentions, add those rows let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = - send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?; + let recipient_ids = send_local_notifs( + mentions, + updated_comment.clone(), + user.clone(), + post, + pool, + true, + ) + .await?; // You like your own comment by default let like_form = CommentLikeForm { @@ -237,122 +258,34 @@ impl Perform for Oper { let user_id = claims.id; - let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??; - let edit_id = data.edit_id; let orig_comment = blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; - let mut editors: Vec = vec![orig_comment.creator_id]; - let mut moderators: Vec = vec![]; - - let community_id = orig_comment.community_id; - moderators.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(&conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - moderators.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - - editors.extend(&moderators); - // You are allowed to mark the comment as read even if you're banned. - if data.read.is_none() { - // Verify its the creator or a mod, or an admin - - if !editors.contains(&user_id) { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - - // Check for a community ban - let community_id = orig_comment.community_id; - let is_banned = - move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); - if blocking(pool, is_banned).await? { - return Err(APIError::err("community_ban").into()); - } - - // Check for a site ban - if user.banned { - return Err(APIError::err("site_ban").into()); - } - } else { - // check that user can mark as read - let parent_id = orig_comment.parent_id; - match parent_id { - Some(pid) => { - let parent_comment = - blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; - if user_id != parent_comment.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - None => { - let parent_post_id = orig_comment.post_id; - let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; - if user_id != parent_post.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - } + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); } + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the creator can edit + if user_id != orig_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + + // Do the update let content_slurs_removed = remove_slurs(&data.content.to_owned()); - let edit_id = data.edit_id; - let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??; - - let comment_form = { - if data.read.is_none() { - // the ban etc checks should been made and have passed - // the comment can be properly edited - let post_removed = if moderators.contains(&user_id) { - data.removed - } else { - Some(read_comment.removed) - }; - - CommentForm { - content: content_slurs_removed, - parent_id: read_comment.parent_id, - post_id: read_comment.post_id, - creator_id: read_comment.creator_id, - removed: post_removed.to_owned(), - deleted: data.deleted.to_owned(), - read: Some(read_comment.read), - published: None, - updated: Some(naive_now()), - ap_id: read_comment.ap_id, - local: read_comment.local, - } - } else { - // the only field that can be updated it the read field - CommentForm { - content: read_comment.content, - parent_id: read_comment.parent_id, - post_id: read_comment.post_id, - creator_id: read_comment.creator_id, - removed: Some(read_comment.removed).to_owned(), - deleted: Some(read_comment.deleted).to_owned(), - read: data.read.to_owned(), - published: None, - updated: orig_comment.updated, - ap_id: read_comment.ap_id, - local: read_comment.local, - } - } - }; - - let edit_id = data.edit_id; - let comment_form2 = comment_form.clone(); let updated_comment = match blocking(pool, move |conn| { - Comment::update(conn, edit_id, &comment_form2) + Comment::update_content(conn, edit_id, &content_slurs_removed) }) .await? { @@ -360,54 +293,19 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; - if data.read.is_none() { - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_comment - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_comment - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else if let Some(removed) = data.removed.to_owned() { - if moderators.contains(&user_id) { - if removed { - updated_comment - .send_remove(&user, &self.client, pool) - .await?; - } else { - updated_comment - .send_undo_remove(&user, &self.client, pool) - .await?; - } - } - } else { - updated_comment - .send_update(&user, &self.client, pool) - .await?; - } + // Send the apub update + updated_comment + .send_update(&user, &self.client, pool) + .await?; - // Mod tables - if moderators.contains(&user_id) { - if let Some(removed) = data.removed.to_owned() { - let form = ModRemoveCommentForm { - mod_user_id: user_id, - comment_id: data.edit_id, - removed: Some(removed), - reason: data.reason.to_owned(), - }; - blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??; - } - } - } - - let post_id = data.post_id; + // Do the mentions / recipients + let post_id = orig_comment.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; - let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; + let updated_comment_content = updated_comment.content.to_owned(); + let mentions = scrape_text_for_mentions(&updated_comment_content); + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; let edit_id = data.edit_id; let comment_view = blocking(pool, move |conn| { @@ -436,6 +334,294 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeleteComment = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the creator can delete + if user_id != orig_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + + // Do the delete + let deleted = data.deleted; + let updated_comment = match blocking(pool, move |conn| { + Comment::update_deleted(conn, edit_id, deleted) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Send the apub message + if deleted { + updated_comment + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_comment + .send_undo_delete(&user, &self.client, pool) + .await?; + } + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + // Build the recipients + let post_id = comment_view.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let mentions = vec![]; + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; + + let mut res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendComment { + op: UserOperation::DeleteComment, + comment: res.clone(), + my_id: ws.id, + }); + + // strip out the recipient_ids, so that + // users don't get double notifs + res.recipient_ids = Vec::new(); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &RemoveComment = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only a mod or admin can remove + let mods_and_admins = blocking(pool, move |conn| { + Community::community_mods_and_admins(conn, community_id) + }) + .await??; + if !mods_and_admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Do the remove + let removed = data.removed; + let updated_comment = match blocking(pool, move |conn| { + Comment::update_removed(conn, edit_id, removed) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Mod tables + let form = ModRemoveCommentForm { + mod_user_id: user_id, + comment_id: data.edit_id, + removed: Some(removed), + reason: data.reason.to_owned(), + }; + blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??; + + // Send the apub message + if removed { + updated_comment + .send_remove(&user, &self.client, pool) + .await?; + } else { + updated_comment + .send_undo_remove(&user, &self.client, pool) + .await?; + } + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + // Build the recipients + let post_id = comment_view.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; + let mentions = vec![]; + let recipient_ids = + send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; + + let mut res = CommentResponse { + comment: comment_view, + recipient_ids, + }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendComment { + op: UserOperation::RemoveComment, + comment: res.clone(), + my_id: ws.id, + }); + + // strip out the recipient_ids, so that + // users don't get double notifs + res.recipient_ids = Vec::new(); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + _websocket_info: Option, + ) -> Result { + let data: &MarkCommentAsRead = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_comment.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the recipient can mark as read + // Needs to fetch the parent comment / post to get the recipient + let parent_id = orig_comment.parent_id; + match parent_id { + Some(pid) => { + let parent_comment = + blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; + if user_id != parent_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + None => { + let parent_post_id = orig_comment.post_id; + let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; + if user_id != parent_post.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + } + + // Do the mark as read + let read = data.read; + match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = CommentResponse { + comment: comment_view, + recipient_ids: Vec::new(), + }; + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; @@ -512,8 +698,12 @@ impl Perform for Oper { } } + let comment_id = data.comment_id; + let orig_comment = + blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??; + // Check for a community ban - let post_id = data.post_id; + let post_id = orig_comment.post_id; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let community_id = post.community_id; let is_banned = @@ -550,7 +740,7 @@ impl Perform for Oper { let like_form = CommentLikeForm { comment_id: data.comment_id, - post_id: data.post_id, + post_id, user_id, score: data.score, }; @@ -675,9 +865,10 @@ pub async fn send_local_notifs( user: User_, post: Post, pool: &DbPool, + do_send_email: bool, ) -> Result, LemmyError> { let ids = blocking(pool, move |conn| { - do_send_local_notifs(conn, &mentions, &comment, &user, &post) + do_send_local_notifs(conn, &mentions, &comment, &user, &post, do_send_email) }) .await?; @@ -690,6 +881,7 @@ fn do_send_local_notifs( comment: &Comment, user: &User_, post: &Post, + do_send_email: bool, ) -> Vec { let mut recipient_ids = Vec::new(); let hostname = &format!("https://{}", Settings::get().hostname); @@ -720,7 +912,7 @@ fn do_send_local_notifs( }; // Send an email to those users that have notifications on - if mention_user.send_notifications_to_email { + if do_send_email && mention_user.send_notifications_to_email { if let Some(mention_email) = mention_user.email { let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,); let html = &format!( @@ -744,7 +936,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) { recipient_ids.push(parent_user.id); - if parent_user.send_notifications_to_email { + if do_send_email && parent_user.send_notifications_to_email { if let Some(comment_reply_email) = parent_user.email { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( @@ -767,7 +959,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, post.creator_id) { recipient_ids.push(parent_user.id); - if parent_user.send_notifications_to_email { + if do_send_email && parent_user.send_notifications_to_email { if let Some(post_reply_email) = parent_user.email { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 1f46c596..5e84bc6c 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -10,7 +10,6 @@ use crate::{ }, DbPool, }; -use diesel::PgConnection; use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType}; use lemmy_utils::{ generate_actor_keypair, @@ -1078,16 +1077,3 @@ impl Perform for Oper { }) } } - -pub fn community_mods_and_admins( - conn: &PgConnection, - community_id: i32, -) -> Result, LemmyError> { - let mut editors: Vec = Vec::new(); - editors.append( - &mut CommunityModeratorView::for_community(conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect())?, - ); - editors.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); - Ok(editors) -} diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 71fecea7..ec61c658 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -936,9 +936,11 @@ impl Perform for Oper { .await??; // TODO: this should probably be a bulk operation + // Not easy to do as a bulk operation, + // because recipient_id isn't in the comment table for reply in &replies { let reply_id = reply.id; - let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id); + let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true); if blocking(pool, mark_as_read).await?.is_err() { return Err(APIError::err("couldnt_update_comment").into()); } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 8d6b255d..41d1a80e 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -393,7 +393,7 @@ async fn receive_create_comment( // anyway. let mentions = scrape_text_for_mentions(&inserted_comment.content); let recipient_ids = - send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?; + send_local_notifs(mentions, inserted_comment.clone(), user, post, pool, true).await?; // Refetch the view let comment_view = blocking(pool, move |conn| { @@ -558,7 +558,7 @@ async fn receive_update_comment( let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let mentions = scrape_text_for_mentions(&updated_comment.content); - let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; + let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool, false).await?; // Refetch the view let comment_view = diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 4722fb81..9fc84f4c 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -83,6 +83,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("", web::post().to(route_post::)) .route("", web::put().to(route_post::)) + .route("/delete", web::post().to(route_post::)) + .route("/remove", web::post().to(route_post::)) + .route( + "/mark_as_read", + web::post().to(route_post::), + ) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)), ) diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index c4c02146..ed8ee272 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -28,6 +28,9 @@ pub enum UserOperation { GetCommunity, CreateComment, EditComment, + DeleteComment, + RemoveComment, + MarkCommentAsRead, SaveComment, CreateCommentLike, GetPosts, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 0344e1b9..6f0516ff 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -506,6 +506,9 @@ impl ChatServer { // Comment ops UserOperation::CreateComment => do_user_operation::(args).await, UserOperation::EditComment => do_user_operation::(args).await, + UserOperation::DeleteComment => do_user_operation::(args).await, + UserOperation::RemoveComment => do_user_operation::(args).await, + UserOperation::MarkCommentAsRead => do_user_operation::(args).await, UserOperation::SaveComment => do_user_operation::(args).await, UserOperation::GetComments => do_user_operation::(args).await, UserOperation::CreateCommentLike => do_user_operation::(args).await, diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 891654b2..f3cc8673 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -11,6 +11,8 @@ import { GetFollowedCommunitiesResponse, GetPostResponse, CommentForm, + DeleteCommentForm, + RemoveCommentForm, CommentResponse, CommunityForm, DeleteCommunityForm, @@ -383,7 +385,6 @@ describe('main', () => { let unlikeCommentForm: CommentLikeForm = { comment_id: createResponse.comment.id, score: 0, - post_id: 2, auth: lemmyAlphaAuth, }; @@ -621,19 +622,16 @@ describe('main', () => { expect(createCommentRes.comment.content).toBe(commentContent); // lemmy_beta deletes the comment - let deleteCommentForm: CommentForm = { - content: commentContent, + let deleteCommentForm: DeleteCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, deleted: true, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let deleteCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -650,19 +648,16 @@ describe('main', () => { expect(getPostRes.comments[0].deleted).toBe(true); // lemmy_beta undeletes the comment - let undeleteCommentForm: CommentForm = { - content: commentContent, + let undeleteCommentForm: DeleteCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, deleted: false, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let undeleteCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -889,19 +884,16 @@ describe('main', () => { expect(createCommentRes.comment.content).toBe(commentContent); // lemmy_beta removes the comment - let removeCommentForm: CommentForm = { - content: commentContent, + let removeCommentForm: RemoveCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, removed: true, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let removeCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -918,19 +910,16 @@ describe('main', () => { expect(getPostRes.comments[0].removed).toBe(true); // lemmy_beta undeletes the comment - let unremoveCommentForm: CommentForm = { - content: commentContent, + let unremoveCommentForm: RemoveCommentForm = { edit_id: createCommentRes.comment.id, - post_id: createPostRes.post.id, removed: false, auth: lemmyBetaAuth, - creator_id: createCommentRes.comment.creator_id, }; let unremoveCommentRes: CommentResponse = await fetch( - `${lemmyBetaApiUrl}/comment`, + `${lemmyBetaApiUrl}/comment/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index dfe52ec1..527cad89 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -3,7 +3,9 @@ import { Link } from 'inferno-router'; import { CommentNode as CommentNodeI, CommentLikeForm, - CommentForm as CommentFormI, + DeleteCommentForm, + RemoveCommentForm, + MarkCommentAsReadForm, MarkUserMentionAsReadForm, SaveCommentForm, BanFromCommunityForm, @@ -848,16 +850,12 @@ export class CommentNode extends Component { } handleDeleteClick(i: CommentNode) { - let deleteForm: CommentFormI = { - content: i.props.node.comment.content, + let deleteForm: DeleteCommentForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, deleted: !i.props.node.comment.deleted, auth: null, }; - WebSocketService.Instance.editComment(deleteForm); + WebSocketService.Instance.deleteComment(deleteForm); } handleSaveCommentClick(i: CommentNode) { @@ -901,7 +899,6 @@ export class CommentNode extends Component { let form: CommentLikeForm = { comment_id: i.comment.id, - post_id: i.comment.post_id, score: this.state.my_vote, }; @@ -929,7 +926,6 @@ export class CommentNode extends Component { let form: CommentLikeForm = { comment_id: i.comment.id, - post_id: i.comment.post_id, score: this.state.my_vote, }; @@ -950,17 +946,13 @@ export class CommentNode extends Component { handleModRemoveSubmit(i: CommentNode) { event.preventDefault(); - let form: CommentFormI = { - content: i.props.node.comment.content, + let form: RemoveCommentForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, removed: !i.props.node.comment.removed, reason: i.state.removeReason, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.removeComment(form); i.state.showRemoveDialog = false; i.setState(i.state); @@ -975,16 +967,12 @@ export class CommentNode extends Component { }; WebSocketService.Instance.markUserMentionAsRead(form); } else { - let form: CommentFormI = { - content: i.props.node.comment.content, + let form: MarkCommentAsReadForm = { edit_id: i.props.node.comment.id, - creator_id: i.props.node.comment.creator_id, - post_id: i.props.node.comment.post_id, - parent_id: i.props.node.comment.parent_id, read: !i.props.node.comment.read, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.markCommentAsRead(form); } i.state.readLoading = true; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 2899c2cb..66eaf96e 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -409,7 +409,11 @@ export class Community extends Component { this.state.comments = data.comments; this.state.loading = false; this.setState(this.state); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 5609879c..66a3d676 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -484,9 +484,16 @@ export class Inbox extends Component { this.setState(this.state); } else if (res.op == UserOperation.MarkAllAsRead) { // Moved to be instant - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.replies); + this.setState(this.state); + } else if (res.op == UserOperation.MarkCommentAsRead) { + let data = res.data as CommentResponse; // If youre in the unread view, just remove it from the list if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) { diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 0392090a..d203cd08 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -701,7 +701,11 @@ export class Main extends Component { this.state.comments = data.comments; this.state.loading = false; this.setState(this.state); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 97f80b6e..91ffeb19 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -8,7 +8,7 @@ import { GetPostResponse, PostResponse, Comment, - CommentForm as CommentFormI, + MarkCommentAsReadForm, CommentResponse, CommentSortType, CommentViewType, @@ -167,16 +167,12 @@ export class Post extends Component { UserService.Instance.user && UserService.Instance.user.id == parent_user_id ) { - let form: CommentFormI = { - content: found.content, + let form: MarkCommentAsReadForm = { edit_id: found.id, - creator_id: found.creator_id, - post_id: found.post_id, - parent_id: found.parent_id, read: true, auth: null, }; - WebSocketService.Instance.editComment(form); + WebSocketService.Instance.markCommentAsRead(form); UserService.Instance.user.unreadCount--; UserService.Instance.sub.next({ user: UserService.Instance.user, @@ -435,7 +431,11 @@ export class Post extends Component { this.state.comments.unshift(data.comment); this.setState(this.state); } - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { let data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState(this.state); diff --git a/ui/src/components/user-details.tsx b/ui/src/components/user-details.tsx index 5f2346a2..339dc5e4 100644 --- a/ui/src/components/user-details.tsx +++ b/ui/src/components/user-details.tsx @@ -257,7 +257,11 @@ export class UserDetails extends Component { this.setState({ comments: this.state.comments, }); - } else if (res.op == UserOperation.EditComment) { + } else if ( + res.op == UserOperation.EditComment || + res.op == UserOperation.DeleteComment || + res.op == UserOperation.RemoveComment + ) { const data = res.data as CommentResponse; editCommentRes(data, this.state.comments); this.setState({ diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 7f650f1b..6006f2ee 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -9,6 +9,9 @@ export enum UserOperation { GetCommunity, CreateComment, EditComment, + DeleteComment, + RemoveComment, + MarkCommentAsRead, SaveComment, CreateCommentLike, GetPosts, @@ -679,14 +682,29 @@ export interface PostResponse { export interface CommentForm { content: string; - post_id: number; + post_id?: number; parent_id?: number; edit_id?: number; creator_id?: number; - removed?: boolean; - deleted?: boolean; + auth: string; +} + +export interface DeleteCommentForm { + edit_id: number; + deleted: boolean; + auth: string; +} + +export interface RemoveCommentForm { + edit_id: number; + removed: boolean; reason?: string; - read?: boolean; + auth: string; +} + +export interface MarkCommentAsReadForm { + edit_id: number; + read: boolean; auth: string; } @@ -703,7 +721,6 @@ export interface CommentResponse { export interface CommentLikeForm { comment_id: number; - post_id: number; score: number; auth?: string; } @@ -901,6 +918,9 @@ export type MessageType = | GetPostsForm | GetCommunityForm | CommentForm + | DeleteCommentForm + | RemoveCommentForm + | MarkCommentAsReadForm | CommentLikeForm | SaveCommentForm | CreatePostLikeForm diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 26e58135..2c85425d 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -9,6 +9,9 @@ import { PostForm, SavePostForm, CommentForm, + DeleteCommentForm, + RemoveCommentForm, + MarkCommentAsReadForm, SaveCommentForm, CommentLikeForm, GetPostForm, @@ -165,14 +168,29 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form)); } - public createComment(commentForm: CommentForm) { - this.setAuth(commentForm); - this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm)); + public createComment(form: CommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form)); } - public editComment(commentForm: CommentForm) { - this.setAuth(commentForm); - this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm)); + public editComment(form: CommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.EditComment, form)); + } + + public deleteComment(form: DeleteCommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeleteComment, form)); + } + + public removeComment(form: RemoveCommentForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.RemoveComment, form)); + } + + public markCommentAsRead(form: MarkCommentAsReadForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.MarkCommentAsRead, form)); } public likeComment(form: CommentLikeForm) { From 2eac0374085a7e45adde54819f5c855c65ef5052 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 20 Jul 2020 23:46:36 -0400 Subject: [PATCH 06/10] Adding post delete, remove, lock, and sticky. --- docs/src/contributing_websocket_http_api.md | 129 ++++- server/lemmy_db/src/post.rs | 40 ++ server/src/api/comment.rs | 5 + server/src/api/post.rs | 523 +++++++++++++++----- server/src/routes/api.rs | 4 + server/src/websocket/mod.rs | 4 + server/src/websocket/server.rs | 4 + ui/src/api_tests/api.spec.ts | 84 ++-- ui/src/components/community.tsx | 8 +- ui/src/components/post-form.tsx | 4 - ui/src/components/post-listing.tsx | 40 +- ui/src/components/post.tsx | 8 +- ui/src/interfaces.ts | 40 +- ui/src/services/WebSocketService.ts | 36 +- 14 files changed, 692 insertions(+), 237 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 390fa988..00e26b0d 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1271,8 +1271,9 @@ Only admins can remove a community. name: String, url: Option, body: Option, + nsfw: bool, community_id: i32, - auth: String + auth: String, } } ``` @@ -1378,25 +1379,17 @@ Post listing types are `All, Subscribed, Community` `POST /post/like` #### Edit Post - -Mods and admins can remove and lock a post, creators can delete it. - ##### Request ```rust { op: "EditPost", data: { edit_id: i32, - creator_id: i32, - community_id: i32, name: String, url: Option, body: Option, - removed: Option, - deleted: Option, - locked: Option, - reason: Option, - auth: String + nsfw: bool, + auth: String, } } ``` @@ -1414,6 +1407,120 @@ Mods and admins can remove and lock a post, creators can delete it. `PUT /post` +#### Delete Post +##### Request +```rust +{ + op: "DeletePost", + data: { + edit_id: i32, + deleted: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "DeletePost", + data: { + post: PostView + } +} +``` + +##### HTTP + +`POST /post/delete` + +#### Remove Post + +Only admins and mods can remove a post. + +##### Request +```rust +{ + op: "RemovePost", + data: { + edit_id: i32, + removed: bool, + reason: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "RemovePost", + data: { + post: PostView + } +} +``` + +##### HTTP + +`POST /post/remove` + +#### Lock Post + +Only admins and mods can lock a post. + +##### Request +```rust +{ + op: "LockPost", + data: { + edit_id: i32, + locked: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "LockPost", + data: { + post: PostView + } +} +``` + +##### HTTP + +`POST /post/lock` + +#### Sticky Post + +Only admins and mods can sticky a post. + +##### Request +```rust +{ + op: "StickyPost", + data: { + edit_id: i32, + stickied: bool, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "StickyPost", + data: { + post: PostView + } +} +``` + +##### HTTP + +`POST /post/sticky` + #### Save Post ##### Request ```rust diff --git a/server/lemmy_db/src/post.rs b/server/lemmy_db/src/post.rs index 66e24773..35b0fead 100644 --- a/server/lemmy_db/src/post.rs +++ b/server/lemmy_db/src/post.rs @@ -108,6 +108,46 @@ impl Post { )) .get_result::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + post_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + post_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(locked.eq(new_locked)) + .get_result::(conn) + } + + pub fn update_stickied( + conn: &PgConnection, + post_id: i32, + new_stickied: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(stickied.eq(new_stickied)) + .get_result::(conn) + } } impl Crud for Post { diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 1a06032b..79b7b2c7 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -162,6 +162,11 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + // Check if post is locked, no new comments + if post.locked { + return Err(APIError::err("locked").into()); + } + // Create the comment let comment_form2 = comment_form.clone(); let inserted_comment = diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 61f3513b..390d291c 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -13,6 +13,7 @@ use crate::{ }; use lemmy_db::{ comment_view::*, + community::*, community_view::*, moderator::*, naive_now, @@ -96,20 +97,42 @@ pub struct CreatePostLike { #[derive(Serialize, Deserialize)] pub struct EditPost { pub edit_id: i32, - creator_id: i32, - community_id: i32, name: String, url: Option, body: Option, - removed: Option, - deleted: Option, nsfw: bool, - locked: Option, - stickied: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct DeletePost { + pub edit_id: i32, + deleted: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct RemovePost { + pub edit_id: i32, + removed: bool, reason: Option, auth: String, } +#[derive(Serialize, Deserialize)] +pub struct LockPost { + pub edit_id: i32, + locked: bool, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct StickyPost { + pub edit_id: i32, + stickied: bool, + auth: String, +} + #[derive(Serialize, Deserialize)] pub struct SavePost { post_id: i32, @@ -549,35 +572,10 @@ impl Perform for Oper { let user_id = claims.id; let edit_id = data.edit_id; - let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; - - // Verify its the creator or a mod or admin - let community_id = read_post.community_id; - let mut editors: Vec = vec![read_post.creator_id]; - let mut moderators: Vec = vec![]; - - moderators.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - moderators.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - - editors.extend(&moderators); - - if !editors.contains(&user_id) { - return Err(APIError::err("no_post_edit_allowed").into()); - } + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; // Check for a community ban - let community_id = read_post.community_id; + let community_id = orig_post.community_id; let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); if blocking(pool, is_banned).await? { @@ -590,55 +588,34 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + // Verify that only the creator can edit + if user_id != orig_post.creator_id { + return Err(APIError::err("no_post_edit_allowed").into()); + } + // Fetch Iframely and Pictrs cached image let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; - let post_form = { - // only modify some properties if they are a moderator - if moderators.contains(&user_id) { - PostForm { - name: data.name.trim().to_owned(), - url: data.url.to_owned(), - body: data.body.to_owned(), - creator_id: read_post.creator_id.to_owned(), - community_id: read_post.community_id, - removed: data.removed.to_owned(), - deleted: data.deleted.to_owned(), - nsfw: data.nsfw, - locked: data.locked.to_owned(), - stickied: data.stickied.to_owned(), - updated: Some(naive_now()), - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, - thumbnail_url: pictrs_thumbnail, - ap_id: read_post.ap_id, - local: read_post.local, - published: None, - } - } else { - PostForm { - name: read_post.name.trim().to_owned(), - url: data.url.to_owned(), - body: data.body.to_owned(), - creator_id: read_post.creator_id.to_owned(), - community_id: read_post.community_id, - removed: Some(read_post.removed), - deleted: data.deleted.to_owned(), - nsfw: data.nsfw, - locked: Some(read_post.locked), - stickied: Some(read_post.stickied), - updated: Some(naive_now()), - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, - thumbnail_url: pictrs_thumbnail, - ap_id: read_post.ap_id, - local: read_post.local, - published: None, - } - } + let post_form = PostForm { + name: data.name.trim().to_owned(), + url: data.url.to_owned(), + body: data.body.to_owned(), + nsfw: data.nsfw, + creator_id: orig_post.creator_id.to_owned(), + community_id: orig_post.community_id, + removed: Some(orig_post.removed), + deleted: Some(orig_post.deleted), + locked: Some(orig_post.locked), + stickied: Some(orig_post.stickied), + updated: Some(naive_now()), + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail, + ap_id: orig_post.ap_id, + local: orig_post.local, + published: None, }; let edit_id = data.edit_id; @@ -656,58 +633,8 @@ impl Perform for Oper { } }; - if moderators.contains(&user_id) { - // Mod tables - if let Some(removed) = data.removed.to_owned() { - let form = ModRemovePostForm { - mod_user_id: user_id, - post_id: data.edit_id, - removed: Some(removed), - reason: data.reason.to_owned(), - }; - blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??; - } - - if let Some(locked) = data.locked.to_owned() { - let form = ModLockPostForm { - mod_user_id: user_id, - post_id: data.edit_id, - locked: Some(locked), - }; - blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??; - } - - if let Some(stickied) = data.stickied.to_owned() { - let form = ModStickyPostForm { - mod_user_id: user_id, - post_id: data.edit_id, - stickied: Some(stickied), - }; - blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??; - } - } - - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_post.send_delete(&user, &self.client, pool).await?; - } else { - updated_post - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else if let Some(removed) = data.removed.to_owned() { - if moderators.contains(&user_id) { - if removed { - updated_post.send_remove(&user, &self.client, pool).await?; - } else { - updated_post - .send_undo_remove(&user, &self.client, pool) - .await?; - } - } - } else { - updated_post.send_update(&user, &self.client, pool).await?; - } + // Send apub update + updated_post.send_update(&user, &self.client, pool).await?; let edit_id = data.edit_id; let post_view = blocking(pool, move |conn| { @@ -729,6 +656,342 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &DeletePost = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the creator can delete + if user_id != orig_post.creator_id { + return Err(APIError::err("no_post_edit_allowed").into()); + } + + // Update the post + let edit_id = data.edit_id; + let deleted = data.deleted; + let updated_post = blocking(pool, move |conn| { + Post::update_deleted(conn, edit_id, deleted) + }) + .await??; + + // apub updates + if deleted { + updated_post.send_delete(&user, &self.client, pool).await?; + } else { + updated_post + .send_undo_delete(&user, &self.client, pool) + .await?; + } + + // Refetch the post + let edit_id = data.edit_id; + let post_view = blocking(pool, move |conn| { + PostView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = PostResponse { post: post_view }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendPost { + op: UserOperation::DeletePost, + post: res.clone(), + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &RemovePost = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the mods can remove + let mods_and_admins = blocking(pool, move |conn| { + Community::community_mods_and_admins(conn, community_id) + }) + .await??; + if !mods_and_admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Update the post + let edit_id = data.edit_id; + let removed = data.removed; + let updated_post = blocking(pool, move |conn| { + Post::update_removed(conn, edit_id, removed) + }) + .await??; + + // Mod tables + let form = ModRemovePostForm { + mod_user_id: user_id, + post_id: data.edit_id, + removed: Some(removed), + reason: data.reason.to_owned(), + }; + blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??; + + // apub updates + if removed { + updated_post.send_remove(&user, &self.client, pool).await?; + } else { + updated_post + .send_undo_remove(&user, &self.client, pool) + .await?; + } + + // Refetch the post + let edit_id = data.edit_id; + let post_view = blocking(pool, move |conn| { + PostView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = PostResponse { post: post_view }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendPost { + op: UserOperation::RemovePost, + post: res.clone(), + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &LockPost = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the mods can lock + let mods_and_admins = blocking(pool, move |conn| { + Community::community_mods_and_admins(conn, community_id) + }) + .await??; + if !mods_and_admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Update the post + let edit_id = data.edit_id; + let locked = data.locked; + let updated_post = + blocking(pool, move |conn| Post::update_locked(conn, edit_id, locked)).await??; + + // Mod tables + let form = ModLockPostForm { + mod_user_id: user_id, + post_id: data.edit_id, + locked: Some(locked), + }; + blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??; + + // apub updates + updated_post.send_update(&user, &self.client, pool).await?; + + // Refetch the post + let edit_id = data.edit_id; + let post_view = blocking(pool, move |conn| { + PostView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = PostResponse { post: post_view }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendPost { + op: UserOperation::LockPost, + post: res.clone(), + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + let data: &StickyPost = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err("not_logged_in").into()), + }; + + let user_id = claims.id; + + let edit_id = data.edit_id; + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; + + // Check for a site ban + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if user.banned { + return Err(APIError::err("site_ban").into()); + } + + // Check for a community ban + let community_id = orig_post.community_id; + let is_banned = + move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + return Err(APIError::err("community_ban").into()); + } + + // Verify that only the mods can sticky + let mods_and_admins = blocking(pool, move |conn| { + Community::community_mods_and_admins(conn, community_id) + }) + .await??; + if !mods_and_admins.contains(&user_id) { + return Err(APIError::err("not_an_admin").into()); + } + + // Update the post + let edit_id = data.edit_id; + let stickied = data.stickied; + let updated_post = blocking(pool, move |conn| { + Post::update_stickied(conn, edit_id, stickied) + }) + .await??; + + // Mod tables + let form = ModStickyPostForm { + mod_user_id: user_id, + post_id: data.edit_id, + stickied: Some(stickied), + }; + blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??; + + // Apub updates + // TODO stickied should pry work like locked for ease of use + updated_post.send_update(&user, &self.client, pool).await?; + + // Refetch the post + let edit_id = data.edit_id; + let post_view = blocking(pool, move |conn| { + PostView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = PostResponse { post: post_view }; + + if let Some(ws) = websocket_info { + ws.chatserver.do_send(SendPost { + op: UserOperation::StickyPost, + post: res.clone(), + my_id: ws.id, + }); + } + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PostResponse; diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 9fc84f4c..31888156 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -73,6 +73,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .wrap(rate_limit.message()) .route("", web::get().to(route_get::)) .route("", web::put().to(route_post::)) + .route("/delete", web::post().to(route_post::)) + .route("/remove", web::post().to(route_post::)) + .route("/lock", web::post().to(route_post::)) + .route("/sticky", web::post().to(route_post::)) .route("/list", web::get().to(route_get::)) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)), diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index ed8ee272..5f3157b1 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -36,6 +36,10 @@ pub enum UserOperation { GetPosts, CreatePostLike, EditPost, + DeletePost, + RemovePost, + LockPost, + StickyPost, SavePost, EditCommunity, DeleteCommunity, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 6f0516ff..cc6de115 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -500,6 +500,10 @@ impl ChatServer { UserOperation::GetPost => do_user_operation::(args).await, UserOperation::GetPosts => do_user_operation::(args).await, UserOperation::EditPost => do_user_operation::(args).await, + UserOperation::DeletePost => do_user_operation::(args).await, + UserOperation::RemovePost => do_user_operation::(args).await, + UserOperation::LockPost => do_user_operation::(args).await, + UserOperation::StickyPost => do_user_operation::(args).await, UserOperation::CreatePostLike => do_user_operation::(args).await, UserOperation::SavePost => do_user_operation::(args).await, diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index f3cc8673..9ab9fc2a 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -4,6 +4,9 @@ import { LoginForm, LoginResponse, PostForm, + DeletePostForm, + RemovePostForm, + // TODO need to test LockPost and StickyPost federated PostResponse, SearchResponse, FollowCommunityForm, @@ -100,7 +103,6 @@ describe('main', () => { name, auth: lemmyAlphaAuth, community_id: 2, - creator_id: 2, nsfw: false, }; @@ -269,7 +271,6 @@ describe('main', () => { name, auth: lemmyAlphaAuth, community_id: 3, - creator_id: 2, nsfw: false, }; @@ -326,7 +327,6 @@ describe('main', () => { edit_id: 2, auth: lemmyAlphaAuth, community_id: 3, - creator_id: 2, nsfw: false, }; @@ -587,7 +587,6 @@ describe('main', () => { name: postName, auth: lemmyBetaAuth, community_id: createCommunityRes.community.id, - creator_id: 2, nsfw: false, }; @@ -673,23 +672,22 @@ describe('main', () => { expect(getPostUndeleteRes.comments[0].deleted).toBe(false); // lemmy_beta deletes the post - let deletePostForm: PostForm = { - name: postName, + let deletePostForm: DeletePostForm = { edit_id: createPostRes.post.id, - auth: lemmyBetaAuth, - community_id: createPostRes.post.community_id, - creator_id: createPostRes.post.creator_id, - nsfw: false, deleted: true, + auth: lemmyBetaAuth, }; - let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: wrapper(deletePostForm), - }).then(d => d.json()); + let deletePostRes: PostResponse = await fetch( + `${lemmyBetaApiUrl}/post/delete`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(deletePostForm), + } + ).then(d => d.json()); expect(deletePostRes.post.deleted).toBe(true); // Make sure lemmy_alpha sees the post is deleted @@ -699,20 +697,16 @@ describe('main', () => { expect(getPostResAgain.post.deleted).toBe(true); // lemmy_beta undeletes the post - let undeletePostForm: PostForm = { - name: postName, + let undeletePostForm: DeletePostForm = { edit_id: createPostRes.post.id, - auth: lemmyBetaAuth, - community_id: createPostRes.post.community_id, - creator_id: createPostRes.post.creator_id, - nsfw: false, deleted: false, + auth: lemmyBetaAuth, }; let undeletePostRes: PostResponse = await fetch( - `${lemmyBetaApiUrl}/post`, + `${lemmyBetaApiUrl}/post/delete`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -849,7 +843,6 @@ describe('main', () => { name: postName, auth: lemmyBetaAuth, community_id: createCommunityRes.community.id, - creator_id: 2, nsfw: false, }; @@ -935,23 +928,22 @@ describe('main', () => { expect(getPostUnremoveRes.comments[0].removed).toBe(false); // lemmy_beta deletes the post - let removePostForm: PostForm = { - name: postName, + let removePostForm: RemovePostForm = { edit_id: createPostRes.post.id, - auth: lemmyBetaAuth, - community_id: createPostRes.post.community_id, - creator_id: createPostRes.post.creator_id, - nsfw: false, removed: true, + auth: lemmyBetaAuth, }; - let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: wrapper(removePostForm), - }).then(d => d.json()); + let removePostRes: PostResponse = await fetch( + `${lemmyBetaApiUrl}/post/remove`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: wrapper(removePostForm), + } + ).then(d => d.json()); expect(removePostRes.post.removed).toBe(true); // Make sure lemmy_alpha sees the post is deleted @@ -961,20 +953,16 @@ describe('main', () => { expect(getPostResAgain.post.removed).toBe(true); // lemmy_beta unremoves the post - let unremovePostForm: PostForm = { - name: postName, + let unremovePostForm: RemovePostForm = { edit_id: createPostRes.post.id, - auth: lemmyBetaAuth, - community_id: createPostRes.post.community_id, - creator_id: createPostRes.post.creator_id, - nsfw: false, removed: false, + auth: lemmyBetaAuth, }; let unremovePostRes: PostResponse = await fetch( - `${lemmyBetaApiUrl}/post`, + `${lemmyBetaApiUrl}/post/remove`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', }, @@ -1226,7 +1214,6 @@ describe('main', () => { name: postName, auth: lemmyAlphaAuth, community_id: 2, - creator_id: 2, nsfw: false, }; @@ -1337,7 +1324,6 @@ describe('main', () => { name: betaPostName, auth: lemmyBetaAuth, community_id: 2, - creator_id: 2, nsfw: false, }; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 66eaf96e..f70fa4f7 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -380,7 +380,13 @@ export class Community extends Component { this.state.loading = false; this.setState(this.state); setupTippy(); - } else if (res.op == UserOperation.EditPost) { + } else if ( + res.op == UserOperation.EditPost || + res.op == UserOperation.DeletePost || + res.op == UserOperation.RemovePost || + res.op == UserOperation.LockPost || + res.op == UserOperation.StickyPost + ) { let data = res.data as PostResponse; editPostFindRes(data, this.state.posts); this.setState(this.state); diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 6656ef84..854cff6e 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -71,9 +71,6 @@ export class PostForm extends Component { nsfw: false, auth: null, community_id: null, - creator_id: UserService.Instance.user - ? UserService.Instance.user.id - : null, }, communities: [], loading: false, @@ -99,7 +96,6 @@ export class PostForm extends Component { name: this.props.post.name, community_id: this.props.post.community_id, edit_id: this.props.post.id, - creator_id: this.props.post.creator_id, url: this.props.post.url, nsfw: this.props.post.nsfw, auth: null, diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index a47aba99..e117a282 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services'; import { Post, CreatePostLikeForm, - PostForm as PostFormI, + DeletePostForm, + RemovePostForm, + LockPostForm, + StickyPostForm, SavePostForm, CommunityUser, UserView, @@ -33,7 +36,6 @@ import { setupTippy, hostname, previewLines, - toast, } from '../utils'; import { i18n } from '../i18next'; @@ -1114,18 +1116,12 @@ export class PostListing extends Component { } handleDeleteClick(i: PostListing) { - let deleteForm: PostFormI = { - body: i.props.post.body, - community_id: i.props.post.community_id, - name: i.props.post.name, - url: i.props.post.url, + let deleteForm: DeletePostForm = { edit_id: i.props.post.id, - creator_id: i.props.post.creator_id, deleted: !i.props.post.deleted, - nsfw: i.props.post.nsfw, auth: null, }; - WebSocketService.Instance.editPost(deleteForm); + WebSocketService.Instance.deletePost(deleteForm); } handleSavePostClick(i: PostListing) { @@ -1163,46 +1159,34 @@ export class PostListing extends Component { handleModRemoveSubmit(i: PostListing) { event.preventDefault(); - let form: PostFormI = { - name: i.props.post.name, - community_id: i.props.post.community_id, + let form: RemovePostForm = { edit_id: i.props.post.id, - creator_id: i.props.post.creator_id, removed: !i.props.post.removed, reason: i.state.removeReason, - nsfw: i.props.post.nsfw, auth: null, }; - WebSocketService.Instance.editPost(form); + WebSocketService.Instance.removePost(form); i.state.showRemoveDialog = false; i.setState(i.state); } handleModLock(i: PostListing) { - let form: PostFormI = { - name: i.props.post.name, - community_id: i.props.post.community_id, + let form: LockPostForm = { edit_id: i.props.post.id, - creator_id: i.props.post.creator_id, - nsfw: i.props.post.nsfw, locked: !i.props.post.locked, auth: null, }; - WebSocketService.Instance.editPost(form); + WebSocketService.Instance.lockPost(form); } handleModSticky(i: PostListing) { - let form: PostFormI = { - name: i.props.post.name, - community_id: i.props.post.community_id, + let form: StickyPostForm = { edit_id: i.props.post.id, - creator_id: i.props.post.creator_id, - nsfw: i.props.post.nsfw, stickied: !i.props.post.stickied, auth: null, }; - WebSocketService.Instance.editPost(form); + WebSocketService.Instance.stickyPost(form); } handleModBanFromCommunityShow(i: PostListing) { diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 91ffeb19..c811062f 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -452,7 +452,13 @@ export class Post extends Component { let data = res.data as PostResponse; createPostLikeRes(data, this.state.post); this.setState(this.state); - } else if (res.op == UserOperation.EditPost) { + } else if ( + res.op == UserOperation.EditPost || + res.op == UserOperation.DeletePost || + res.op == UserOperation.RemovePost || + res.op == UserOperation.LockPost || + res.op == UserOperation.StickyPost + ) { let data = res.data as PostResponse; this.state.post = data.post; this.setState(this.state); diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 6006f2ee..8dced1a6 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -17,6 +17,10 @@ export enum UserOperation { GetPosts, CreatePostLike, EditPost, + DeletePost, + RemovePost, + LockPost, + StickyPost, SavePost, EditCommunity, DeleteCommunity, @@ -636,19 +640,37 @@ export interface PostForm { name: string; url?: string; body?: string; - community_id: number; - updated?: number; + community_id?: number; edit_id?: number; - creator_id: number; - removed?: boolean; - deleted?: boolean; nsfw: boolean; - locked?: boolean; - stickied?: boolean; + auth: string; +} + +export interface DeletePostForm { + edit_id: number; + deleted: boolean; + auth: string; +} + +export interface RemovePostForm { + edit_id: number; + removed: boolean; reason?: string; auth: string; } +export interface LockPostForm { + edit_id: number; + locked: boolean; + auth: string; +} + +export interface StickyPostForm { + edit_id: number; + stickied: boolean; + auth: string; +} + export interface PostFormParams { name: string; url?: string; @@ -914,6 +936,10 @@ export type MessageType = | ListCommunitiesForm | GetFollowedCommunitiesForm | PostForm + | DeletePostForm + | RemovePostForm + | LockPostForm + | StickyPostForm | GetPostForm | GetPostsForm | GetCommunityForm diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 2c85425d..aabfc4dd 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -7,6 +7,10 @@ import { DeleteCommunityForm, RemoveCommunityForm, PostForm, + DeletePostForm, + RemovePostForm, + LockPostForm, + StickyPostForm, SavePostForm, CommentForm, DeleteCommentForm, @@ -153,9 +157,9 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {})); } - public createPost(postForm: PostForm) { - this.setAuth(postForm); - this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm)); + public createPost(form: PostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form)); } public getPost(form: GetPostForm) { @@ -218,9 +222,29 @@ export class WebSocketService { this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form)); } - public editPost(postForm: PostForm) { - this.setAuth(postForm); - this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm)); + public editPost(form: PostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.EditPost, form)); + } + + public deletePost(form: DeletePostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.DeletePost, form)); + } + + public removePost(form: RemovePostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.RemovePost, form)); + } + + public lockPost(form: LockPostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.LockPost, form)); + } + + public stickyPost(form: StickyPostForm) { + this.setAuth(form); + this.ws.send(this.wsSendWrapper(UserOperation.StickyPost, form)); } public savePost(form: SavePostForm) { From 4b6a762a5681759e58c172f7c844562534415e8e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 21 Jul 2020 10:15:17 -0400 Subject: [PATCH 07/10] Added an is_mod_or_admin function to Community --- docs/src/contributing_websocket_http_api.md | 144 +++++++++++++++----- server/lemmy_db/src/community.rs | 8 +- server/src/api/comment.rs | 8 +- server/src/api/community.rs | 50 ++----- server/src/api/post.rs | 24 ++-- 5 files changed, 144 insertions(+), 90 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 00e26b0d..568bafc3 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -17,6 +17,7 @@ - [Errors](#errors) - [API documentation](#api-documentation) * [Sort Types](#sort-types) + * [Undoing actions](#undoing-actions) * [Websocket vs HTTP](#websocket-vs-http) * [User / Authentication / Admin actions](#user--authentication--admin-actions) + [Login](#login) @@ -43,142 +44,198 @@ - [Request](#request-5) - [Response](#response-5) - [HTTP](#http-6) - + [Edit User Mention](#edit-user-mention) + + [Mark User Mention as read](#mark-user-mention-as-read) - [Request](#request-6) - [Response](#response-6) - [HTTP](#http-7) - + [Mark All As Read](#mark-all-as-read) + + [Get Private Messages](#get-private-messages) - [Request](#request-7) - [Response](#response-7) - [HTTP](#http-8) - + [Delete Account](#delete-account) + + [Create Private Message](#create-private-message) - [Request](#request-8) - [Response](#response-8) - [HTTP](#http-9) - + [Add admin](#add-admin) + + [Edit Private Message](#edit-private-message) - [Request](#request-9) - [Response](#response-9) - [HTTP](#http-10) - + [Ban user](#ban-user) + + [Delete Private Message](#delete-private-message) - [Request](#request-10) - [Response](#response-10) - [HTTP](#http-11) - * [Site](#site) - + [List Categories](#list-categories) + + [Mark Private Message as Read](#mark-private-message-as-read) - [Request](#request-11) - [Response](#response-11) - [HTTP](#http-12) - + [Search](#search) + + [Mark All As Read](#mark-all-as-read) - [Request](#request-12) - [Response](#response-12) - [HTTP](#http-13) - + [Get Modlog](#get-modlog) + + [Delete Account](#delete-account) - [Request](#request-13) - [Response](#response-13) - [HTTP](#http-14) - + [Create Site](#create-site) + + [Add admin](#add-admin) - [Request](#request-14) - [Response](#response-14) - [HTTP](#http-15) - + [Edit Site](#edit-site) + + [Ban user](#ban-user) - [Request](#request-15) - [Response](#response-15) - [HTTP](#http-16) - + [Get Site](#get-site) + * [Site](#site) + + [List Categories](#list-categories) - [Request](#request-16) - [Response](#response-16) - [HTTP](#http-17) - + [Transfer Site](#transfer-site) + + [Search](#search) - [Request](#request-17) - [Response](#response-17) - [HTTP](#http-18) - + [Get Site Config](#get-site-config) + + [Get Modlog](#get-modlog) - [Request](#request-18) - [Response](#response-18) - [HTTP](#http-19) - + [Save Site Config](#save-site-config) + + [Create Site](#create-site) - [Request](#request-19) - [Response](#response-19) - [HTTP](#http-20) - * [Community](#community) - + [Get Community](#get-community) + + [Edit Site](#edit-site) - [Request](#request-20) - [Response](#response-20) - [HTTP](#http-21) - + [Create Community](#create-community) + + [Get Site](#get-site) - [Request](#request-21) - [Response](#response-21) - [HTTP](#http-22) - + [List Communities](#list-communities) + + [Transfer Site](#transfer-site) - [Request](#request-22) - [Response](#response-22) - [HTTP](#http-23) - + [Ban from Community](#ban-from-community) + + [Get Site Config](#get-site-config) - [Request](#request-23) - [Response](#response-23) - [HTTP](#http-24) - + [Add Mod to Community](#add-mod-to-community) + + [Save Site Config](#save-site-config) - [Request](#request-24) - [Response](#response-24) - [HTTP](#http-25) - + [Edit Community](#edit-community) + * [Community](#community) + + [Get Community](#get-community) - [Request](#request-25) - [Response](#response-25) - [HTTP](#http-26) - + [Follow Community](#follow-community) + + [Create Community](#create-community) - [Request](#request-26) - [Response](#response-26) - [HTTP](#http-27) - + [Get Followed Communities](#get-followed-communities) + + [List Communities](#list-communities) - [Request](#request-27) - [Response](#response-27) - [HTTP](#http-28) - + [Transfer Community](#transfer-community) + + [Ban from Community](#ban-from-community) - [Request](#request-28) - [Response](#response-28) - [HTTP](#http-29) - * [Post](#post) - + [Create Post](#create-post) + + [Add Mod to Community](#add-mod-to-community) - [Request](#request-29) - [Response](#response-29) - [HTTP](#http-30) - + [Get Post](#get-post) + + [Edit Community](#edit-community) - [Request](#request-30) - [Response](#response-30) - [HTTP](#http-31) - + [Get Posts](#get-posts) + + [Delete Community](#delete-community) - [Request](#request-31) - [Response](#response-31) - [HTTP](#http-32) - + [Create Post Like](#create-post-like) + + [Remove Community](#remove-community) - [Request](#request-32) - [Response](#response-32) - [HTTP](#http-33) - + [Edit Post](#edit-post) + + [Follow Community](#follow-community) - [Request](#request-33) - [Response](#response-33) - [HTTP](#http-34) - + [Save Post](#save-post) + + [Get Followed Communities](#get-followed-communities) - [Request](#request-34) - [Response](#response-34) - [HTTP](#http-35) - * [Comment](#comment) - + [Create Comment](#create-comment) + + [Transfer Community](#transfer-community) - [Request](#request-35) - [Response](#response-35) - [HTTP](#http-36) - + [Edit Comment](#edit-comment) + * [Post](#post) + + [Create Post](#create-post) - [Request](#request-36) - [Response](#response-36) - [HTTP](#http-37) - + [Save Comment](#save-comment) + + [Get Post](#get-post) - [Request](#request-37) - [Response](#response-37) - [HTTP](#http-38) - + [Create Comment Like](#create-comment-like) + + [Get Posts](#get-posts) - [Request](#request-38) - [Response](#response-38) - [HTTP](#http-39) + + [Create Post Like](#create-post-like) + - [Request](#request-39) + - [Response](#response-39) + - [HTTP](#http-40) + + [Edit Post](#edit-post) + - [Request](#request-40) + - [Response](#response-40) + - [HTTP](#http-41) + + [Delete Post](#delete-post) + - [Request](#request-41) + - [Response](#response-41) + - [HTTP](#http-42) + + [Remove Post](#remove-post) + - [Request](#request-42) + - [Response](#response-42) + - [HTTP](#http-43) + + [Lock Post](#lock-post) + - [Request](#request-43) + - [Response](#response-43) + - [HTTP](#http-44) + + [Sticky Post](#sticky-post) + - [Request](#request-44) + - [Response](#response-44) + - [HTTP](#http-45) + + [Save Post](#save-post) + - [Request](#request-45) + - [Response](#response-45) + - [HTTP](#http-46) + * [Comment](#comment) + + [Create Comment](#create-comment) + - [Request](#request-46) + - [Response](#response-46) + - [HTTP](#http-47) + + [Edit Comment](#edit-comment) + - [Request](#request-47) + - [Response](#response-47) + - [HTTP](#http-48) + + [Delete Comment](#delete-comment) + - [Request](#request-48) + - [Response](#response-48) + - [HTTP](#http-49) + + [Remove Comment](#remove-comment) + - [Request](#request-49) + - [Response](#response-49) + - [HTTP](#http-50) + + [Mark Comment as Read](#mark-comment-as-read) + - [Request](#request-50) + - [Response](#response-50) + - [HTTP](#http-51) + + [Save Comment](#save-comment) + - [Request](#request-51) + - [Response](#response-51) + - [HTTP](#http-52) + + [Create Comment Like](#create-comment-like) + - [Request](#request-52) + - [Response](#response-52) + - [HTTP](#http-53) * [RSS / Atom feeds](#rss--atom-feeds) + [All](#all) + [Community](#community-1) @@ -281,6 +338,10 @@ These go wherever there is a `sort` field. The available sort types are: - `TopYear` - the most upvoted posts/communities of the current year. - `TopAll` - the most upvoted posts/communities on the current instance. +### Undoing actions + +Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`. + ### Websocket vs HTTP - Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`. @@ -465,6 +526,9 @@ Only the first user will be able to be the admin. `GET /user/mentions` #### Mark User Mention as read + +Only the recipient can do this. + ##### Request ```rust { @@ -595,6 +659,9 @@ Only the first user will be able to be the admin. `POST /private_message/delete` #### Mark Private Message as Read + +Only the recipient can do this. + ##### Request ```rust { @@ -1661,6 +1728,9 @@ Only a mod or admin can remove the comment. `POST /comment/remove` #### Mark Comment as Read + +Only the recipient can do this. + ##### Request ```rust { diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index 03c47e46..3a78d769 100644 --- a/server/lemmy_db/src/community.rs +++ b/server/lemmy_db/src/community.rs @@ -133,7 +133,7 @@ impl Community { .get_result::(conn) } - pub fn community_mods_and_admins( + fn community_mods_and_admins( conn: &PgConnection, community_id: i32, ) -> Result, Error> { @@ -147,6 +147,12 @@ impl Community { .append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?); Ok(mods_and_admins) } + + pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool { + Self::community_mods_and_admins(conn, community_id) + .unwrap_or_default() + .contains(&user_id) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 79b7b2c7..c461f792 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -474,11 +474,11 @@ impl Perform for Oper { } // Verify that only a mod or admin can remove - let mods_and_admins = blocking(pool, move |conn| { - Community::community_mods_and_admins(conn, community_id) + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) }) - .await??; - if !mods_and_admins.contains(&user_id) { + .await?; + if !is_mod_or_admin { return Err(APIError::err("not_an_admin").into()); } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 5e84bc6c..f4ba15fb 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -802,26 +802,15 @@ impl Perform for Oper { let user_id = claims.id; - let mut community_moderators: Vec = vec![]; - let community_id = data.community_id; - community_moderators.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(&conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - community_moderators.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - - if !community_moderators.contains(&user_id) { - return Err(APIError::err("couldnt_update_community").into()); + // Verify that only mods or admins can ban + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(APIError::err("not_an_admin").into()); } let community_user_ban_form = CommunityUserBanForm { @@ -901,26 +890,15 @@ impl Perform for Oper { user_id: data.user_id, }; - let mut community_moderators: Vec = vec![]; - let community_id = data.community_id; - community_moderators.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(&conn, community_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - community_moderators.append( - &mut blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??, - ); - - if !community_moderators.contains(&user_id) { - return Err(APIError::err("couldnt_update_community").into()); + // Verify that only mods or admins can add mod + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(APIError::err("not_an_admin").into()); } if data.added { diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 390d291c..15c38a3f 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -770,11 +770,11 @@ impl Perform for Oper { } // Verify that only the mods can remove - let mods_and_admins = blocking(pool, move |conn| { - Community::community_mods_and_admins(conn, community_id) + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) }) - .await??; - if !mods_and_admins.contains(&user_id) { + .await?; + if !is_mod_or_admin { return Err(APIError::err("not_an_admin").into()); } @@ -861,11 +861,11 @@ impl Perform for Oper { } // Verify that only the mods can lock - let mods_and_admins = blocking(pool, move |conn| { - Community::community_mods_and_admins(conn, community_id) + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) }) - .await??; - if !mods_and_admins.contains(&user_id) { + .await?; + if !is_mod_or_admin { return Err(APIError::err("not_an_admin").into()); } @@ -943,11 +943,11 @@ impl Perform for Oper { } // Verify that only the mods can sticky - let mods_and_admins = blocking(pool, move |conn| { - Community::community_mods_and_admins(conn, community_id) + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) }) - .await??; - if !mods_and_admins.contains(&user_id) { + .await?; + if !is_mod_or_admin { return Err(APIError::err("not_an_admin").into()); } From f81a7ad9ab64408a4a336e8dfcca6261a9d916b6 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 21 Jul 2020 10:56:41 -0400 Subject: [PATCH 08/10] Adding form_id to comment creates and edits. - This adds a form_id to CreateComment, EditComment, and CommentResponse - This is so any front end clients can add a randomly generated string, and know which comment they submitted, is the one they're getting back. - This gets rid of all the weird complicated logic in handleFinished(), and should stop the comment forms getting cleared once and for all. --- docs/src/contributing_websocket_http_api.md | 2 + server/src/api/comment.rs | 10 +++++ server/src/apub/shared_inbox.rs | 9 +++++ ui/src/components/comment-form.tsx | 45 ++++++--------------- ui/src/components/markdown-textarea.tsx | 5 ++- ui/src/interfaces.ts | 2 + 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 568bafc3..5445a23a 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1623,6 +1623,7 @@ Only admins and mods can sticky a post. content: String, parent_id: Option, post_id: i32, + form_id: Option, // An optional form id, so you know which message came back auth: String } } @@ -1652,6 +1653,7 @@ Only the creator can edit the comment. data: { content: String, edit_id: i32, + form_id: Option, auth: String, } } diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index c461f792..2a521455 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -44,6 +44,7 @@ pub struct CreateComment { content: String, parent_id: Option, pub post_id: i32, + form_id: Option, auth: String, } @@ -51,6 +52,7 @@ pub struct CreateComment { pub struct EditComment { content: String, edit_id: i32, + form_id: Option, auth: String, } @@ -87,6 +89,7 @@ pub struct SaveComment { pub struct CommentResponse { pub comment: CommentView, pub recipient_ids: Vec, + pub form_id: Option, } #[derive(Serialize, Deserialize)] @@ -227,6 +230,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: comment_view, recipient_ids, + form_id: data.form_id.to_owned(), }; if let Some(ws) = websocket_info { @@ -321,6 +325,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: comment_view, recipient_ids, + form_id: data.form_id.to_owned(), }; if let Some(ws) = websocket_info { @@ -419,6 +424,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; if let Some(ws) = websocket_info { @@ -530,6 +536,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; if let Some(ws) = websocket_info { @@ -621,6 +628,7 @@ impl Perform for Oper { let res = CommentResponse { comment: comment_view, recipient_ids: Vec::new(), + form_id: None, }; Ok(res) @@ -671,6 +679,7 @@ impl Perform for Oper { Ok(CommentResponse { comment: comment_view, recipient_ids: Vec::new(), + form_id: None, }) } } @@ -782,6 +791,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: liked_comment, recipient_ids, + form_id: None, }; if let Some(ws) = websocket_info { diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 41d1a80e..aca2e09f 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -404,6 +404,7 @@ async fn receive_create_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -567,6 +568,7 @@ async fn receive_update_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -616,6 +618,7 @@ async fn receive_like_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -665,6 +668,7 @@ async fn receive_dislike_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -960,6 +964,7 @@ async fn receive_delete_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -1017,6 +1022,7 @@ async fn receive_remove_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -1108,6 +1114,7 @@ async fn receive_undo_delete_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -1165,6 +1172,7 @@ async fn receive_undo_remove_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { @@ -1464,6 +1472,7 @@ async fn receive_undo_like_comment( let res = CommentResponse { comment: comment_view, recipient_ids, + form_id: None, }; chat_server.do_send(SendComment { diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index 6e45229b..01222b27 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -115,34 +115,9 @@ export class CommentForm extends Component { ); } - handleFinished(op: UserOperation, data: CommentResponse) { - let isReply = - this.props.node !== undefined && data.comment.parent_id !== null; - let xor = - +!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined); - - if ( - (data.comment.creator_id == UserService.Instance.user.id && - ((op == UserOperation.CreateComment && - // If its a reply, make sure parent child match - isReply && - data.comment.parent_id == this.props.node.comment.id) || - // Otherwise, check the XOR of the two - (!isReply && xor))) || - // If its a comment edit, only check that its from your user, and that its a - // text edit only - - (data.comment.creator_id == UserService.Instance.user.id && - op == UserOperation.EditComment && - data.comment.content) - ) { - this.state.finished = true; - this.setState(this.state); - } - } - - handleCommentSubmit(val: string) { - this.state.commentForm.content = val; + handleCommentSubmit(msg: { val: string; formId: string }) { + this.state.commentForm.content = msg.val; + this.state.commentForm.form_id = msg.formId; if (this.props.edit) { WebSocketService.Instance.editComment(this.state.commentForm); } else { @@ -160,12 +135,16 @@ export class CommentForm extends Component { // Only do the showing and hiding if logged in if (UserService.Instance.user) { - if (res.op == UserOperation.CreateComment) { + if ( + res.op == UserOperation.CreateComment || + res.op == UserOperation.EditComment + ) { let data = res.data as CommentResponse; - this.handleFinished(res.op, data); - } else if (res.op == UserOperation.EditComment) { - let data = res.data as CommentResponse; - this.handleFinished(res.op, data); + + // This only finishes this form, if the randomly generated form_id matches the one received + if (this.state.commentForm.form_id == data.form_id) { + this.setState({ finished: true }); + } } } } diff --git a/ui/src/components/markdown-textarea.tsx b/ui/src/components/markdown-textarea.tsx index 2f6d0a7e..9e4dbf84 100644 --- a/ui/src/components/markdown-textarea.tsx +++ b/ui/src/components/markdown-textarea.tsx @@ -21,7 +21,7 @@ interface MarkdownTextAreaProps { replyType?: boolean; focus?: boolean; disabled?: boolean; - onSubmit?(val: string): any; + onSubmit?(msg: { val: string; formId: string }): any; onContentChange?(val: string): any; onReplyCancel?(): any; } @@ -373,7 +373,8 @@ export class MarkdownTextArea extends Component< event.preventDefault(); i.state.loading = true; i.setState(i.state); - i.props.onSubmit(i.state.content); + let msg = { val: i.state.content, formId: i.formId }; + i.props.onSubmit(msg); } handleReplyCancel(i: MarkdownTextArea) { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 8dced1a6..05de0c05 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -708,6 +708,7 @@ export interface CommentForm { parent_id?: number; edit_id?: number; creator_id?: number; + form_id?: string; auth: string; } @@ -739,6 +740,7 @@ export interface SaveCommentForm { export interface CommentResponse { comment: Comment; recipient_ids: Array; + form_id?: string; } export interface CommentLikeForm { From 5e5063cbddaedd53c6573c17a9f320096ce35d60 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 21 Jul 2020 13:52:57 -0400 Subject: [PATCH 09/10] Adding some helper functions. --- server/lemmy_db/src/post.rs | 4 +++ server/src/api/community.rs | 63 ++++++++++++++----------------------- server/src/api/post.rs | 4 +-- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/server/lemmy_db/src/post.rs b/server/lemmy_db/src/post.rs index 35b0fead..d4667789 100644 --- a/server/lemmy_db/src/post.rs +++ b/server/lemmy_db/src/post.rs @@ -148,6 +148,10 @@ impl Post { .set(stickied.eq(new_stickied)) .get_result::(conn) } + + pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool { + user_id == post_creator_id + } } impl Crud for Post { diff --git a/server/src/api/community.rs b/server/src/api/community.rs index f4ba15fb..1ae7036f 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -433,19 +433,7 @@ impl Perform for Oper { community: community_view, }; - if let Some(ws) = websocket_info { - // Strip out the user id and subscribed when sending to others - let mut res_sent = res.clone(); - res_sent.community.user_id = None; - res_sent.community.subscribed = None; - - ws.chatserver.do_send(SendCommunityRoomMessage { - op: UserOperation::EditCommunity, - response: res_sent, - community_id: data.edit_id, - my_id: ws.id, - }); - } + send_community_websocket(&res, websocket_info, UserOperation::EditCommunity); Ok(res) } @@ -515,19 +503,7 @@ impl Perform for Oper { community: community_view, }; - if let Some(ws) = websocket_info { - // Strip out the user id and subscribed when sending to others - let mut res_sent = res.clone(); - res_sent.community.user_id = None; - res_sent.community.subscribed = None; - - ws.chatserver.do_send(SendCommunityRoomMessage { - op: UserOperation::DeleteCommunity, - response: res_sent, - community_id: data.edit_id, - my_id: ws.id, - }); - } + send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity); Ok(res) } @@ -613,19 +589,7 @@ impl Perform for Oper { community: community_view, }; - if let Some(ws) = websocket_info { - // Strip out the user id and subscribed when sending to others - let mut res_sent = res.clone(); - res_sent.community.user_id = None; - res_sent.community.subscribed = None; - - ws.chatserver.do_send(SendCommunityRoomMessage { - op: UserOperation::RemoveCommunity, - response: res_sent, - community_id: data.edit_id, - my_id: ws.id, - }); - } + send_community_websocket(&res, websocket_info, UserOperation::RemoveCommunity); Ok(res) } @@ -831,6 +795,7 @@ impl Perform for Oper { } // Mod tables + // TODO eventually do correct expires let expires = match data.expires { Some(time) => Some(naive_from_unix(time)), None => None, @@ -1055,3 +1020,23 @@ impl Perform for Oper { }) } } + +pub fn send_community_websocket( + res: &CommunityResponse, + websocket_info: Option, + op: UserOperation, +) { + if let Some(ws) = websocket_info { + // Strip out the user id and subscribed when sending to others + let mut res_sent = res.clone(); + res_sent.community.user_id = None; + res_sent.community.subscribed = None; + + ws.chatserver.do_send(SendCommunityRoomMessage { + op, + response: res_sent, + community_id: res.community.id, + my_id: ws.id, + }); + } +} diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 15c38a3f..70f46b2a 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -589,7 +589,7 @@ impl Perform for Oper { } // Verify that only the creator can edit - if user_id != orig_post.creator_id { + if !Post::is_post_creator(user_id, orig_post.creator_id) { return Err(APIError::err("no_post_edit_allowed").into()); } @@ -692,7 +692,7 @@ impl Perform for Oper { } // Verify that only the creator can delete - if user_id != orig_post.creator_id { + if !Post::is_post_creator(user_id, orig_post.creator_id) { return Err(APIError::err("no_post_edit_allowed").into()); } From 59da2976abfc26268ee129424e1f8049cc5538ae Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 22 Jul 2020 14:20:08 -0400 Subject: [PATCH 10/10] Some more API cleanup. - Extracted methods for is_mod_or_admin, and is_admin. - Removed admins from GetPostResponse and GetCommunityResponse. - Some cleanup. --- docs/src/contributing_websocket_http_api.md | 2 -- server/src/api/comment.rs | 11 ++---- server/src/api/community.rs | 36 +++---------------- server/src/api/mod.rs | 34 ++++++++++++++++-- server/src/api/post.rs | 39 +++------------------ server/src/api/site.rs | 19 +++------- server/src/api/user.rs | 12 ++----- ui/src/components/community.tsx | 2 +- ui/src/components/post.tsx | 2 -- ui/src/interfaces.ts | 2 -- 10 files changed, 50 insertions(+), 109 deletions(-) diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 8577a5e5..62cb1fc4 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/docs/src/contributing_websocket_http_api.md @@ -1054,7 +1054,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url` data: { community: CommunityView, moderators: Vec, - admins: Vec, } } ``` @@ -1379,7 +1378,6 @@ Only admins can remove a community. comments: Vec, community: CommunityView, moderators: Vec, - admins: Vec, } } ``` diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 2a521455..df772f53 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -1,5 +1,5 @@ use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform}, apub::{ApubLikeableType, ApubObjectType}, blocking, websocket::{ @@ -13,7 +13,6 @@ use crate::{ use lemmy_db::{ comment::*, comment_view::*, - community::Community, community_view::*, moderator::*, post::*, @@ -480,13 +479,7 @@ impl Perform for Oper { } // Verify that only a mod or admin can remove - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; // Do the remove let removed = data.removed; diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 1ae7036f..c5ae152a 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -1,6 +1,6 @@ use super::*; use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform}, apub::ActorType, blocking, websocket::{ @@ -34,7 +34,6 @@ pub struct GetCommunity { pub struct GetCommunityResponse { pub community: CommunityView, pub moderators: Vec, - pub admins: Vec, // TODO this should be from GetSite, shouldn't need this pub online: usize, } @@ -196,13 +195,6 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; - let site = blocking(pool, move |conn| Site::read(conn, 1)).await??; - let site_creator_id = site.creator_id; - let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??; - let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); - let creator_user = admins.remove(creator_index); - admins.insert(0, creator_user); - let online = if let Some(ws) = websocket_info { if let Some(id) = ws.id { ws.chatserver.do_send(JoinCommunityRoom { @@ -224,7 +216,6 @@ impl Perform for Oper { let res = GetCommunityResponse { community: community_view, moderators, - admins, online, }; @@ -534,13 +525,7 @@ impl Perform for Oper { } // Verify its an admin (only an admin can remove a community) - let admins: Vec = blocking(pool, move |conn| { - UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect()) - }) - .await??; - if !admins.contains(&user_id) { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; // Do the remove let edit_id = data.edit_id; @@ -769,13 +754,7 @@ impl Perform for Oper { let community_id = data.community_id; // Verify that only mods or admins can ban - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; let community_user_ban_form = CommunityUserBanForm { community_id: data.community_id, @@ -858,13 +837,7 @@ impl Perform for Oper { let community_id = data.community_id; // Verify that only mods or admins can add mod - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; if data.added { let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); @@ -1015,7 +988,6 @@ impl Perform for Oper { Ok(GetCommunityResponse { community: community_view, moderators, - admins, online: 0, }) } diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index bb65815a..90117260 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -1,6 +1,14 @@ -use crate::{websocket::WebsocketInfo, DbPool, LemmyError}; +use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError}; use actix_web::client::Client; -use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*}; +use lemmy_db::{ + community::*, + community_view::*, + moderator::*, + site::*, + user::*, + user_view::*, + Crud, +}; pub mod claims; pub mod comment; @@ -44,3 +52,25 @@ pub trait Perform { websocket_info: Option, ) -> Result; } + +pub async fn is_mod_or_admin( + pool: &DbPool, + user_id: i32, + community_id: i32, +) -> Result<(), LemmyError> { + let is_mod_or_admin = blocking(pool, move |conn| { + Community::is_mod_or_admin(conn, user_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(APIError::err("not_an_admin").into()); + } + Ok(()) +} +pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> { + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; + if !user.admin { + return Err(APIError::err("not_an_admin").into()); + } + Ok(()) +} diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 70f46b2a..79881c4b 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -1,5 +1,5 @@ use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform}, apub::{ApubLikeableType, ApubObjectType}, blocking, fetch_iframely_and_pictrs_data, @@ -13,16 +13,13 @@ use crate::{ }; use lemmy_db::{ comment_view::*, - community::*, community_view::*, moderator::*, naive_now, post::*, post_view::*, - site::*, site_view::*, user::*, - user_view::*, Crud, Likeable, ListingType, @@ -67,7 +64,6 @@ pub struct GetPostResponse { comments: Vec, community: CommunityView, moderators: Vec, - admins: Vec, pub online: usize, } @@ -334,14 +330,6 @@ impl Perform for Oper { }) .await??; - let site_creator_id = - blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??; - - let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??; - let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); - let creator_user = admins.remove(creator_index); - admins.insert(0, creator_user); - let online = if let Some(ws) = websocket_info { if let Some(id) = ws.id { ws.chatserver.do_send(JoinPostRoom { @@ -366,7 +354,6 @@ impl Perform for Oper { comments, community, moderators, - admins, online, }) } @@ -770,13 +757,7 @@ impl Perform for Oper { } // Verify that only the mods can remove - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; // Update the post let edit_id = data.edit_id; @@ -861,13 +842,7 @@ impl Perform for Oper { } // Verify that only the mods can lock - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; // Update the post let edit_id = data.edit_id; @@ -943,13 +918,7 @@ impl Perform for Oper { } // Verify that only the mods can sticky - let is_mod_or_admin = blocking(pool, move |conn| { - Community::is_mod_or_admin(conn, user_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(APIError::err("not_an_admin").into()); - } + is_mod_or_admin(pool, user_id, community_id).await?; // Update the post let edit_id = data.edit_id; diff --git a/server/src/api/site.rs b/server/src/api/site.rs index a945d9ec..85511e6c 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -1,6 +1,6 @@ use super::user::Register; use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, APIError, Oper, Perform}, apub::fetcher::search_by_apub_id, blocking, version, @@ -257,10 +257,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; - if !user.admin { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let site_form = SiteForm { name: data.name.to_owned(), @@ -311,10 +308,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; - if !user.admin { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; @@ -693,12 +687,7 @@ impl Perform for Oper { let user_id = claims.id; // Only let admins read this - let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; - let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); - - if !admin_ids.contains(&user_id) { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let config_hjson = Settings::read_config_file()?; diff --git a/server/src/api/user.rs b/server/src/api/user.rs index ec61c658..32a16b00 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -1,5 +1,5 @@ use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, APIError, Oper, Perform}, apub::ApubObjectType, blocking, websocket::{ @@ -679,10 +679,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); - if !blocking(pool, is_admin).await?? { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let added = data.added; let added_user_id = data.user_id; @@ -741,10 +738,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); - if !blocking(pool, is_admin).await?? { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let ban = data.ban; let banned_user_id = data.user_id; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index f70fa4f7..f4610a08 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -355,7 +355,6 @@ export class Community extends Component { let data = res.data as GetCommunityResponse; this.state.community = data.community; this.state.moderators = data.moderators; - this.state.admins = data.admins; this.state.online = data.online; document.title = `/c/${this.state.community.name} - ${this.state.site.name}`; this.setState(this.state); @@ -442,6 +441,7 @@ export class Community extends Component { } else if (res.op == UserOperation.GetSite) { let data = res.data as GetSiteResponse; this.state.site = data.site; + this.state.admins = data.admins; this.setState(this.state); } } diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 9721eb05..87b44bc3 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -405,7 +405,6 @@ export class Post extends Component { this.state.comments = data.comments; this.state.community = data.community; this.state.moderators = data.moderators; - this.state.siteRes.admins = data.admins; this.state.online = data.online; this.state.loading = false; document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`; @@ -531,7 +530,6 @@ export class Post extends Component { let data = res.data as GetCommunityResponse; this.state.community = data.community; this.state.moderators = data.moderators; - this.state.siteRes.admins = data.admins; this.setState(this.state); } } diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index e04eec63..559fbca5 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -613,7 +613,6 @@ export interface GetCommunityForm { export interface GetCommunityResponse { community: Community; moderators: Array; - admins: Array; online: number; } @@ -688,7 +687,6 @@ export interface GetPostResponse { comments: Array; community: Community; moderators: Array; - admins: Array; online: number; }