From 253bc3e0afb6adf64b79f334a8bc1f972aa45eba Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 22 Jan 2020 16:35:29 -0500 Subject: [PATCH] Adding private messaging, and matrix user ids. - Fixes #244 --- README.md | 18 +- .../down.sql | 34 ++ .../up.sql | 90 ++++++ server/src/api/comment.rs | 4 +- server/src/api/mod.rs | 5 + server/src/api/user.rs | 214 +++++++++++++ server/src/apub/mod.rs | 1 + server/src/db/comment.rs | 1 + server/src/db/comment_view.rs | 1 + server/src/db/community.rs | 1 + server/src/db/mod.rs | 2 + server/src/db/moderator.rs | 2 + server/src/db/password_reset_request.rs | 1 + server/src/db/post.rs | 1 + server/src/db/post_view.rs | 1 + server/src/db/private_message.rs | 144 +++++++++ server/src/db/private_message_view.rs | 140 +++++++++ server/src/db/user.rs | 4 + server/src/db/user_mention.rs | 2 + server/src/db/user_view.rs | 3 + server/src/routes/index.rs | 1 + server/src/schema.rs | 15 + server/src/websocket/server.rs | 16 + ui/src/components/create-private-message.tsx | 52 ++++ ui/src/components/inbox.tsx | 117 +++++-- ui/src/components/navbar.tsx | 41 ++- ui/src/components/private-message-form.tsx | 291 ++++++++++++++++++ ui/src/components/private-message.tsx | 249 +++++++++++++++ ui/src/components/user.tsx | 51 ++- ui/src/index.tsx | 5 + ui/src/interfaces.ts | 55 ++++ ui/src/services/WebSocketService.ts | 26 +- ui/src/translations/en.ts | 13 + ui/src/utils.ts | 5 + 34 files changed, 1560 insertions(+), 46 deletions(-) create mode 100644 server/migrations/2020-01-21-001001_create_private_message/down.sql create mode 100644 server/migrations/2020-01-21-001001_create_private_message/up.sql create mode 100644 server/src/db/private_message.rs create mode 100644 server/src/db/private_message_view.rs create mode 100644 ui/src/components/create-private-message.tsx create mode 100644 ui/src/components/private-message-form.tsx create mode 100644 ui/src/components/private-message.tsx diff --git a/README.md b/README.md index 51bd0476..752e7d98 100644 --- a/README.md +++ b/README.md @@ -157,15 +157,15 @@ If you'd like to add translations, take a look a look at the [English translatio lang | done | missing --- | --- | --- -de | 93% | avatar,upload_avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -eo | 80% | number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,are_you_sure,yes,no,email_already_exists -es | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -fr | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -it | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -nl | 99% | donate_to_lemmy,donate,email_already_exists -ru | 77% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists -sv | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists -zh | 75% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists +de | 88% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +eo | 76% | number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +es | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +fr | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +it | 85% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +nl | 93% | create_private_message,send_secure_message,send_message,message,message_sent,messages,matrix_user_id,private_message_disclaimer,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +ru | 72% | cross_posts,cross_post,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +sv | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message +zh | 71% | cross_posts,cross_post,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message diff --git a/server/migrations/2020-01-21-001001_create_private_message/down.sql b/server/migrations/2020-01-21-001001_create_private_message/down.sql new file mode 100644 index 00000000..0d951e3e --- /dev/null +++ b/server/migrations/2020-01-21-001001_create_private_message/down.sql @@ -0,0 +1,34 @@ +-- Drop the triggers +drop trigger refresh_private_message on private_message; +drop function refresh_private_message(); + +-- Drop the view and table +drop view private_message_view cascade; +drop table private_message; + +-- Rebuild the old views +drop view user_view cascade; +create view user_view as +select +u.id, +u.name, +u.avatar, +u.email, +u.fedi_name, +u.admin, +u.banned, +u.show_avatars, +u.send_notifications_to_email, +u.published, +(select count(*) from post p where p.creator_id = u.id) as number_of_posts, +(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, +(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, +(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score +from user_ u; + +create materialized view user_mview as select * from user_view; + +create unique index idx_user_mview_id on user_mview (id); + +-- Drop the columns +alter table user_ drop column matrix_user_id; diff --git a/server/migrations/2020-01-21-001001_create_private_message/up.sql b/server/migrations/2020-01-21-001001_create_private_message/up.sql new file mode 100644 index 00000000..48e16dd8 --- /dev/null +++ b/server/migrations/2020-01-21-001001_create_private_message/up.sql @@ -0,0 +1,90 @@ +-- Creating private message +create table private_message ( + id serial primary key, + creator_id int references user_ on update cascade on delete cascade not null, + recipient_id int references user_ on update cascade on delete cascade not null, + content text not null, + deleted boolean default false not null, + read boolean default false not null, + published timestamp not null default now(), + updated timestamp +); + +-- Create the view and materialized view which has the avatar and creator name +create view private_message_view as +select +pm.*, +u.name as creator_name, +u.avatar as creator_avatar, +u2.name as recipient_name, +u2.avatar as recipient_avatar +from private_message pm +inner join user_ u on u.id = pm.creator_id +inner join user_ u2 on u2.id = pm.recipient_id; + +create materialized view private_message_mview as select * from private_message_view; + +create unique index idx_private_message_mview_id on private_message_mview (id); + +-- Create the triggers +create or replace function refresh_private_message() +returns trigger language plpgsql +as $$ +begin + refresh materialized view concurrently private_message_mview; + return null; +end $$; + +create trigger refresh_private_message +after insert or update or delete or truncate +on private_message +for each statement +execute procedure refresh_private_message(); + +-- Update user to include matrix id +alter table user_ add column matrix_user_id text unique; + +drop view user_view cascade; +create view user_view as +select +u.id, +u.name, +u.avatar, +u.email, +u.matrix_user_id, +u.fedi_name, +u.admin, +u.banned, +u.show_avatars, +u.send_notifications_to_email, +u.published, +(select count(*) from post p where p.creator_id = u.id) as number_of_posts, +(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score, +(select count(*) from comment c where c.creator_id = u.id) as number_of_comments, +(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score +from user_ u; + +create materialized view user_mview as select * from user_view; + +create unique index idx_user_mview_id on user_mview (id); + +-- This is what a group pm table would look like +-- Not going to do it now because of the complications +-- +-- create table private_message ( +-- id serial primary key, +-- creator_id int references user_ on update cascade on delete cascade not null, +-- content text not null, +-- deleted boolean default false not null, +-- published timestamp not null default now(), +-- updated timestamp +-- ); +-- +-- create table private_message_recipient ( +-- id serial primary key, +-- private_message_id int references private_message on update cascade on delete cascade not null, +-- recipient_id int references user_ on update cascade on delete cascade not null, +-- read boolean default false not null, +-- published timestamp not null default now(), +-- unique(private_message_id, recipient_id) +-- ) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 61cc9506..382afb5b 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -7,7 +7,7 @@ use diesel::PgConnection; pub struct CreateComment { content: String, parent_id: Option, - edit_id: Option, + edit_id: Option, // TODO this isn't used pub post_id: i32, auth: String, } @@ -15,7 +15,7 @@ pub struct CreateComment { #[derive(Serialize, Deserialize)] pub struct EditComment { content: String, - parent_id: Option, + 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, diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index e3580447..3b2466ac 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -8,6 +8,8 @@ use crate::db::moderator_views::*; use crate::db::password_reset_request::*; use crate::db::post::*; use crate::db::post_view::*; +use crate::db::private_message::*; +use crate::db::private_message_view::*; use crate::db::site::*; use crate::db::site_view::*; use crate::db::user::*; @@ -67,6 +69,9 @@ pub enum UserOperation { DeleteAccount, PasswordReset, PasswordChange, + CreatePrivateMessage, + EditPrivateMessage, + GetPrivateMessages, } #[derive(Fail, Debug)] diff --git a/server/src/api/user.rs b/server/src/api/user.rs index ac700aca..046da6fb 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -30,6 +30,7 @@ pub struct SaveUserSettings { lang: String, avatar: Option, email: Option, + matrix_user_id: Option, new_password: Option, new_password_verify: Option, old_password: Option, @@ -167,6 +168,42 @@ pub struct PasswordChange { password_verify: String, } +#[derive(Serialize, Deserialize)] +pub struct CreatePrivateMessage { + content: String, + recipient_id: i32, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct EditPrivateMessage { + edit_id: i32, + content: Option, + deleted: Option, + read: Option, + auth: String, +} + +#[derive(Serialize, Deserialize)] +pub struct GetPrivateMessages { + unread_only: bool, + page: Option, + limit: Option, + auth: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PrivateMessagesResponse { + op: String, + messages: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PrivateMessageResponse { + op: String, + message: PrivateMessageView, +} + impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &Login = &self.data; @@ -221,6 +258,7 @@ impl Perform for Oper { name: data.username.to_owned(), fedi_name: Settings::get().hostname.to_owned(), email: data.email.to_owned(), + matrix_user_id: None, avatar: None, password_encrypted: data.password.to_owned(), preferred_username: None, @@ -357,6 +395,7 @@ impl Perform for Oper { name: read_user.name, fedi_name: read_user.fedi_name, email, + matrix_user_id: data.matrix_user_id.to_owned(), avatar: data.avatar.to_owned(), password_encrypted, preferred_username: read_user.preferred_username, @@ -504,10 +543,12 @@ impl Perform for Oper { let read_user = User_::read(&conn, data.user_id)?; + // TODO make addadmin easier let user_form = UserForm { name: read_user.name, fedi_name: read_user.fedi_name, email: read_user.email, + matrix_user_id: read_user.matrix_user_id, avatar: read_user.avatar, password_encrypted: read_user.password_encrypted, preferred_username: read_user.preferred_username, @@ -568,10 +609,12 @@ impl Perform for Oper { let read_user = User_::read(&conn, data.user_id)?; + // TODO make bans and addadmins easier let user_form = UserForm { name: read_user.name, fedi_name: read_user.fedi_name, email: read_user.email, + matrix_user_id: read_user.matrix_user_id, avatar: read_user.avatar, password_encrypted: read_user.password_encrypted, preferred_username: read_user.preferred_username, @@ -762,6 +805,30 @@ impl Perform for Oper { }; } + // messages + let messages = PrivateMessageQueryBuilder::create(&conn, user_id) + .page(1) + .limit(999) + .unread_only(true) + .list()?; + + for message in &messages { + let private_message_form = PrivateMessageForm { + content: None, + creator_id: message.to_owned().creator_id, + recipient_id: message.to_owned().recipient_id, + deleted: None, + read: Some(true), + updated: None, + }; + + let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form) + { + Ok(message) => message, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()), + }; + } + Ok(GetRepliesResponse { op: self.op.to_string(), replies: vec![], @@ -905,3 +972,150 @@ impl Perform for Oper { }) } } + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &CreatePrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + }; + + let user_id = claims.id; + + let hostname = &format!("https://{}", Settings::get().hostname); + + // Check for a site ban + if UserView::read(&conn, user_id)?.banned { + return Err(APIError::err(&self.op, "site_ban").into()); + } + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + let private_message_form = PrivateMessageForm { + content: Some(content_slurs_removed.to_owned()), + creator_id: user_id, + recipient_id: data.recipient_id, + deleted: None, + read: None, + updated: None, + }; + + let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) { + Ok(private_message) => private_message, + Err(_e) => { + return Err(APIError::err(&self.op, "couldnt_create_private_message").into()); + } + }; + + // Send notifications to the recipient + let recipient_user = User_::read(&conn, data.recipient_id)?; + if recipient_user.send_notifications_to_email { + if let Some(email) = recipient_user.email { + let subject = &format!( + "{} - Private Message from {}", + Settings::get().hostname, + claims.username + ); + let html = &format!( + "

Private Message


{} - {}

inbox", + claims.username, &content_slurs_removed, hostname + ); + match send_email(subject, &email, &recipient_user.name, html) { + Ok(_o) => _o, + Err(e) => eprintln!("{}", e), + }; + } + } + + let private_message_view = PrivateMessageView::read(&conn, inserted_private_message.id)?; + + Ok(PrivateMessageResponse { + op: self.op.to_string(), + message: private_message_view, + }) + } +} + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &EditPrivateMessage = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + }; + + let user_id = claims.id; + + let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?; + + // Check for a site ban + if UserView::read(&conn, user_id)?.banned { + return Err(APIError::err(&self.op, "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)) + { + return Err(APIError::err(&self.op, "no_private_message_edit_allowed").into()); + } + + let content_slurs_removed = match &data.content { + Some(content) => Some(remove_slurs(content)), + None => None, + }; + + let private_message_form = 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: data.read.to_owned(), + updated: if data.read.is_some() { + orig_private_message.updated + } else { + Some(naive_now()) + }, + }; + + let _updated_private_message = + match PrivateMessage::update(&conn, data.edit_id, &private_message_form) { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_private_message").into()), + }; + + let private_message_view = PrivateMessageView::read(&conn, data.edit_id)?; + + Ok(PrivateMessageResponse { + op: self.op.to_string(), + message: private_message_view, + }) + } +} + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &GetPrivateMessages = &self.data; + + let claims = match Claims::decode(&data.auth) { + Ok(claims) => claims.claims, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), + }; + + let user_id = claims.id; + + let messages = PrivateMessageQueryBuilder::create(&conn, user_id) + .page(data.page) + .limit(data.limit) + .unread_only(data.unread_only) + .list()?; + + Ok(PrivateMessagesResponse { + op: self.op.to_string(), + messages, + }) + } +} diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 2d2e5ad3..c5a0b2f0 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -22,6 +22,7 @@ mod tests { preferred_username: None, password_encrypted: "here".into(), email: None, + matrix_user_id: None, avatar: None, published: naive_now(), admin: false, diff --git a/server/src/db/comment.rs b/server/src/db/comment.rs index a9c7d81d..efba07a5 100644 --- a/server/src/db/comment.rs +++ b/server/src/db/comment.rs @@ -174,6 +174,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs index 3b06e8e3..d4a65c9a 100644 --- a/server/src/db/comment_view.rs +++ b/server/src/db/comment_view.rs @@ -398,6 +398,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/community.rs b/server/src/db/community.rs index b482ca4a..63500963 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -220,6 +220,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index fef3ffce..dacdb6f6 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -15,6 +15,8 @@ pub mod moderator_views; pub mod password_reset_request; pub mod post; pub mod post_view; +pub mod private_message; +pub mod private_message_view; pub mod site; pub mod site_view; pub mod user; diff --git a/server/src/db/moderator.rs b/server/src/db/moderator.rs index 3c6233cb..4fd532af 100644 --- a/server/src/db/moderator.rs +++ b/server/src/db/moderator.rs @@ -442,6 +442,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -463,6 +464,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/password_reset_request.rs b/server/src/db/password_reset_request.rs index fa060a59..6951fd39 100644 --- a/server/src/db/password_reset_request.rs +++ b/server/src/db/password_reset_request.rs @@ -92,6 +92,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/post.rs b/server/src/db/post.rs index d3fba4da..9e7a4341 100644 --- a/server/src/db/post.rs +++ b/server/src/db/post.rs @@ -187,6 +187,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index f6cc274f..c80d1696 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -339,6 +339,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, updated: None, admin: false, diff --git a/server/src/db/private_message.rs b/server/src/db/private_message.rs new file mode 100644 index 00000000..cc073b59 --- /dev/null +++ b/server/src/db/private_message.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::schema::private_message; + +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] +#[table_name = "private_message"] +pub struct PrivateMessage { + pub id: i32, + pub creator_id: i32, + pub recipient_id: i32, + pub content: String, + pub deleted: bool, + pub read: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, +} + +#[derive(Insertable, AsChangeset, Clone)] +#[table_name = "private_message"] +pub struct PrivateMessageForm { + pub creator_id: i32, + pub recipient_id: i32, + pub content: Option, + pub deleted: Option, + pub read: Option, + pub updated: Option, +} + +impl Crud for PrivateMessage { + fn read(conn: &PgConnection, private_message_id: i32) -> Result { + use crate::schema::private_message::dsl::*; + private_message.find(private_message_id).first::(conn) + } + + fn delete(conn: &PgConnection, private_message_id: i32) -> Result { + use crate::schema::private_message::dsl::*; + diesel::delete(private_message.find(private_message_id)).execute(conn) + } + + fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result { + use crate::schema::private_message::dsl::*; + insert_into(private_message) + .values(private_message_form) + .get_result::(conn) + } + + fn update( + conn: &PgConnection, + private_message_id: i32, + private_message_form: &PrivateMessageForm, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(private_message_form) + .get_result::(conn) + } +} + +#[cfg(test)] +mod tests { + use super::super::user::*; + use super::*; + #[test] + fn test_crud() { + let conn = establish_unpooled_connection(); + + let creator_form = UserForm { + name: "creator_pm".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + admin: false, + banned: false, + updated: None, + show_nsfw: false, + theme: "darkly".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + }; + + let inserted_creator = User_::create(&conn, &creator_form).unwrap(); + + let recipient_form = UserForm { + name: "recipient_pm".into(), + fedi_name: "rrf".into(), + preferred_username: None, + password_encrypted: "nope".into(), + email: None, + matrix_user_id: None, + avatar: None, + admin: false, + banned: false, + updated: None, + show_nsfw: false, + theme: "darkly".into(), + default_sort_type: SortType::Hot as i16, + default_listing_type: ListingType::Subscribed as i16, + lang: "browser".into(), + show_avatars: true, + send_notifications_to_email: false, + }; + + let inserted_recipient = User_::create(&conn, &recipient_form).unwrap(); + + let private_message_form = PrivateMessageForm { + content: Some("A test private message".into()), + creator_id: inserted_creator.id, + recipient_id: inserted_recipient.id, + deleted: None, + read: None, + updated: None, + }; + + let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap(); + + let expected_private_message = PrivateMessage { + id: inserted_private_message.id, + content: "A test private message".into(), + creator_id: inserted_creator.id, + recipient_id: inserted_recipient.id, + deleted: false, + read: false, + updated: None, + published: inserted_private_message.published, + }; + + 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 num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap(); + User_::delete(&conn, inserted_creator.id).unwrap(); + User_::delete(&conn, inserted_recipient.id).unwrap(); + + 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_eq!(1, num_deleted); + } +} diff --git a/server/src/db/private_message_view.rs b/server/src/db/private_message_view.rs new file mode 100644 index 00000000..59a573f4 --- /dev/null +++ b/server/src/db/private_message_view.rs @@ -0,0 +1,140 @@ +use super::*; +use diesel::pg::Pg; + +// The faked schema since diesel doesn't do views +table! { + private_message_view (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + creator_name -> Varchar, + creator_avatar -> Nullable, + recipient_name -> Varchar, + recipient_avatar -> Nullable, + } +} + +table! { + private_message_mview (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + creator_name -> Varchar, + creator_avatar -> Nullable, + recipient_name -> Varchar, + recipient_avatar -> Nullable, + } +} + +#[derive( + Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, +)] +#[table_name = "private_message_view"] +pub struct PrivateMessageView { + pub id: i32, + pub creator_id: i32, + pub recipient_id: i32, + pub content: String, + pub deleted: bool, + pub read: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub creator_name: String, + pub creator_avatar: Option, + pub recipient_name: String, + pub recipient_avatar: Option, +} + +pub struct PrivateMessageQueryBuilder<'a> { + conn: &'a PgConnection, + query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>, + for_recipient_id: i32, + unread_only: bool, + page: Option, + limit: Option, +} + +impl<'a> PrivateMessageQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self { + use super::private_message_view::private_message_mview::dsl::*; + + let query = private_message_mview.into_boxed(); + + PrivateMessageQueryBuilder { + conn, + query, + for_recipient_id, + unread_only: false, + page: None, + limit: None, + } + } + + pub fn unread_only(mut self, unread_only: bool) -> Self { + self.unread_only = unread_only; + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn list(self) -> Result, Error> { + use super::private_message_view::private_message_mview::dsl::*; + + let mut query = self.query; + + // If its unread, I only want the ones to me + if self.unread_only { + query = query + .filter(read.eq(false)) + .filter(recipient_id.eq(self.for_recipient_id)); + } + // Otherwise, I want the ALL view to show both sent and received + else { + query = query.filter( + recipient_id + .eq(self.for_recipient_id) + .or(creator_id.eq(self.for_recipient_id)), + ) + } + + let (limit, offset) = limit_and_offset(self.page, self.limit); + + query + .limit(limit) + .offset(offset) + .order_by(published.desc()) + .load::(self.conn) + } +} + +impl PrivateMessageView { + pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result { + use super::private_message_view::private_message_view::dsl::*; + + let mut query = private_message_view.into_boxed(); + + query = query + .filter(id.eq(from_private_message_id)) + .order_by(published.desc()); + + query.first::(conn) + } +} diff --git a/server/src/db/user.rs b/server/src/db/user.rs index 71b63d74..b36c07be 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -26,6 +26,7 @@ pub struct User_ { pub lang: String, pub show_avatars: bool, pub send_notifications_to_email: bool, + pub matrix_user_id: Option, } #[derive(Insertable, AsChangeset, Clone)] @@ -47,6 +48,7 @@ pub struct UserForm { pub lang: String, pub show_avatars: bool, pub send_notifications_to_email: bool, + pub matrix_user_id: Option, } impl Crud for User_ { @@ -184,6 +186,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -206,6 +209,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/user_mention.rs b/server/src/db/user_mention.rs index 21dd1675..3b10fd0f 100644 --- a/server/src/db/user_mention.rs +++ b/server/src/db/user_mention.rs @@ -68,6 +68,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, @@ -89,6 +90,7 @@ mod tests { preferred_username: None, password_encrypted: "nope".into(), email: None, + matrix_user_id: None, avatar: None, admin: false, banned: false, diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs index 23e47d4b..3ea506e7 100644 --- a/server/src/db/user_view.rs +++ b/server/src/db/user_view.rs @@ -8,6 +8,7 @@ table! { name -> Varchar, avatar -> Nullable, email -> Nullable, + matrix_user_id -> Nullable, fedi_name -> Varchar, admin -> Bool, banned -> Bool, @@ -27,6 +28,7 @@ table! { name -> Varchar, avatar -> Nullable, email -> Nullable, + matrix_user_id -> Nullable, fedi_name -> Varchar, admin -> Bool, banned -> Bool, @@ -49,6 +51,7 @@ pub struct UserView { pub name: String, pub avatar: Option, pub email: Option, + pub matrix_user_id: Option, pub fedi_name: String, pub admin: bool, pub banned: bool, diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs index 2453a1b2..b044833e 100644 --- a/server/src/routes/index.rs +++ b/server/src/routes/index.rs @@ -12,6 +12,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .route("/login", web::get().to(index)) .route("/create_post", web::get().to(index)) .route("/create_community", web::get().to(index)) + .route("/create_private_message", web::get().to(index)) .route("/communities/page/{page}", web::get().to(index)) .route("/communities", web::get().to(index)) .route("/post/{id}/comment/{id2}", web::get().to(index)) diff --git a/server/src/schema.rs b/server/src/schema.rs index 61957067..5330ed07 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -238,6 +238,19 @@ table! { } } +table! { + private_message (id) { + id -> Int4, + creator_id -> Int4, + recipient_id -> Int4, + content -> Text, + deleted -> Bool, + read -> Bool, + published -> Timestamp, + updated -> Nullable, + } +} + table! { site (id) { id -> Int4, @@ -272,6 +285,7 @@ table! { lang -> Varchar, show_avatars -> Bool, send_notifications_to_email -> Bool, + matrix_user_id -> Nullable, } } @@ -357,6 +371,7 @@ allow_tables_to_appear_in_same_query!( post_like, post_read, post_saved, + private_message, site, user_, user_ban, diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 957c5f64..5efcb7bf 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -547,5 +547,21 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { + chat.check_rate_limit_message(msg.id)?; + let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?; + let res = Oper::new(user_operation, create_private_message).perform(&conn)?; + Ok(serde_json::to_string(&res)?) + } + UserOperation::EditPrivateMessage => { + let edit_private_message: EditPrivateMessage = serde_json::from_str(data)?; + let res = Oper::new(user_operation, edit_private_message).perform(&conn)?; + Ok(serde_json::to_string(&res)?) + } + UserOperation::GetPrivateMessages => { + let messages: GetPrivateMessages = serde_json::from_str(data)?; + let res = Oper::new(user_operation, messages).perform(&conn)?; + Ok(serde_json::to_string(&res)?) + } } } diff --git a/ui/src/components/create-private-message.tsx b/ui/src/components/create-private-message.tsx new file mode 100644 index 00000000..f74d5e9f --- /dev/null +++ b/ui/src/components/create-private-message.tsx @@ -0,0 +1,52 @@ +import { Component } from 'inferno'; +import { PrivateMessageForm } from './private-message-form'; +import { WebSocketService } from '../services'; +import { PrivateMessageFormParams } from '../interfaces'; +import { i18n } from '../i18next'; + +export class CreatePrivateMessage extends Component { + constructor(props: any, context: any) { + super(props, context); + this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind( + this + ); + } + + componentDidMount() { + document.title = `${i18n.t('create_private_message')} - ${ + WebSocketService.Instance.site.name + }`; + } + + render() { + return ( +
+
+
+
{i18n.t('create_private_message')}
+ +
+
+
+ ); + } + + get params(): PrivateMessageFormParams { + let urlParams = new URLSearchParams(this.props.location.search); + let params: PrivateMessageFormParams = { + recipient_id: Number(urlParams.get('recipient_id')), + }; + + return params; + } + + handlePrivateMessageCreate() { + alert(i18n.t('message_sent')); + + // Navigate to the front + this.props.history.push(`/`); + } +} diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index a302b834..6a426bcc 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -12,10 +12,15 @@ import { GetUserMentionsResponse, UserMentionResponse, CommentResponse, + PrivateMessage as PrivateMessageI, + GetPrivateMessagesForm, + PrivateMessagesResponse, + PrivateMessageResponse, } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { msgOp, fetchLimit } from '../utils'; +import { msgOp, fetchLimit, isCommentType } from '../utils'; import { CommentNodes } from './comment-nodes'; +import { PrivateMessage } from './private-message'; import { SortSelect } from './sort-select'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; @@ -26,9 +31,10 @@ enum UnreadOrAll { } enum UnreadType { - Both, + All, Replies, Mentions, + Messages, } interface InboxState { @@ -36,6 +42,7 @@ interface InboxState { unreadType: UnreadType; replies: Array; mentions: Array; + messages: Array; sort: SortType; page: number; } @@ -44,9 +51,10 @@ export class Inbox extends Component { private subscription: Subscription; private emptyState: InboxState = { unreadOrAll: UnreadOrAll.Unread, - unreadType: UnreadType.Both, + unreadType: UnreadType.All, replies: [], mentions: [], + messages: [], sort: SortType.New, page: 1, }; @@ -103,7 +111,10 @@ export class Inbox extends Component { - {this.state.replies.length + this.state.mentions.length > 0 && + {this.state.replies.length + + this.state.mentions.length + + this.state.messages.length > + 0 && this.state.unreadOrAll == UnreadOrAll.Unread && (
  • @@ -114,9 +125,10 @@ export class Inbox extends Component {
)} {this.selects()} - {this.state.unreadType == UnreadType.Both && this.both()} + {this.state.unreadType == UnreadType.All && this.all()} {this.state.unreadType == UnreadType.Replies && this.replies()} {this.state.unreadType == UnreadType.Mentions && this.mentions()} + {this.state.unreadType == UnreadType.Messages && this.messages()} {this.paginator()} @@ -150,8 +162,8 @@ export class Inbox extends Component { -