Squashed commit of the following:

commit f5b75f342b
Merge: bd1fc2b 69389f6
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 19:17:42 2020 -0500

    Done merging http-api and private_message

commit bd1fc2b80b
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 16:46:07 2020 -0500

    Remove danger from private-message.tsx

commit 69389f61c9
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 11:21:21 2020 -0500

    Fixing http curl POST docs.

commit 7fdcae4f07
Merge: dbe9ad0 752318f
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 11:01:06 2020 -0500

    Merge remote-tracking branch 'nutomic/http-api' into dessalines-http-api

commit 752318fdf3
Author: Felix <me@nutomic.com>
Date:   Thu Jan 23 15:22:17 2020 +0100

    api fixes

commit 9ccff18f23
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 22:29:11 2020 -0500

    Adding a toaster to replace alerts. Fixes #457

commit 5197407dd2
Merge: bacb9ac 58f673a
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 21:20:38 2020 -0500

    Merge branch 'private_messaging' into dev

commit 58f673ab78
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 21:09:17 2020 -0500

    Adding message to comment node actions.

commit bacb9ac59e
Merge: 10c6505 7d3adda
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 20:37:08 2020 -0500

    Merge branch 'private_messaging' into dev

commit 10c6505968
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 20:35:20 2020 -0500

    Adding correct hello_name to mail.

commit 7d3adda0cd
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 16:35:29 2020 -0500

    Adding private messaging, and matrix user ids.

    - Fixes #244

commit dbe9ad0998
Author: Dessalines <happydooby@gmail.com>
Date:   Mon Jan 20 18:49:54 2020 -0500

    Fixing last.

commit 20c9c54806
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 13:31:37 2020 -0500

    Updating API docs.

commit dc84ccaac9
Merge: 6c61dd2 3edd75e
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 10:06:25 2020 -0500

    Merge branch 'master' into dessalines-http-api

commit 6c61dd266b
Merge: c5eecd0 e518954
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 09:09:00 2020 -0500

    Merge remote-tracking branch 'nutomic/websocket-generics' into dessalines-http-api

commit e518954bca
Author: Felix <me@nutomic.com>
Date:   Sun Jan 19 14:25:50 2020 +0100

    Use generics to reduce code duplication in websocket

commit c5eecd055e
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 00:38:45 2020 -0500

    Strongly typing WebsocketJsonResponse. Forgot comment-form.tsx

commit 0c5eb47135
Author: Dessalines <happydooby@gmail.com>
Date:   Sat Jan 18 23:54:10 2020 -0500

    First pass at fixing UI to work with new websocketresponses.

commit baf77bb6be
Author: Felix <me@nutomic.com>
Date:   Sat Jan 18 17:25:45 2020 +0100

    simplify json serialization code

commit 047ec97e18
Author: Felix <me@nutomic.com>
Date:   Sat Jan 18 14:22:25 2020 +0100

    rewrite api endpoint urls

commit 2fb4900b0c
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 17:04:37 2020 +0100

    fix typo

commit cba8081579
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 16:47:38 2020 +0100

    fix formatting

commit d7285d8c25
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 16:09:01 2020 +0100

    small fix

commit 415040a1e9
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 15:39:08 2020 +0100

    working!

commit 7a97c981a0
Author: Felix <me@nutomic.com>
Date:   Wed Jan 15 16:48:21 2020 +0100

    try to simplify code with higher order functions

commit c41082f98f
Author: Felix <me@nutomic.com>
Date:   Wed Jan 15 16:37:25 2020 +0100

    Implement HTTP API using generics (fixes #380)
This commit is contained in:
Dessalines 2020-01-23 19:39:59 -05:00
parent 9f499faa29
commit 8b88a8e75b
63 changed files with 2967 additions and 1093 deletions

18
README.md vendored
View file

@ -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,logged_in,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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
es | 83% | 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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
fr | 83% | 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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
it | 84% | 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,logged_in,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,logged_in,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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
sv | 83% | 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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
zh | 70% | 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,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
<!-- translationsstop -->

2
docs/src/SUMMARY.md vendored
View file

@ -12,5 +12,5 @@
- [Contributing](contributing.md)
- [Docker Development](contributing_docker_development.md)
- [Local Development](contributing_local_development.md)
- [Websocket API](contributing_websocket_api.md)
- [Websocket/HTTP API](contributing_websocket_http_api.md)
- [ActivityPub API Outline](contributing_apub_api_outline.md)

View file

@ -1,2 +1,2 @@
tab_spaces = 2
edition="2018"
edition="2018"

View file

@ -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;

View file

@ -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)
-- )

View file

@ -7,7 +7,7 @@ use diesel::PgConnection;
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>,
edit_id: Option<i32>, // 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<i32>,
parent_id: Option<i32>, // 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,
@ -35,7 +35,6 @@ pub struct SaveComment {
#[derive(Serialize, Deserialize, Clone)]
pub struct CommentResponse {
op: String,
pub comment: CommentView,
}
@ -53,7 +52,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -63,12 +62,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
let content_slurs_removed = remove_slurs(&data.content.to_owned());
@ -86,7 +85,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let inserted_comment = match Comment::create(&conn, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
// Scan the comment for user mentions, add those rows
@ -193,13 +192,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
};
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
Ok(CommentResponse {
op: self.op.to_string(),
comment: comment_view,
})
}
@ -211,7 +209,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -231,17 +229,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "no_comment_edit_allowed").into());
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
}
@ -264,7 +262,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Scan the comment for user mentions, add those rows
@ -310,7 +308,6 @@ impl Perform<CommentResponse> for Oper<EditComment> {
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
Ok(CommentResponse {
op: self.op.to_string(),
comment: comment_view,
})
}
@ -322,7 +319,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -335,19 +332,18 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
if data.save {
match CommentSaved::save(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
};
} else {
match CommentSaved::unsave(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
};
}
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
Ok(CommentResponse {
op: self.op.to_string(),
comment: comment_view,
})
}
@ -359,7 +355,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -368,19 +364,19 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
if data.score == -1 {
let site = SiteView::read(&conn)?;
if !site.enable_downvotes {
return Err(APIError::err(&self.op, "downvotes_disabled").into());
return Err(APIError::err("downvotes_disabled").into());
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
let like_form = CommentLikeForm {
@ -398,7 +394,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
if do_add {
let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
};
}
@ -406,7 +402,6 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
Ok(CommentResponse {
op: self.op.to_string(),
comment: liked_comment,
})
}

View file

@ -11,7 +11,6 @@ pub struct GetCommunity {
#[derive(Serialize, Deserialize)]
pub struct GetCommunityResponse {
op: String,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
@ -29,7 +28,6 @@ pub struct CreateCommunity {
#[derive(Serialize, Deserialize, Clone)]
pub struct CommunityResponse {
op: String,
pub community: CommunityView,
}
@ -43,7 +41,6 @@ pub struct ListCommunities {
#[derive(Serialize, Deserialize)]
pub struct ListCommunitiesResponse {
op: String,
communities: Vec<CommunityView>,
}
@ -59,7 +56,6 @@ pub struct BanFromCommunity {
#[derive(Serialize, Deserialize)]
pub struct BanFromCommunityResponse {
op: String,
user: UserView,
banned: bool,
}
@ -74,7 +70,6 @@ pub struct AddModToCommunity {
#[derive(Serialize, Deserialize)]
pub struct AddModToCommunityResponse {
op: String,
moderators: Vec<CommunityModeratorView>,
}
@ -107,7 +102,6 @@ pub struct GetFollowedCommunities {
#[derive(Serialize, Deserialize)]
pub struct GetFollowedCommunitiesResponse {
op: String,
communities: Vec<CommunityFollowerView>,
}
@ -141,19 +135,19 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
) {
Ok(community) => community.id,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}
}
};
let community_view = match CommunityView::read(&conn, community_id, user_id) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
let site_creator_id = Site::read(&conn, 1)?.creator_id;
@ -164,7 +158,6 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
// Return the jwt
Ok(GetCommunityResponse {
op: self.op.to_string(),
community: community_view,
moderators,
admins,
@ -178,21 +171,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
if has_slurs(&data.name)
|| has_slurs(&data.title)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
// When you create a community, make sure the user becomes a moderator and a follower
@ -210,7 +203,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let inserted_community = match Community::create(&conn, &community_form) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists").into()),
Err(_e) => return Err(APIError::err("community_already_exists").into()),
};
let community_moderator_form = CommunityModeratorForm {
@ -221,9 +214,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let _inserted_community_moderator =
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
};
let community_follower_form = CommunityFollowerForm {
@ -234,13 +225,12 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let _inserted_community_follower =
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
Ok(CommunityResponse {
op: self.op.to_string(),
community: community_view,
})
}
@ -251,19 +241,19 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let data: &EditCommunity = &self.data;
if has_slurs(&data.name) || has_slurs(&data.title) {
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
// Verify its a mod
@ -276,7 +266,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
);
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "no_community_edit_allowed").into());
return Err(APIError::err("no_community_edit_allowed").into());
}
let community_form = CommunityForm {
@ -293,7 +283,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
@ -315,7 +305,6 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
Ok(CommunityResponse {
op: self.op.to_string(),
community: community_view,
})
}
@ -354,10 +343,7 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
.list()?;
// Return the jwt
Ok(ListCommunitiesResponse {
op: self.op.to_string(),
communities,
})
Ok(ListCommunitiesResponse { communities })
}
}
@ -367,7 +353,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -380,19 +366,18 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
} else {
match CommunityFollower::ignore(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
}
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
Ok(CommunityResponse {
op: self.op.to_string(),
community: community_view,
})
}
@ -404,7 +389,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -412,14 +397,11 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let communities: Vec<CommunityFollowerView> =
match CommunityFollowerView::for_user(&conn, user_id) {
Ok(communities) => communities,
Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()),
Err(_e) => return Err(APIError::err("system_err_login").into()),
};
// Return the jwt
Ok(GetFollowedCommunitiesResponse {
op: self.op.to_string(),
communities,
})
Ok(GetFollowedCommunitiesResponse { communities })
}
}
@ -429,7 +411,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -442,12 +424,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
if data.ban {
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
};
} else {
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
};
}
@ -470,7 +452,6 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
let user_view = UserView::read(&conn, data.user_id)?;
Ok(BanFromCommunityResponse {
op: self.op.to_string(),
user: user_view,
banned: data.ban,
})
@ -483,7 +464,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -496,16 +477,12 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
if data.added {
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
};
} else {
match CommunityModerator::leave(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
};
}
@ -520,10 +497,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
Ok(AddModToCommunityResponse {
op: self.op.to_string(),
moderators,
})
Ok(AddModToCommunityResponse { moderators })
}
}
@ -533,7 +507,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -548,7 +522,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
// Make sure user is the creator, or an admin
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
let community_form = CommunityForm {
@ -565,7 +539,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// You also have to re-do the community_moderator table, reordering it.
@ -588,9 +562,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let _inserted_community_moderator =
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
};
}
@ -605,17 +577,16 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
// Return the jwt
Ok(GetCommunityResponse {
op: self.op.to_string(),
community: community_view,
moderators,
admins,

View file

@ -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::*;
@ -26,73 +28,27 @@ pub mod post;
pub mod site;
pub mod user;
#[derive(EnumString, ToString, Debug)]
pub enum UserOperation {
Login,
Register,
CreateCommunity,
CreatePost,
ListCommunities,
ListCategories,
GetPost,
GetCommunity,
CreateComment,
EditComment,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
SavePost,
EditCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
GetModlog,
BanFromCommunity,
AddModToCommunity,
CreateSite,
EditSite,
GetSite,
AddAdmin,
BanUser,
Search,
MarkAllAsRead,
SaveUserSettings,
TransferCommunity,
TransferSite,
DeleteAccount,
PasswordReset,
PasswordChange,
}
#[derive(Fail, Debug)]
#[fail(display = "{{\"op\":\"{}\", \"error\":\"{}\"}}", op, message)]
#[fail(display = "{{\"error\":\"{}\"}}", message)]
pub struct APIError {
pub op: String,
pub message: String,
}
impl APIError {
pub fn err(op: &UserOperation, msg: &str) -> Self {
pub fn err(msg: &str) -> Self {
APIError {
op: op.to_string(),
message: msg.to_string(),
}
}
}
pub struct Oper<T> {
op: UserOperation,
data: T,
}
impl<T> Oper<T> {
pub fn new(op: UserOperation, data: T) -> Oper<T> {
Oper { op, data }
pub fn new(data: T) -> Oper<T> {
Oper { data }
}
}

View file

@ -14,7 +14,6 @@ pub struct CreatePost {
#[derive(Serialize, Deserialize, Clone)]
pub struct PostResponse {
op: String,
pub post: PostView,
}
@ -26,7 +25,6 @@ pub struct GetPost {
#[derive(Serialize, Deserialize)]
pub struct GetPostResponse {
op: String,
post: PostView,
comments: Vec<CommentView>,
community: CommunityView,
@ -46,7 +44,6 @@ pub struct GetPosts {
#[derive(Serialize, Deserialize)]
pub struct GetPostsResponse {
op: String,
posts: Vec<PostView>,
}
@ -59,7 +56,6 @@ pub struct CreatePostLike {
#[derive(Serialize, Deserialize)]
pub struct CreatePostLikeResponse {
op: String,
post: PostView,
}
@ -93,23 +89,23 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let user_id = claims.id;
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
let post_form = PostForm {
@ -128,7 +124,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let inserted_post = match Post::create(&conn, &post_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()),
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
};
// They like their own post by default
@ -141,19 +137,16 @@ impl Perform<PostResponse> for Oper<CreatePost> {
// Only add the like if the score isnt 0
let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
};
// Refetch the view
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
};
Ok(PostResponse {
op: self.op.to_string(),
post: post_view,
})
Ok(PostResponse { post: post_view })
}
}
@ -174,7 +167,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
let post_view = match PostView::read(&conn, data.id, user_id) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
};
let comments = CommentQueryBuilder::create(&conn)
@ -195,7 +188,6 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
// Return the jwt
Ok(GetPostResponse {
op: self.op.to_string(),
post: post_view,
comments,
community,
@ -241,13 +233,10 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
.list()
{
Ok(posts) => posts,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()),
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
};
Ok(GetPostsResponse {
op: self.op.to_string(),
posts,
})
Ok(GetPostsResponse { posts })
}
}
@ -257,7 +246,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -266,19 +255,19 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
if data.score == -1 {
let site = SiteView::read(&conn)?;
if !site.enable_downvotes {
return Err(APIError::err(&self.op, "downvotes_disabled").into());
return Err(APIError::err("downvotes_disabled").into());
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
let like_form = PostLikeForm {
@ -295,20 +284,17 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
if do_add {
let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
};
}
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
};
// just output the score
Ok(CreatePostLikeResponse {
op: self.op.to_string(),
post: post_view,
})
Ok(CreatePostLikeResponse { post: post_view })
}
}
@ -316,12 +302,12 @@ impl Perform<PostResponse> for Oper<EditPost> {
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
let data: &EditPost = &self.data;
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -336,17 +322,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
);
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "no_post_edit_allowed").into());
return Err(APIError::err("no_post_edit_allowed").into());
}
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban").into());
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban").into());
return Err(APIError::err("site_ban").into());
}
let post_form = PostForm {
@ -365,7 +351,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
};
// Mod tables
@ -399,10 +385,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
Ok(PostResponse {
op: self.op.to_string(),
post: post_view,
})
Ok(PostResponse { post: post_view })
}
}
@ -412,7 +395,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -425,20 +408,17 @@ impl Perform<PostResponse> for Oper<SavePost> {
if data.save {
match PostSaved::save(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
};
} else {
match PostSaved::unsave(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
};
}
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
Ok(PostResponse {
op: self.op.to_string(),
post: post_view,
})
Ok(PostResponse { post: post_view })
}
}

View file

@ -7,7 +7,6 @@ pub struct ListCategories;
#[derive(Serialize, Deserialize)]
pub struct ListCategoriesResponse {
op: String,
categories: Vec<Category>,
}
@ -24,7 +23,6 @@ pub struct Search {
#[derive(Serialize, Deserialize)]
pub struct SearchResponse {
op: String,
type_: String,
comments: Vec<CommentView>,
posts: Vec<PostView>,
@ -42,7 +40,6 @@ pub struct GetModlog {
#[derive(Serialize, Deserialize)]
pub struct GetModlogResponse {
op: String,
removed_posts: Vec<ModRemovePostView>,
locked_posts: Vec<ModLockPostView>,
stickied_posts: Vec<ModStickyPostView>,
@ -79,13 +76,11 @@ pub struct GetSite;
#[derive(Serialize, Deserialize)]
pub struct SiteResponse {
op: String,
site: SiteView,
}
#[derive(Serialize, Deserialize)]
pub struct GetSiteResponse {
op: String,
site: Option<SiteView>,
admins: Vec<UserView>,
banned: Vec<UserView>,
@ -105,10 +100,7 @@ impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
let categories: Vec<Category> = Category::list_all(&conn)?;
// Return the jwt
Ok(ListCategoriesResponse {
op: self.op.to_string(),
categories,
})
Ok(ListCategoriesResponse { categories })
}
}
@ -172,7 +164,6 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
// Return the jwt
Ok(GetModlogResponse {
op: self.op.to_string(),
removed_posts,
locked_posts,
stickied_posts,
@ -192,20 +183,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
if has_slurs(&data.name)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
let site_form = SiteForm {
@ -220,15 +211,12 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
match Site::create(&conn, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()),
Err(_e) => return Err(APIError::err("site_already_exists").into()),
};
let site_view = SiteView::read(&conn)?;
Ok(SiteResponse {
op: self.op.to_string(),
site: site_view,
})
Ok(SiteResponse { site: site_view })
}
}
@ -238,20 +226,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
if has_slurs(&data.name)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
let found_site = Site::read(&conn, 1)?;
@ -268,15 +256,12 @@ impl Perform<SiteResponse> for Oper<EditSite> {
match Site::update(&conn, 1, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
};
let site_view = SiteView::read(&conn)?;
Ok(SiteResponse {
op: self.op.to_string(),
site: site_view,
})
Ok(SiteResponse { site: site_view })
}
}
@ -301,7 +286,6 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
let banned = UserView::banned(&conn)?;
Ok(GetSiteResponse {
op: self.op.to_string(),
site: site_view,
admins,
banned,
@ -419,7 +403,6 @@ impl Perform<SearchResponse> for Oper<Search> {
// Return the jwt
Ok(SearchResponse {
op: self.op.to_string(),
type_: data.type_.to_owned(),
comments,
posts,
@ -435,7 +418,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -444,7 +427,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
// Make sure user is the creator
if read_site.creator_id != user_id {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
let site_form = SiteForm {
@ -459,7 +442,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
match Site::update(&conn, 1, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
};
// Mod tables
@ -484,7 +467,6 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
let banned = UserView::banned(&conn)?;
Ok(GetSiteResponse {
op: self.op.to_string(),
site: Some(site_view),
admins,
banned,

View file

@ -30,6 +30,7 @@ pub struct SaveUserSettings {
lang: String,
avatar: Option<String>,
email: Option<String>,
matrix_user_id: Option<String>,
new_password: Option<String>,
new_password_verify: Option<String>,
old_password: Option<String>,
@ -40,7 +41,6 @@ pub struct SaveUserSettings {
#[derive(Serialize, Deserialize)]
pub struct LoginResponse {
op: String,
jwt: String,
}
@ -58,7 +58,6 @@ pub struct GetUserDetails {
#[derive(Serialize, Deserialize)]
pub struct GetUserDetailsResponse {
op: String,
user: UserView,
follows: Vec<CommunityFollowerView>,
moderates: Vec<CommunityModeratorView>,
@ -69,13 +68,11 @@ pub struct GetUserDetailsResponse {
#[derive(Serialize, Deserialize)]
pub struct GetRepliesResponse {
op: String,
replies: Vec<ReplyView>,
}
#[derive(Serialize, Deserialize)]
pub struct GetUserMentionsResponse {
op: String,
mentions: Vec<UserMentionView>,
}
@ -93,7 +90,6 @@ pub struct AddAdmin {
#[derive(Serialize, Deserialize)]
pub struct AddAdminResponse {
op: String,
admins: Vec<UserView>,
}
@ -108,7 +104,6 @@ pub struct BanUser {
#[derive(Serialize, Deserialize)]
pub struct BanUserResponse {
op: String,
user: UserView,
banned: bool,
}
@ -140,7 +135,6 @@ pub struct EditUserMention {
#[derive(Serialize, Deserialize, Clone)]
pub struct UserMentionResponse {
op: String,
mention: UserMentionView,
}
@ -156,9 +150,7 @@ pub struct PasswordReset {
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PasswordResetResponse {
op: String,
}
pub struct PasswordResetResponse {}
#[derive(Serialize, Deserialize)]
pub struct PasswordChange {
@ -167,6 +159,40 @@ 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<String>,
deleted: Option<bool>,
read: Option<bool>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetPrivateMessages {
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PrivateMessagesResponse {
messages: Vec<PrivateMessageView>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PrivateMessageResponse {
message: PrivateMessageView,
}
impl Perform<LoginResponse> for Oper<Login> {
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
let data: &Login = &self.data;
@ -174,20 +200,17 @@ impl Perform<LoginResponse> for Oper<Login> {
// Fetch that username / email
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
};
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect").into());
return Err(APIError::err("password_incorrect").into());
}
// Return the jwt
Ok(LoginResponse {
op: self.op.to_string(),
jwt: user.jwt(),
})
Ok(LoginResponse { jwt: user.jwt() })
}
}
@ -198,22 +221,22 @@ impl Perform<LoginResponse> for Oper<Register> {
// Make sure site has open registration
if let Ok(site) = SiteView::read(&conn) {
if !site.open_registration {
return Err(APIError::err(&self.op, "registration_closed").into());
return Err(APIError::err("registration_closed").into());
}
}
// Make sure passwords match
if data.password != data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match").into());
return Err(APIError::err("passwords_dont_match").into());
}
if has_slurs(&data.username) {
return Err(APIError::err(&self.op, "no_slurs").into());
return Err(APIError::err("no_slurs").into());
}
// Make sure there are no admins
if data.admin && !UserView::admins(&conn)?.is_empty() {
return Err(APIError::err(&self.op, "admin_already_created").into());
return Err(APIError::err("admin_already_created").into());
}
// Register the new user
@ -221,6 +244,7 @@ impl Perform<LoginResponse> for Oper<Register> {
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,
@ -248,7 +272,7 @@ impl Perform<LoginResponse> for Oper<Register> {
"user_already_exists"
};
return Err(APIError::err(&self.op, err_type).into());
return Err(APIError::err(err_type).into());
}
};
@ -280,7 +304,7 @@ impl Perform<LoginResponse> for Oper<Register> {
let _inserted_community_follower =
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
// If its an admin, add them as a mod and follower to main
@ -293,15 +317,12 @@ impl Perform<LoginResponse> for Oper<Register> {
let _inserted_community_moderator =
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
};
}
// Return the jwt
Ok(LoginResponse {
op: self.op.to_string(),
jwt: inserted_user.jwt(),
})
}
@ -313,7 +334,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -331,7 +352,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
Some(new_password_verify) => {
// Make sure passwords match
if new_password != new_password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match").into());
return Err(APIError::err("passwords_dont_match").into());
}
// Check the old password
@ -340,14 +361,14 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
let valid: bool =
verify(old_password, &read_user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect").into());
return Err(APIError::err("password_incorrect").into());
}
User_::update_password(&conn, user_id, &new_password)?.password_encrypted
}
None => return Err(APIError::err(&self.op, "password_incorrect").into()),
None => return Err(APIError::err("password_incorrect").into()),
}
}
None => return Err(APIError::err(&self.op, "passwords_dont_match").into()),
None => return Err(APIError::err("passwords_dont_match").into()),
}
}
None => read_user.password_encrypted,
@ -357,6 +378,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
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,
@ -383,13 +405,12 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
"user_already_exists"
};
return Err(APIError::err(&self.op, err_type).into());
return Err(APIError::err(err_type).into());
}
};
// Return the jwt
Ok(LoginResponse {
op: self.op.to_string(),
jwt: updated_user.jwt(),
})
}
@ -430,9 +451,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
.unwrap_or_else(|| "admin".to_string()),
) {
Ok(user) => user.id,
Err(_e) => {
return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into())
}
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
}
}
};
@ -475,7 +494,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
// Return the jwt
Ok(GetUserDetailsResponse {
op: self.op.to_string(),
user: user_view,
follows,
moderates,
@ -492,22 +510,24 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
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,
@ -525,7 +545,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
match User_::update(&conn, data.user_id, &user_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
};
// Mod tables
@ -543,10 +563,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
Ok(AddAdminResponse {
op: self.op.to_string(),
admins,
})
Ok(AddAdminResponse { admins })
}
}
@ -556,22 +573,24 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
return Err(APIError::err("not_an_admin").into());
}
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,
@ -589,7 +608,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
match User_::update(&conn, data.user_id, &user_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
};
// Mod tables
@ -611,7 +630,6 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
let user_view = UserView::read(&conn, data.user_id)?;
Ok(BanUserResponse {
op: self.op.to_string(),
user: user_view,
banned: data.ban,
})
@ -624,7 +642,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -638,10 +656,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
.limit(data.limit)
.list()?;
Ok(GetRepliesResponse {
op: self.op.to_string(),
replies,
})
Ok(GetRepliesResponse { replies })
}
}
@ -651,7 +666,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -665,10 +680,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
.limit(data.limit)
.list()?;
Ok(GetUserMentionsResponse {
op: self.op.to_string(),
mentions,
})
Ok(GetUserMentionsResponse { mentions })
}
}
@ -678,7 +690,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -694,13 +706,12 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
let _updated_user_mention =
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
Ok(UserMentionResponse {
op: self.op.to_string(),
mention: user_mention_view,
})
}
@ -712,7 +723,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -737,7 +748,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
}
@ -758,14 +769,35 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let _updated_mention =
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
Ok(mention) => mention,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
}
Ok(GetRepliesResponse {
op: self.op.to_string(),
replies: vec![],
})
// 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("couldnt_update_private_message").into()),
};
}
Ok(GetRepliesResponse { replies: vec![] })
}
}
@ -775,7 +807,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
@ -785,7 +817,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect").into());
return Err(APIError::err("password_incorrect").into());
}
// Comments
@ -808,7 +840,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
}
@ -836,12 +868,11 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
let _updated_post = match Post::update(&conn, post.id, &post_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
};
}
Ok(LoginResponse {
op: self.op.to_string(),
jwt: data.auth.to_owned(),
})
}
@ -854,7 +885,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
// Fetch that email
let user: User_ = match User_::find_by_email(&conn, &data.email) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
};
// Generate a random token
@ -871,12 +902,10 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
match send_email(subject, user_email, &user.name, html) {
Ok(_o) => _o,
Err(_e) => return Err(APIError::err(&self.op, &_e).into()),
Err(_e) => return Err(APIError::err(&_e).into()),
};
Ok(PasswordResetResponse {
op: self.op.to_string(),
})
Ok(PasswordResetResponse {})
}
}
@ -889,19 +918,156 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
// Make sure passwords match
if data.password != data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match").into());
return Err(APIError::err("passwords_dont_match").into());
}
// Update the user with the new password
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
};
// Return the jwt
Ok(LoginResponse {
op: self.op.to_string(),
jwt: updated_user.jwt(),
})
}
}
impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
let data: &CreatePrivateMessage = &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 hostname = &format!("https://{}", Settings::get().hostname);
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err("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("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!(
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
claims.username, &content_slurs_removed, hostname
);
match send_email(subject, &email, &recipient_user.name, html) {
Ok(_o) => _o,
Err(e) => eprintln!("{}", e),
};
}
}
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
Ok(PrivateMessageResponse { message })
}
}
impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
let data: &EditPrivateMessage = &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 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("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("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("couldnt_update_private_message").into()),
};
let message = PrivateMessageView::read(&conn, data.edit_id)?;
Ok(PrivateMessageResponse { message })
}
}
impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
let data: &GetPrivateMessages = &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 messages = PrivateMessageQueryBuilder::create(&conn, user_id)
.page(data.page)
.limit(data.limit)
.unread_only(data.unread_only)
.list()?;
Ok(PrivateMessagesResponse { messages })
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<chrono::NaiveDateTime>,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "private_message"]
pub struct PrivateMessageForm {
pub creator_id: i32,
pub recipient_id: i32,
pub content: Option<String>,
pub deleted: Option<bool>,
pub read: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>,
}
impl Crud<PrivateMessageForm> for PrivateMessage {
fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
private_message.find(private_message_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
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<Self, Error> {
use crate::schema::private_message::dsl::*;
insert_into(private_message)
.values(private_message_form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
private_message_id: i32,
private_message_form: &PrivateMessageForm,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(private_message_form)
.get_result::<Self>(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);
}
}

View file

@ -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<Timestamp>,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
}
}
table! {
private_message_mview (id) {
id -> Int4,
creator_id -> Int4,
recipient_id -> Int4,
content -> Text,
deleted -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
}
}
#[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<chrono::NaiveDateTime>,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub recipient_name: String,
pub recipient_avatar: Option<String>,
}
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<i64>,
limit: Option<i64>,
}
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<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<PrivateMessageView>, 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::<PrivateMessageView>(self.conn)
}
}
impl PrivateMessageView {
pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
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::<Self>(conn)
}
}

View file

@ -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<String>,
}
#[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<String>,
}
impl Crud<UserForm> 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,

View file

@ -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,

View file

@ -8,6 +8,7 @@ table! {
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
@ -27,6 +28,7 @@ table! {
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
@ -49,6 +51,7 @@ pub struct UserView {
pub name: String,
pub avatar: Option<String>,
pub email: Option<String>,
pub matrix_user_id: Option<String>,
pub fedi_name: String,
pub admin: bool,
pub banned: bool,

View file

@ -105,7 +105,7 @@ pub fn send_email(
let mut mailer = SmtpClient::new_simple(&email_config.smtp_server)
.unwrap()
.hello_name(ClientId::Domain("localhost".to_string()))
.hello_name(ClientId::Domain(Settings::get().hostname.to_owned()))
.credentials(Credentials::new(
email_config.smtp_login.to_owned(),
email_config.smtp_password.to_owned(),
@ -117,6 +117,8 @@ pub fn send_email(
let result = mailer.send(email.into());
mailer.close();
match result {
Ok(_) => Ok(()),
Err(_) => Err("no_email_setup".to_string()),

View file

@ -6,7 +6,7 @@ use actix::prelude::*;
use actix_web::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket};
use lemmy_server::settings::Settings;
use lemmy_server::websocket::server::*;
use std::io;
@ -44,6 +44,7 @@ async fn main() -> io::Result<()> {
.data(pool.clone())
.data(server.clone())
// The routes
.configure(api::config)
.configure(federation::config)
.configure(feeds::config)
.configure(index::config)

103
server/src/routes/api.rs Normal file
View file

@ -0,0 +1,103 @@
use crate::api::comment::*;
use crate::api::community::*;
use crate::api::post::*;
use crate::api::site::*;
use crate::api::user::*;
use crate::api::{Oper, Perform};
use actix_web::{web, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Serialize;
type DbParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
#[rustfmt::skip]
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
// Site
.route("/api/v1/site", web::get().to(route_get::<GetSite, GetSiteResponse>))
.route("/api/v1/categories", web::get().to(route_get::<ListCategories, ListCategoriesResponse>))
.route("/api/v1/modlog", web::get().to(route_get::<GetModlog, GetModlogResponse>))
.route("/api/v1/search", web::get().to(route_get::<Search, SearchResponse>))
// Community
.route("/api/v1/community", web::post().to(route_post::<CreateCommunity, CommunityResponse>))
.route("/api/v1/community", web::get().to(route_get::<GetCommunity, GetCommunityResponse>))
.route("/api/v1/community", web::put().to(route_post::<EditCommunity, CommunityResponse>))
.route("/api/v1/community/list", web::get().to(route_get::<ListCommunities, ListCommunitiesResponse>))
.route("/api/v1/community/follow", web::post().to(route_post::<FollowCommunity, CommunityResponse>))
// Post
.route("/api/v1/post", web::post().to(route_post::<CreatePost, PostResponse>))
.route("/api/v1/post", web::put().to(route_post::<EditPost, PostResponse>))
.route("/api/v1/post", web::get().to(route_get::<GetPost, GetPostResponse>))
.route("/api/v1/post/list", web::get().to(route_get::<GetPosts, GetPostsResponse>))
.route("/api/v1/post/like", web::post().to(route_post::<CreatePostLike, CreatePostLikeResponse>))
.route("/api/v1/post/save", web::put().to(route_post::<SavePost, PostResponse>))
// Comment
.route("/api/v1/comment", web::post().to(route_post::<CreateComment, CommentResponse>))
.route("/api/v1/comment", web::put().to(route_post::<EditComment, CommentResponse>))
.route("/api/v1/comment/like", web::post().to(route_post::<CreateCommentLike, CommentResponse>))
.route("/api/v1/comment/save", web::put().to(route_post::<SaveComment, CommentResponse>))
// User
.route("/api/v1/user", web::get().to(route_get::<GetUserDetails, GetUserDetailsResponse>))
.route("/api/v1/user/mention", web::get().to(route_get::<GetUserMentions, GetUserMentionsResponse>))
.route("/api/v1/user/mention", web::put().to(route_post::<EditUserMention, UserMentionResponse>))
.route("/api/v1/user/replies", web::get().to(route_get::<GetReplies, GetRepliesResponse>))
.route("/api/v1/user/followed_communities", web::get().to(route_get::<GetFollowedCommunities, GetFollowedCommunitiesResponse>))
// Mod actions
.route("/api/v1/community/transfer", web::post().to(route_post::<TransferCommunity, GetCommunityResponse>))
.route("/api/v1/community/ban_user", web::post().to(route_post::<BanFromCommunity, BanFromCommunityResponse>))
.route("/api/v1/community/mod", web::post().to(route_post::<AddModToCommunity, AddModToCommunityResponse>))
// Admin actions
.route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
.route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
.route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
.route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
// User account actions
.route("/api/v1/user/login", web::post().to(route_post::<Login, LoginResponse>))
.route("/api/v1/user/register", web::post().to(route_post::<Register, LoginResponse>))
.route("/api/v1/user/delete_account", web::post().to(route_post::<DeleteAccount, LoginResponse>))
.route("/api/v1/user/password_reset", web::post().to(route_post::<PasswordReset, PasswordResetResponse>))
.route("/api/v1/user/password_change", web::post().to(route_post::<PasswordChange, LoginResponse>))
.route("/api/v1/user/mark_all_as_read", web::post().to(route_post::<MarkAllAsRead, GetRepliesResponse>))
.route("/api/v1/user/save_user_settings", web::put().to(route_post::<SaveUserSettings, LoginResponse>));
}
fn perform<Request, Response>(data: Request, db: DbParam) -> Result<HttpResponse, Error>
where
Response: Serialize,
Oper<Request>: Perform<Response>,
{
let conn = match db.get() {
Ok(c) => c,
Err(e) => return Err(format_err!("{}", e)),
};
let oper: Oper<Request> = Oper::new(data);
let response = oper.perform(&conn);
Ok(HttpResponse::Ok().json(response?))
}
async fn route_get<Data, Response>(
data: web::Query<Data>,
db: DbParam,
) -> Result<HttpResponse, Error>
where
Data: Serialize,
Response: Serialize,
Oper<Data>: Perform<Response>,
{
perform::<Data, Response>(data.0, db)
}
async fn route_post<Data, Response>(
data: web::Json<Data>,
db: DbParam,
) -> Result<HttpResponse, Error>
where
Data: Serialize,
Response: Serialize,
Oper<Data>: Perform<Response>,
{
perform::<Data, Response>(data.0, db)
}

View file

@ -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))

View file

@ -1,3 +1,4 @@
pub mod api;
pub mod federation;
pub mod feeds;
pub mod index;

View file

@ -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<Timestamp>,
}
}
table! {
site (id) {
id -> Int4,
@ -272,6 +285,7 @@ table! {
lang -> Varchar,
show_avatars -> Bool,
send_notifications_to_email -> Bool,
matrix_user_id -> Nullable<Text>,
}
}
@ -357,6 +371,7 @@ allow_tables_to_appear_in_same_query!(
post_like,
post_read,
post_saved,
private_message,
site,
user_,
user_ban,

View file

@ -1 +1,47 @@
pub mod server;
#[derive(EnumString, ToString, Debug)]
pub enum UserOperation {
Login,
Register,
CreateCommunity,
CreatePost,
ListCommunities,
ListCategories,
GetPost,
GetCommunity,
CreateComment,
EditComment,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
SavePost,
EditCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
GetModlog,
BanFromCommunity,
AddModToCommunity,
CreateSite,
EditSite,
GetSite,
AddAdmin,
BanUser,
Search,
MarkAllAsRead,
SaveUserSettings,
TransferCommunity,
TransferSite,
DeleteAccount,
PasswordReset,
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
GetPrivateMessages,
}

View file

@ -3,7 +3,7 @@
//! room through `ChatServer`.
use actix::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use diesel::PgConnection;
use failure::Error;
use rand::{rngs::ThreadRng, Rng};
@ -19,6 +19,7 @@ use crate::api::post::*;
use crate::api::site::*;
use crate::api::user::*;
use crate::api::*;
use crate::websocket::UserOperation;
use crate::Settings;
/// Chat server sends this messages to session
@ -201,7 +202,6 @@ impl ChatServer {
);
Err(
APIError {
op: "Rate Limit".to_string(),
message: format!("Too many requests. {} per {} seconds", rate, per),
}
.into(),
@ -295,11 +295,42 @@ impl Handler<StandardMessage> for ChatServer {
}
}
#[derive(Serialize)]
struct WebsocketResponse<T> {
op: String,
data: T,
}
fn to_json_string<T>(op: &UserOperation, data: T) -> Result<String, Error>
where
T: Serialize,
{
let response = WebsocketResponse {
op: op.to_string(),
data,
};
Ok(serde_json::to_string(&response)?)
}
fn do_user_operation<'a, Data, Response>(
op: UserOperation,
data: &str,
conn: &PooledConnection<ConnectionManager<PgConnection>>,
) -> Result<String, Error>
where
for<'de> Data: Deserialize<'de> + 'a,
Response: Serialize,
Oper<Data>: Perform<Response>,
{
let parsed_data: Data = serde_json::from_str(data)?;
let res = Oper::new(parsed_data).perform(&conn)?;
to_json_string(&op, &res)
}
fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<String, Error> {
let json: Value = serde_json::from_str(&msg.msg)?;
let data = &json["data"].to_string();
let op = &json["op"].as_str().ok_or(APIError {
op: "Unknown op type".to_string(),
message: "Unknown op type".to_string(),
})?;
@ -307,245 +338,194 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
let user_operation: UserOperation = UserOperation::from_str(&op)?;
// TODO: none of the chat messages are going to work if stuff is submitted via http api,
// need to move that handling elsewhere
match user_operation {
UserOperation::Login => {
let login: Login = serde_json::from_str(data)?;
let res = Oper::new(user_operation, login).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
}
UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn),
UserOperation::Register => {
let register: Register = serde_json::from_str(data)?;
let res = Oper::new(user_operation, register).perform(&conn);
if res.is_ok() {
chat.check_rate_limit_register(msg.id)?;
}
Ok(serde_json::to_string(&res?)?)
chat.check_rate_limit_register(msg.id)?;
do_user_operation::<Register, LoginResponse>(user_operation, data, &conn)
}
UserOperation::GetUserDetails => {
let get_user_details: GetUserDetails = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_user_details).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn)
}
UserOperation::SaveUserSettings => {
let save_user_settings: SaveUserSettings = serde_json::from_str(data)?;
let res = Oper::new(user_operation, save_user_settings).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<SaveUserSettings, LoginResponse>(user_operation, data, &conn)
}
UserOperation::AddAdmin => {
let add_admin: AddAdmin = serde_json::from_str(data)?;
let res = Oper::new(user_operation, add_admin).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<AddAdmin, AddAdminResponse>(user_operation, data, &conn)
}
UserOperation::BanUser => {
let ban_user: BanUser = serde_json::from_str(data)?;
let res = Oper::new(user_operation, ban_user).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<BanUser, BanUserResponse>(user_operation, data, &conn)
}
UserOperation::GetReplies => {
let get_replies: GetReplies = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_replies).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetReplies, GetRepliesResponse>(user_operation, data, &conn)
}
UserOperation::GetUserMentions => {
let get_user_mentions: GetUserMentions = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_user_mentions).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetUserMentions, GetUserMentionsResponse>(user_operation, data, &conn)
}
UserOperation::EditUserMention => {
let edit_user_mention: EditUserMention = serde_json::from_str(data)?;
let res = Oper::new(user_operation, edit_user_mention).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<EditUserMention, UserMentionResponse>(user_operation, data, &conn)
}
UserOperation::MarkAllAsRead => {
let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
let res = Oper::new(user_operation, mark_all_as_read).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<MarkAllAsRead, GetRepliesResponse>(user_operation, data, &conn)
}
UserOperation::GetCommunity => {
let get_community: GetCommunity = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_community).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetCommunity, GetCommunityResponse>(user_operation, data, &conn)
}
UserOperation::ListCommunities => {
let list_communities: ListCommunities = serde_json::from_str(data)?;
let res = Oper::new(user_operation, list_communities).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<ListCommunities, ListCommunitiesResponse>(user_operation, data, &conn)
}
UserOperation::CreateCommunity => {
chat.check_rate_limit_register(msg.id)?;
let create_community: CreateCommunity = serde_json::from_str(data)?;
let res = Oper::new(user_operation, create_community).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<CreateCommunity, CommunityResponse>(user_operation, data, &conn)
}
UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(data)?;
let res = Oper::new(user_operation, edit_community).perform(&conn)?;
let res = Oper::new(edit_community).perform(&conn)?;
let mut community_sent: CommunityResponse = res.clone();
community_sent.community.user_id = None;
community_sent.community.subscribed = None;
let community_sent_str = serde_json::to_string(&community_sent)?;
let community_sent_str = to_json_string(&user_operation, &community_sent)?;
chat.send_community_message(community_sent.community.id, &community_sent_str, msg.id)?;
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::FollowCommunity => {
let follow_community: FollowCommunity = serde_json::from_str(data)?;
let res = Oper::new(user_operation, follow_community).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
}
UserOperation::GetFollowedCommunities => {
let followed_communities: GetFollowedCommunities = serde_json::from_str(data)?;
let res = Oper::new(user_operation, followed_communities).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<FollowCommunity, CommunityResponse>(user_operation, data, &conn)
}
UserOperation::GetFollowedCommunities => do_user_operation::<
GetFollowedCommunities,
GetFollowedCommunitiesResponse,
>(user_operation, data, &conn),
UserOperation::BanFromCommunity => {
let ban_from_community: BanFromCommunity = serde_json::from_str(data)?;
let community_id = ban_from_community.community_id;
let res = Oper::new(user_operation, ban_from_community).perform(&conn)?;
let res_str = serde_json::to_string(&res)?;
let res = Oper::new(ban_from_community).perform(&conn)?;
let res_str = to_json_string(&user_operation, &res)?;
chat.send_community_message(community_id, &res_str, msg.id)?;
Ok(res_str)
}
UserOperation::AddModToCommunity => {
let mod_add_to_community: AddModToCommunity = serde_json::from_str(data)?;
let community_id = mod_add_to_community.community_id;
let res = Oper::new(user_operation, mod_add_to_community).perform(&conn)?;
let res_str = serde_json::to_string(&res)?;
let res = Oper::new(mod_add_to_community).perform(&conn)?;
let res_str = to_json_string(&user_operation, &res)?;
chat.send_community_message(community_id, &res_str, msg.id)?;
Ok(res_str)
}
UserOperation::ListCategories => {
let list_categories: ListCategories = ListCategories;
let res = Oper::new(user_operation, list_categories).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<ListCategories, ListCategoriesResponse>(user_operation, data, &conn)
}
UserOperation::CreatePost => {
chat.check_rate_limit_post(msg.id)?;
let create_post: CreatePost = serde_json::from_str(data)?;
let res = Oper::new(user_operation, create_post).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<CreatePost, PostResponse>(user_operation, data, &conn)
}
UserOperation::GetPost => {
let get_post: GetPost = serde_json::from_str(data)?;
chat.join_room(get_post.id, msg.id);
let res = Oper::new(user_operation, get_post).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
let res = Oper::new(get_post).perform(&conn)?;
to_json_string(&user_operation, &res)
}
UserOperation::GetPosts => {
let get_posts: GetPosts = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_posts).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetPosts, GetPostsResponse>(user_operation, data, &conn)
}
UserOperation::CreatePostLike => {
chat.check_rate_limit_message(msg.id)?;
let create_post_like: CreatePostLike = serde_json::from_str(data)?;
let res = Oper::new(user_operation, create_post_like).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<CreatePostLike, CreatePostLikeResponse>(user_operation, data, &conn)
}
UserOperation::EditPost => {
let edit_post: EditPost = serde_json::from_str(data)?;
let res = Oper::new(user_operation, edit_post).perform(&conn)?;
let res = Oper::new(edit_post).perform(&conn)?;
let mut post_sent = res.clone();
post_sent.post.my_vote = None;
let post_sent_str = serde_json::to_string(&post_sent)?;
let post_sent_str = to_json_string(&user_operation, &post_sent)?;
chat.send_room_message(post_sent.post.id, &post_sent_str, msg.id);
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::SavePost => {
let save_post: SavePost = serde_json::from_str(data)?;
let res = Oper::new(user_operation, save_post).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn)
}
UserOperation::CreateComment => {
chat.check_rate_limit_message(msg.id)?;
let create_comment: CreateComment = serde_json::from_str(data)?;
let post_id = create_comment.post_id;
let res = Oper::new(user_operation, create_comment).perform(&conn)?;
let res = Oper::new(create_comment).perform(&conn)?;
let mut comment_sent = res.clone();
comment_sent.comment.my_vote = None;
comment_sent.comment.user_id = None;
let comment_sent_str = serde_json::to_string(&comment_sent)?;
let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
chat.send_room_message(post_id, &comment_sent_str, msg.id);
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::EditComment => {
let edit_comment: EditComment = serde_json::from_str(data)?;
let post_id = edit_comment.post_id;
let res = Oper::new(user_operation, edit_comment).perform(&conn)?;
let res = Oper::new(edit_comment).perform(&conn)?;
let mut comment_sent = res.clone();
comment_sent.comment.my_vote = None;
comment_sent.comment.user_id = None;
let comment_sent_str = serde_json::to_string(&comment_sent)?;
let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
chat.send_room_message(post_id, &comment_sent_str, msg.id);
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::SaveComment => {
let save_comment: SaveComment = serde_json::from_str(data)?;
let res = Oper::new(user_operation, save_comment).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn)
}
UserOperation::CreateCommentLike => {
chat.check_rate_limit_message(msg.id)?;
let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
let post_id = create_comment_like.post_id;
let res = Oper::new(user_operation, create_comment_like).perform(&conn)?;
let res = Oper::new(create_comment_like).perform(&conn)?;
let mut comment_sent = res.clone();
comment_sent.comment.my_vote = None;
comment_sent.comment.user_id = None;
let comment_sent_str = serde_json::to_string(&comment_sent)?;
let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
chat.send_room_message(post_id, &comment_sent_str, msg.id);
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::GetModlog => {
let get_modlog: GetModlog = serde_json::from_str(data)?;
let res = Oper::new(user_operation, get_modlog).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<GetModlog, GetModlogResponse>(user_operation, data, &conn)
}
UserOperation::CreateSite => {
let create_site: CreateSite = serde_json::from_str(data)?;
let res = Oper::new(user_operation, create_site).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<CreateSite, SiteResponse>(user_operation, data, &conn)
}
UserOperation::EditSite => {
let edit_site: EditSite = serde_json::from_str(data)?;
let res = Oper::new(user_operation, edit_site).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<EditSite, SiteResponse>(user_operation, data, &conn)
}
UserOperation::GetSite => {
let online: usize = chat.sessions.len();
let get_site: GetSite = serde_json::from_str(data)?;
let mut res = Oper::new(user_operation, get_site).perform(&conn)?;
let mut res = Oper::new(get_site).perform(&conn)?;
res.online = online;
Ok(serde_json::to_string(&res)?)
to_json_string(&user_operation, &res)
}
UserOperation::Search => {
let search: Search = serde_json::from_str(data)?;
let res = Oper::new(user_operation, search).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
}
UserOperation::TransferCommunity => {
let transfer_community: TransferCommunity = serde_json::from_str(data)?;
let res = Oper::new(user_operation, transfer_community).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<TransferCommunity, GetCommunityResponse>(user_operation, data, &conn)
}
UserOperation::TransferSite => {
let transfer_site: TransferSite = serde_json::from_str(data)?;
let res = Oper::new(user_operation, transfer_site).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<TransferSite, GetSiteResponse>(user_operation, data, &conn)
}
UserOperation::DeleteAccount => {
let delete_account: DeleteAccount = serde_json::from_str(data)?;
let res = Oper::new(user_operation, delete_account).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<DeleteAccount, LoginResponse>(user_operation, data, &conn)
}
UserOperation::PasswordReset => {
let password_reset: PasswordReset = serde_json::from_str(data)?;
let res = Oper::new(user_operation, password_reset).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<PasswordReset, PasswordResetResponse>(user_operation, data, &conn)
}
UserOperation::PasswordChange => {
let password_change: PasswordChange = serde_json::from_str(data)?;
let res = Oper::new(user_operation, password_change).perform(&conn)?;
Ok(serde_json::to_string(&res)?)
do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
}
UserOperation::CreatePrivateMessage => {
chat.check_rate_limit_message(msg.id)?;
do_user_operation::<CreatePrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
}
UserOperation::EditPrivateMessage => {
do_user_operation::<EditPrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
}
UserOperation::GetPrivateMessages => {
do_user_operation::<GetPrivateMessages, PrivateMessagesResponse>(user_operation, data, &conn)
}
}
}

78
ui/assets/css/toastify.css vendored Normal file
View file

@ -0,0 +1,78 @@
/*!
* Toastify js 1.6.2
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/
.toastify {
padding: 12px 20px;
color: #ffffff;
display: inline-block;
box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3);
background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);
background: linear-gradient(135deg, #73a5ff, #5477f5);
position: fixed;
opacity: 0;
transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
border-radius: 2px;
cursor: pointer;
text-decoration: none;
max-width: calc(50% - 20px);
z-index: 2147483647;
}
.toastify.on {
opacity: 1;
}
.toast-close {
opacity: 0.4;
padding: 0 5px;
}
.toastify-right {
right: 15px;
}
.toastify-left {
left: 15px;
}
.toastify-top {
top: -150px;
}
.toastify-bottom {
bottom: -150px;
}
.toastify-rounded {
border-radius: 25px;
}
.toastify-avatar {
width: 1.5em;
height: 1.5em;
margin: 0 5px;
border-radius: 2px;
}
.toastify-center {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
}
@media only screen and (max-width: 360px) {
.toastify-right, .toastify-left {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
}
}

1
ui/package.json vendored
View file

@ -36,6 +36,7 @@
"prettier": "^1.18.2",
"rxjs": "^6.4.0",
"terser": "^4.6.0",
"toastify-js": "^1.6.2",
"tributejs": "^4.1.1",
"twemoji": "^12.1.2",
"ws": "^7.0.0"

View file

@ -10,12 +10,13 @@ import {
} from '../interfaces';
import { Subscription } from 'rxjs';
import {
wsJsonToRes,
capitalizeFirstLetter,
mentionDropdownFetchLimit,
msgOp,
mdToHtml,
randomStr,
markdownHelpUrl,
toast,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
@ -293,7 +294,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
alert(error);
toast(error, 'danger');
});
}
@ -311,10 +312,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
this.userSub = WebSocketService.Instance.subject.subscribe(
msg => {
let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) {
let res: SearchResponse = msg;
let users = res.users.map(u => {
let res = wsJsonToRes(msg);
if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
let users = data.users.map(u => {
return { key: u.name };
});
cb(users);
@ -343,10 +344,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
this.communitySub = WebSocketService.Instance.subject.subscribe(
msg => {
let op: UserOperation = msgOp(msg);
if (op == UserOperation.Search) {
let res: SearchResponse = msg;
let communities = res.communities.map(u => {
let res = wsJsonToRes(msg);
if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
let communities = data.communities.map(u => {
return { key: u.name };
});
cb(communities);

View file

@ -293,6 +293,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</li>
</>
)}
{!this.myComment && (
<li className="list-inline-item">
<Link
class="text-muted"
to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
>
{i18n.t('message').toLowerCase()}
</Link>
</li>
)}
<li className="list-inline-item"></li>
<li className="list-inline-item">
<span

View file

@ -10,9 +10,10 @@ import {
FollowCommunityForm,
ListCommunitiesForm,
SortType,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import { msgOp } from '../utils';
import { wsJsonToRes, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -231,15 +232,15 @@ export class Communities extends Component<any, CommunitiesState> {
WebSocketService.Instance.listCommunities(listCommunitiesForm);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg;
this.state.communities = res.communities;
} else if (res.op == UserOperation.ListCommunities) {
let data = res.data as ListCommunitiesResponse;
this.state.communities = data.communities;
this.state.communities.sort(
(a, b) => b.number_of_subscribers - a.number_of_subscribers
);
@ -248,11 +249,11 @@ export class Communities extends Component<any, CommunitiesState> {
this.setState(this.state);
let table = document.querySelector('#community_table');
Sortable.initTable(table);
} else if (op == UserOperation.FollowCommunity) {
let res: CommunityResponse = msg;
let found = this.state.communities.find(c => c.id == res.community.id);
found.subscribed = res.community.subscribed;
found.number_of_subscribers = res.community.number_of_subscribers;
} else if (res.op == UserOperation.FollowCommunity) {
let data = res.data as CommunityResponse;
let found = this.state.communities.find(c => c.id == data.community.id);
found.subscribed = data.community.subscribed;
found.number_of_subscribers = data.community.number_of_subscribers;
this.setState(this.state);
}
}

View file

@ -8,10 +8,11 @@ import {
ListCategoriesResponse,
CommunityResponse,
GetSiteResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import { msgOp, capitalizeFirstLetter } from '../utils';
import * as autosize from 'autosize';
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
import autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -67,14 +68,7 @@ export class CommunityForm extends Component<
}
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
@ -246,34 +240,34 @@ export class CommunityForm extends Component<
i.props.onCancel();
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
console.log(msg);
if (msg.error) {
alert(i18n.t(msg.error));
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
return;
} else if (op == UserOperation.ListCategories) {
let res: ListCategoriesResponse = msg;
this.state.categories = res.categories;
} else if (res.op == UserOperation.ListCategories) {
let data = res.data as ListCategoriesResponse;
this.state.categories = data.categories;
if (!this.props.community) {
this.state.communityForm.category_id = res.categories[0].id;
this.state.communityForm.category_id = data.categories[0].id;
}
this.setState(this.state);
} else if (op == UserOperation.CreateCommunity) {
let res: CommunityResponse = msg;
} else if (res.op == UserOperation.CreateCommunity) {
let data = res.data as CommunityResponse;
this.state.loading = false;
this.props.onCreate(res.community);
this.props.onCreate(data.community);
}
// TODO is ths necessary
else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
// TODO is this necessary
else if (res.op == UserOperation.EditCommunity) {
let data = res.data as CommunityResponse;
this.state.loading = false;
this.props.onEdit(res.community);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
this.state.enable_nsfw = res.site.enable_nsfw;
this.props.onEdit(data.community);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
}
}

View file

@ -14,16 +14,18 @@ import {
ListingType,
GetPostsResponse,
CreatePostLikeResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
import { SortSelect } from './sort-select';
import { Sidebar } from './sidebar';
import {
msgOp,
wsJsonToRes,
routeSortTypeToEnum,
fetchLimit,
postRefetchSeconds,
toast,
} from '../utils';
import { T } from 'inferno-i18next';
import { i18n } from '../i18next';
@ -253,43 +255,43 @@ export class Community extends Component<any, State> {
WebSocketService.Instance.getPosts(getPostsForm);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.context.router.history.push('/');
return;
} else if (op == UserOperation.GetCommunity) {
let res: GetCommunityResponse = msg;
this.state.community = res.community;
this.state.moderators = res.moderators;
this.state.admins = res.admins;
} else if (res.op == UserOperation.GetCommunity) {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.admins = data.admins;
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
this.setState(this.state);
this.keepFetchingPosts();
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
} else if (res.op == UserOperation.EditCommunity) {
let data = res.data as CommunityResponse;
this.state.community = data.community;
this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) {
let res: CommunityResponse = msg;
this.state.community.subscribed = res.community.subscribed;
} else if (res.op == UserOperation.FollowCommunity) {
let data = res.data as CommunityResponse;
this.state.community.subscribed = data.community.subscribed;
this.state.community.number_of_subscribers =
res.community.number_of_subscribers;
data.community.number_of_subscribers;
this.setState(this.state);
} else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg;
this.state.posts = res.posts;
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.state.posts = data.posts;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as CreatePostLikeResponse;
let found = this.state.posts.find(c => c.id == data.post.id);
found.my_vote = data.post.my_vote;
found.score = data.post.score;
found.upvotes = data.post.upvotes;
found.downvotes = data.post.downvotes;
this.setState(this.state);
}
}

View file

@ -0,0 +1,53 @@
import { Component } from 'inferno';
import { PrivateMessageForm } from './private-message-form';
import { WebSocketService } from '../services';
import { PrivateMessageFormParams } from '../interfaces';
import { toast } from '../utils';
import { i18n } from '../i18next';
export class CreatePrivateMessage extends Component<any, any> {
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 (
<div class="container">
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_private_message')}</h5>
<PrivateMessageForm
onCreate={this.handlePrivateMessageCreate}
params={this.params}
/>
</div>
</div>
</div>
);
}
get params(): PrivateMessageFormParams {
let urlParams = new URLSearchParams(this.props.location.search);
let params: PrivateMessageFormParams = {
recipient_id: Number(urlParams.get('recipient_id')),
};
return params;
}
handlePrivateMessageCreate() {
toast(i18n.t('message_sent'));
// Navigate to the front
this.props.history.push(`/`);
}
}

View file

@ -12,10 +12,16 @@ import {
GetUserMentionsResponse,
UserMentionResponse,
CommentResponse,
WebSocketJsonResponse,
PrivateMessage as PrivateMessageI,
GetPrivateMessagesForm,
PrivateMessagesResponse,
PrivateMessageResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, fetchLimit } from '../utils';
import { wsJsonToRes, fetchLimit, isCommentType, toast } 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 +32,10 @@ enum UnreadOrAll {
}
enum UnreadType {
Both,
All,
Replies,
Mentions,
Messages,
}
interface InboxState {
@ -36,6 +43,7 @@ interface InboxState {
unreadType: UnreadType;
replies: Array<Comment>;
mentions: Array<Comment>;
messages: Array<PrivateMessageI>;
sort: SortType;
page: number;
}
@ -44,9 +52,10 @@ export class Inbox extends Component<any, InboxState> {
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 +112,10 @@ export class Inbox extends Component<any, InboxState> {
</a>
</small>
</h5>
{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 && (
<ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item">
@ -114,9 +126,10 @@ export class Inbox extends Component<any, InboxState> {
</ul>
)}
{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()}
</div>
</div>
@ -150,8 +163,8 @@ export class Inbox extends Component<any, InboxState> {
<option disabled>
<T i18nKey="type">#</T>
</option>
<option value={UnreadType.Both}>
<T i18nKey="both">#</T>
<option value={UnreadType.All}>
<T i18nKey="all">#</T>
</option>
<option value={UnreadType.Replies}>
<T i18nKey="replies">#</T>
@ -159,6 +172,9 @@ export class Inbox extends Component<any, InboxState> {
<option value={UnreadType.Mentions}>
<T i18nKey="mentions">#</T>
</option>
<option value={UnreadType.Messages}>
<T i18nKey="messages">#</T>
</option>
</select>
<SortSelect
sort={this.state.sort}
@ -169,33 +185,25 @@ export class Inbox extends Component<any, InboxState> {
);
}
both() {
let combined: Array<{
type_: string;
data: Comment;
}> = [];
let replies = this.state.replies.map(e => {
return { type_: 'replies', data: e };
});
let mentions = this.state.mentions.map(e => {
return { type_: 'mentions', data: e };
});
all() {
let combined: Array<Comment | PrivateMessageI> = [];
combined.push(...replies);
combined.push(...mentions);
combined.push(...this.state.replies);
combined.push(...this.state.mentions);
combined.push(...this.state.messages);
// Sort it
if (this.state.sort == SortType.New) {
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
} else {
combined.sort((a, b) => b.data.score - a.data.score);
}
combined.sort((a, b) => b.published.localeCompare(a.published));
return (
<div>
{combined.map(i => (
<CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
))}
{combined.map(i =>
isCommentType(i) ? (
<CommentNodes nodes={[{ comment: i }]} noIndent markable />
) : (
<PrivateMessage privateMessage={i} />
)
)}
</div>
);
}
@ -220,6 +228,16 @@ export class Inbox extends Component<any, InboxState> {
);
}
messages() {
return (
<div>
{this.state.messages.map(message => (
<PrivateMessage privateMessage={message} />
))}
</div>
);
}
paginator() {
return (
<div class="mt-2">
@ -283,6 +301,13 @@ export class Inbox extends Component<any, InboxState> {
limit: fetchLimit,
};
WebSocketService.Instance.getUserMentions(userMentionsForm);
let privateMessagesForm: GetPrivateMessagesForm = {
unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
page: this.state.page,
limit: fetchLimit,
};
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
}
handleSortChange(val: SortType) {
@ -296,94 +321,122 @@ export class Inbox extends Component<any, InboxState> {
WebSocketService.Instance.markAllAsRead();
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg;
this.state.replies = res.replies;
} else if (res.op == UserOperation.GetReplies) {
let data = res.data as GetRepliesResponse;
this.state.replies = data.replies;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.GetUserMentions) {
let res: GetUserMentionsResponse = msg;
this.state.mentions = res.mentions;
} else if (res.op == UserOperation.GetUserMentions) {
let data = res.data as GetUserMentionsResponse;
this.state.mentions = data.mentions;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.MarkAllAsRead) {
} else if (res.op == UserOperation.GetPrivateMessages) {
let data = res.data as PrivateMessagesResponse;
this.state.messages = data.messages;
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (res.op == UserOperation.EditPrivateMessage) {
let data = res.data as PrivateMessageResponse;
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;
}
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (res.op == UserOperation.MarkAllAsRead) {
this.state.replies = [];
this.state.mentions = [];
this.state.messages = [];
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
let found = this.state.replies.find(c => c.id == res.comment.id);
found.content = res.comment.content;
found.updated = res.comment.updated;
found.removed = res.comment.removed;
found.deleted = res.comment.deleted;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
found.score = res.comment.score;
let found = this.state.replies.find(c => c.id == data.comment.id);
found.content = data.comment.content;
found.updated = data.comment.updated;
found.removed = data.comment.removed;
found.deleted = data.comment.deleted;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
found.score = data.comment.score;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
this.state.replies = this.state.replies.filter(
r => r.id !== res.comment.id
r => r.id !== data.comment.id
);
} else {
let found = this.state.replies.find(c => c.id == res.comment.id);
found.read = res.comment.read;
let found = this.state.replies.find(c => c.id == data.comment.id);
found.read = data.comment.read;
}
this.sendUnreadCount();
this.setState(this.state);
} else if (op == UserOperation.EditUserMention) {
let res: UserMentionResponse = msg;
} else if (res.op == UserOperation.EditUserMention) {
let data = res.data as UserMentionResponse;
let found = this.state.mentions.find(c => c.id == res.mention.id);
found.content = res.mention.content;
found.updated = res.mention.updated;
found.removed = res.mention.removed;
found.deleted = res.mention.deleted;
found.upvotes = res.mention.upvotes;
found.downvotes = res.mention.downvotes;
found.score = res.mention.score;
let found = this.state.mentions.find(c => c.id == data.mention.id);
found.content = data.mention.content;
found.updated = data.mention.updated;
found.removed = data.mention.removed;
found.deleted = data.mention.deleted;
found.upvotes = data.mention.upvotes;
found.downvotes = data.mention.downvotes;
found.score = data.mention.score;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
this.state.mentions = this.state.mentions.filter(
r => r.id !== res.mention.id
r => r.id !== data.mention.id
);
} else {
let found = this.state.mentions.find(c => c.id == res.mention.id);
found.read = res.mention.read;
let found = this.state.mentions.find(c => c.id == data.mention.id);
found.read = data.mention.read;
}
this.sendUnreadCount();
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
} else if (res.op == UserOperation.CreateComment) {
// let res: CommentResponse = msg;
alert(i18n.t('reply_sent'));
toast(i18n.t('reply_sent'));
// this.state.replies.unshift(res.comment); // TODO do this right
// this.setState(this.state);
} else if (op == UserOperation.SaveComment) {
let res: CommentResponse = msg;
let found = this.state.replies.find(c => c.id == res.comment.id);
found.saved = res.comment.saved;
} else if (res.op == UserOperation.SaveComment) {
let data = res.data as CommentResponse;
let found = this.state.replies.find(c => c.id == data.comment.id);
found.saved = data.comment.saved;
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
let found: Comment = this.state.replies.find(
c => c.id === res.comment.id
c => c.id === data.comment.id
);
found.score = res.comment.score;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
found.score = data.comment.score;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
this.setState(this.state);
}
}
@ -391,7 +444,10 @@ export class Inbox extends Component<any, InboxState> {
sendUnreadCount() {
let count =
this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length;
this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter(
r => !r.read && r.creator_id !== UserService.Instance.user.id
).length;
UserService.Instance.sub.next({
user: UserService.Instance.user,
unreadCount: count,

View file

@ -8,9 +8,10 @@ import {
UserOperation,
PasswordResetForm,
GetSiteResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, validEmail } from '../utils';
import { wsJsonToRes, validEmail, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -48,14 +49,7 @@ export class Login extends Component<any, State> {
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
@ -299,31 +293,32 @@ export class Login extends Component<any, State> {
WebSocketService.Instance.passwordReset(resetForm);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state = this.emptyState;
this.setState(this.state);
return;
} else {
if (op == UserOperation.Login) {
if (res.op == UserOperation.Login) {
let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
UserService.Instance.login(data);
toast(i18n.t('logged_in'));
this.props.history.push('/');
} else if (op == UserOperation.Register) {
} else if (res.op == UserOperation.Register) {
let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
UserService.Instance.login(data);
this.props.history.push('/communities');
} else if (op == UserOperation.PasswordReset) {
alert(i18n.t('reset_password_mail_sent'));
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
this.state.enable_nsfw = res.site.enable_nsfw;
} else if (res.op == UserOperation.PasswordReset) {
toast(i18n.t('reset_password_mail_sent'));
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
document.title = `${i18n.t('login')} - ${
WebSocketService.Instance.site.name

View file

@ -17,6 +17,7 @@ import {
CreatePostLikeResponse,
Post,
GetPostsForm,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
@ -24,7 +25,7 @@ import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select';
import { SiteForm } from './site-form';
import {
msgOp,
wsJsonToRes,
repoUrl,
mdToHtml,
fetchLimit,
@ -33,6 +34,7 @@ import {
postRefetchSeconds,
pictshareAvatarThumbnail,
showAvatars,
toast,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -56,7 +58,6 @@ export class Main extends Component<any, MainState> {
subscribedCommunities: [],
trendingCommunities: [],
site: {
op: null,
site: {
id: null,
name: null,
@ -562,50 +563,50 @@ export class Main extends Component<any, MainState> {
WebSocketService.Instance.getPosts(getPostsForm);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.GetFollowedCommunities) {
let res: GetFollowedCommunitiesResponse = msg;
this.state.subscribedCommunities = res.communities;
} else if (res.op == UserOperation.GetFollowedCommunities) {
let data = res.data as GetFollowedCommunitiesResponse;
this.state.subscribedCommunities = data.communities;
this.setState(this.state);
} else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg;
this.state.trendingCommunities = res.communities;
} else if (res.op == UserOperation.ListCommunities) {
let data = res.data as ListCommunitiesResponse;
this.state.trendingCommunities = data.communities;
this.setState(this.state);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
// This means it hasn't been set up yet
if (!res.site) {
if (!data.site) {
this.context.router.history.push('/setup');
}
this.state.site.admins = res.admins;
this.state.site.site = res.site;
this.state.site.banned = res.banned;
this.state.site.online = res.online;
this.state.site.admins = data.admins;
this.state.site.site = data.site;
this.state.site.banned = data.banned;
this.state.site.online = data.online;
this.setState(this.state);
document.title = `${WebSocketService.Instance.site.name}`;
} else if (op == UserOperation.EditSite) {
let res: SiteResponse = msg;
this.state.site.site = res.site;
} else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse;
this.state.site.site = data.site;
this.state.showEditSite = false;
this.setState(this.state);
} else if (op == UserOperation.GetPosts) {
let res: GetPostsResponse = msg;
this.state.posts = res.posts;
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.state.posts = data.posts;
this.state.loading = false;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as CreatePostLikeResponse;
let found = this.state.posts.find(c => c.id == data.post.id);
found.my_vote = data.post.my_vote;
found.score = data.post.score;
found.upvotes = data.post.upvotes;
found.downvotes = data.post.downvotes;
this.setState(this.state);
}
}

View file

@ -17,9 +17,9 @@ import {
ModAdd,
} from '../interfaces';
import { WebSocketService } from '../services';
import { msgOp, addTypeInfo, fetchLimit } from '../utils';
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
import { MomentTime } from './moment-time';
import * as moment from 'moment';
import moment from 'moment';
import { i18n } from '../i18next';
interface ModlogState {
@ -55,14 +55,7 @@ export class Modlog extends Component<any, ModlogState> {
? Number(this.props.match.params.community_id)
: undefined;
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
@ -429,17 +422,17 @@ export class Modlog extends Component<any, ModlogState> {
WebSocketService.Instance.getModlog(modlogForm);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.GetModlog) {
let res: GetModlogResponse = msg;
} else if (res.op == UserOperation.GetModlog) {
let data = res.data as GetModlogResponse;
this.state.loading = false;
window.scrollTo(0, 0);
this.setCombined(res);
this.setCombined(data);
}
}
}

View file

@ -1,5 +1,5 @@
import { Component } from 'inferno';
import * as moment from 'moment';
import moment from 'moment';
import { getMomentLanguage } from '../utils';
import { i18n } from '../i18next';

View file

@ -9,15 +9,21 @@ import {
GetRepliesResponse,
GetUserMentionsForm,
GetUserMentionsResponse,
GetPrivateMessagesForm,
PrivateMessagesResponse,
SortType,
GetSiteResponse,
Comment,
PrivateMessage,
WebSocketJsonResponse,
} from '../interfaces';
import {
msgOp,
wsJsonToRes,
pictshareAvatarThumbnail,
showAvatars,
fetchLimit,
isCommentType,
toast,
} from '../utils';
import { version } from '../version';
import { i18n } from '../i18next';
@ -28,6 +34,7 @@ interface NavbarState {
expanded: boolean;
replies: Array<Comment>;
mentions: Array<Comment>;
messages: Array<PrivateMessage>;
fetchCount: number;
unreadCount: number;
siteName: string;
@ -42,6 +49,7 @@ export class Navbar extends Component<any, NavbarState> {
fetchCount: 0,
replies: [],
mentions: [],
messages: [],
expanded: false,
siteName: undefined,
};
@ -192,17 +200,17 @@ export class Navbar extends Component<any, NavbarState> {
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
if (msg.error == 'not_logged_in') {
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.error) {
if (res.error == 'not_logged_in') {
UserService.Instance.logout();
location.reload();
}
return;
} else if (op == UserOperation.GetReplies) {
let res: GetRepliesResponse = msg;
let unreadReplies = res.replies.filter(r => !r.read);
} else if (res.op == UserOperation.GetReplies) {
let data = res.data as GetRepliesResponse;
let unreadReplies = data.replies.filter(r => !r.read);
if (
unreadReplies.length > 0 &&
this.state.fetchCount > 1 &&
@ -214,9 +222,9 @@ export class Navbar extends Component<any, NavbarState> {
this.state.replies = unreadReplies;
this.setState(this.state);
this.sendUnreadCount();
} else if (op == UserOperation.GetUserMentions) {
let res: GetUserMentionsResponse = msg;
let unreadMentions = res.mentions.filter(r => !r.read);
} else if (res.op == UserOperation.GetUserMentions) {
let data = res.data as GetUserMentionsResponse;
let unreadMentions = data.mentions.filter(r => !r.read);
if (
unreadMentions.length > 0 &&
this.state.fetchCount > 1 &&
@ -228,12 +236,26 @@ export class Navbar extends Component<any, NavbarState> {
this.state.mentions = unreadMentions;
this.setState(this.state);
this.sendUnreadCount();
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
} else if (res.op == UserOperation.GetPrivateMessages) {
let data = res.data as PrivateMessagesResponse;
let unreadMessages = data.messages.filter(r => !r.read);
if (
unreadMessages.length > 0 &&
this.state.fetchCount > 1 &&
JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
) {
this.notify(unreadMessages);
}
if (res.site) {
this.state.siteName = res.site.name;
WebSocketService.Instance.site = res.site;
this.state.messages = unreadMessages;
this.setState(this.state);
this.sendUnreadCount();
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
if (data.site) {
this.state.siteName = data.site.name;
WebSocketService.Instance.site = data.site;
this.setState(this.state);
}
}
@ -259,9 +281,17 @@ export class Navbar extends Component<any, NavbarState> {
page: 1,
limit: fetchLimit,
};
let privateMessagesForm: GetPrivateMessagesForm = {
unread_only: true,
page: 1,
limit: fetchLimit,
};
if (this.currentLocation !== '/inbox') {
WebSocketService.Instance.getReplies(repliesForm);
WebSocketService.Instance.getUserMentions(userMentionsForm);
WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
this.state.fetchCount++;
}
}
@ -281,7 +311,8 @@ export class Navbar extends Component<any, NavbarState> {
get unreadCount() {
return (
this.state.replies.filter(r => !r.read).length +
this.state.mentions.filter(r => !r.read).length
this.state.mentions.filter(r => !r.read).length +
this.state.messages.filter(r => !r.read).length
);
}
@ -289,7 +320,7 @@ export class Navbar extends Component<any, NavbarState> {
if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() {
if (!Notification) {
alert(i18n.t('notifications_error'));
toast(i18n.t('notifications_error'), 'danger');
return;
}
@ -299,21 +330,25 @@ export class Navbar extends Component<any, NavbarState> {
}
}
notify(replies: Array<Comment>) {
notify(replies: Array<Comment | PrivateMessage>) {
let recentReply = replies[0];
if (Notification.permission !== 'granted') Notification.requestPermission();
else {
var notification = new Notification(
`${replies.length} ${i18n.t('unread_messages')}`,
{
icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
icon: recentReply.creator_avatar
? recentReply.creator_avatar
: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
body: `${recentReply.creator_name}: ${recentReply.content}`,
}
);
notification.onclick = () => {
this.context.router.history.push(
`/post/${recentReply.post_id}/comment/${recentReply.id}`
isCommentType(recentReply)
? `/post/${recentReply.post_id}/comment/${recentReply.id}`
: `/inbox`
);
};
}

View file

@ -5,9 +5,10 @@ import {
UserOperation,
LoginResponse,
PasswordChangeForm,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, capitalizeFirstLetter } from '../utils';
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -34,14 +35,7 @@ export class PasswordChange extends Component<any, State> {
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
@ -140,19 +134,19 @@ export class PasswordChange extends Component<any, State> {
WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (msg.error) {
alert(i18n.t(msg.error));
toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
return;
} else {
if (op == UserOperation.PasswordChange) {
if (res.op == UserOperation.PasswordChange) {
let data = res.data as LoginResponse;
this.state = this.emptyState;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
UserService.Instance.login(data);
this.props.history.push('/');
}
}

View file

@ -16,10 +16,11 @@ import {
SearchType,
SearchResponse,
GetSiteResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
msgOp,
wsJsonToRes,
getPageTitle,
validURL,
capitalizeFirstLetter,
@ -28,6 +29,7 @@ import {
mdToHtml,
debounce,
isImage,
toast,
} from '../utils';
import autosize from 'autosize';
import { i18n } from '../i18next';
@ -453,51 +455,51 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
alert(error);
toast(error, 'danger');
});
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
return;
} else if (op == UserOperation.ListCommunities) {
let res: ListCommunitiesResponse = msg;
this.state.communities = res.communities;
} else if (res.op == UserOperation.ListCommunities) {
let data = res.data as ListCommunitiesResponse;
this.state.communities = data.communities;
if (this.props.post) {
this.state.postForm.community_id = this.props.post.community_id;
} else if (this.props.params && this.props.params.community) {
let foundCommunityId = res.communities.find(
let foundCommunityId = data.communities.find(
r => r.name == this.props.params.community
).id;
this.state.postForm.community_id = foundCommunityId;
} else {
this.state.postForm.community_id = res.communities[0].id;
this.state.postForm.community_id = data.communities[0].id;
}
this.setState(this.state);
} else if (op == UserOperation.CreatePost) {
} else if (res.op == UserOperation.CreatePost) {
let data = res.data as PostResponse;
this.state.loading = false;
let res: PostResponse = msg;
this.props.onCreate(res.post.id);
} else if (op == UserOperation.EditPost) {
this.props.onCreate(data.post.id);
} else if (res.op == UserOperation.EditPost) {
let data = res.data as PostResponse;
this.state.loading = false;
let res: PostResponse = msg;
this.props.onEdit(res.post);
} else if (op == UserOperation.Search) {
let res: SearchResponse = msg;
this.props.onEdit(data.post);
} else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
if (res.type_ == SearchType[SearchType.Posts]) {
this.state.suggestedPosts = res.posts;
} else if (res.type_ == SearchType[SearchType.Url]) {
this.state.crossPosts = res.posts;
if (data.type_ == SearchType[SearchType.Posts]) {
this.state.suggestedPosts = data.posts;
} else if (data.type_ == SearchType[SearchType.Url]) {
this.state.crossPosts = data.posts;
}
this.setState(this.state);
} else if (op == UserOperation.GetSite) {
let res: GetSiteResponse = msg;
this.state.enable_nsfw = res.site.enable_nsfw;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
}
}

View file

@ -26,9 +26,10 @@ import {
SearchResponse,
GetSiteResponse,
GetCommunityResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, hotRank } from '../utils';
import { wsJsonToRes, hotRank, toast } from '../utils';
import { PostListing } from './post-listing';
import { PostListings } from './post-listings';
import { Sidebar } from './sidebar';
@ -341,19 +342,19 @@ export class Post extends Component<any, PostState> {
);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.GetPost) {
let res: GetPostResponse = msg;
this.state.post = res.post;
this.state.comments = res.comments;
this.state.community = res.community;
this.state.moderators = res.moderators;
this.state.admins = res.admins;
} else if (res.op == UserOperation.GetPost) {
let data = res.data as GetPostResponse;
this.state.post = data.post;
this.state.comments = data.comments;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.loading = false;
document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
@ -370,111 +371,111 @@ export class Post extends Component<any, PostState> {
}
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
let res: CommentResponse = msg;
this.state.comments.unshift(res.comment);
} else if (res.op == UserOperation.CreateComment) {
let data = res.data as CommentResponse;
this.state.comments.unshift(data.comment);
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
let res: CommentResponse = msg;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.content = res.comment.content;
found.updated = res.comment.updated;
found.removed = res.comment.removed;
found.deleted = res.comment.deleted;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
found.score = res.comment.score;
found.read = res.comment.read;
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
let found = this.state.comments.find(c => c.id == data.comment.id);
found.content = data.comment.content;
found.updated = data.comment.updated;
found.removed = data.comment.removed;
found.deleted = data.comment.deleted;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
found.score = data.comment.score;
found.read = data.comment.read;
this.setState(this.state);
} else if (op == UserOperation.SaveComment) {
let res: CommentResponse = msg;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.saved = res.comment.saved;
} else if (res.op == UserOperation.SaveComment) {
let data = res.data as CommentResponse;
let found = this.state.comments.find(c => c.id == data.comment.id);
found.saved = data.comment.saved;
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
let found: Comment = this.state.comments.find(
c => c.id === res.comment.id
c => c.id === data.comment.id
);
found.score = res.comment.score;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) {
found.my_vote = res.comment.my_vote;
found.score = data.comment.score;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
if (data.comment.my_vote !== null) {
found.my_vote = data.comment.my_vote;
found.upvoteLoading = false;
found.downvoteLoading = false;
}
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
this.state.post.my_vote = res.post.my_vote;
this.state.post.score = res.post.score;
this.state.post.upvotes = res.post.upvotes;
this.state.post.downvotes = res.post.downvotes;
this.state.post.upvoteLoading = false;
this.state.post.downvoteLoading = false;
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as CreatePostLikeResponse;
this.state.post.my_vote = data.post.my_vote;
this.state.post.score = data.post.score;
this.state.post.upvotes = data.post.upvotes;
this.state.post.downvotes = data.post.downvotes;
this.setState(this.state);
} else if (op == UserOperation.EditPost) {
let res: PostResponse = msg;
this.state.post = res.post;
} else if (res.op == UserOperation.EditPost) {
let data = res.data as PostResponse;
this.state.post = data.post;
this.setState(this.state);
} else if (op == UserOperation.SavePost) {
let res: PostResponse = msg;
this.state.post = res.post;
} else if (res.op == UserOperation.SavePost) {
let data = res.data as PostResponse;
this.state.post = data.post;
this.setState(this.state);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
this.state.post.community_id = res.community.id;
this.state.post.community_name = res.community.name;
} else if (res.op == UserOperation.EditCommunity) {
let data = res.data as CommunityResponse;
this.state.community = data.community;
this.state.post.community_id = data.community.id;
this.state.post.community_name = data.community.name;
this.setState(this.state);
} else if (op == UserOperation.FollowCommunity) {
let res: CommunityResponse = msg;
this.state.community.subscribed = res.community.subscribed;
} else if (res.op == UserOperation.FollowCommunity) {
let data = res.data as CommunityResponse;
this.state.community.subscribed = data.community.subscribed;
this.state.community.number_of_subscribers =
res.community.number_of_subscribers;
data.community.number_of_subscribers;
this.setState(this.state);
} else if (op == UserOperation.BanFromCommunity) {
let res: BanFromCommunityResponse = msg;
} else if (res.op == UserOperation.BanFromCommunity) {
let data = res.data as BanFromCommunityResponse;
this.state.comments
.filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned_from_community = res.banned));
if (this.state.post.creator_id == res.user.id) {
this.state.post.banned_from_community = res.banned;
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned_from_community = data.banned));
if (this.state.post.creator_id == data.user.id) {
this.state.post.banned_from_community = data.banned;
}
this.setState(this.state);
} else if (op == UserOperation.AddModToCommunity) {
let res: AddModToCommunityResponse = msg;
this.state.moderators = res.moderators;
} else if (res.op == UserOperation.AddModToCommunity) {
let data = res.data as AddModToCommunityResponse;
this.state.moderators = data.moderators;
this.setState(this.state);
} else if (op == UserOperation.BanUser) {
let res: BanUserResponse = msg;
} else if (res.op == UserOperation.BanUser) {
let data = res.data as BanUserResponse;
this.state.comments
.filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned = res.banned));
if (this.state.post.creator_id == res.user.id) {
this.state.post.banned = res.banned;
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
if (this.state.post.creator_id == data.user.id) {
this.state.post.banned = data.banned;
}
this.setState(this.state);
} else if (op == UserOperation.AddAdmin) {
let res: AddAdminResponse = msg;
this.state.admins = res.admins;
} else if (res.op == UserOperation.AddAdmin) {
let data = res.data as AddAdminResponse;
this.state.admins = data.admins;
this.setState(this.state);
} else if (op == UserOperation.Search) {
let res: SearchResponse = msg;
this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
} else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
this.state.crossPosts = data.posts.filter(
p => p.id != this.state.post.id
);
this.setState(this.state);
} else if (op == UserOperation.TransferSite) {
let res: GetSiteResponse = msg;
} else if (res.op == UserOperation.TransferSite) {
let data = res.data as GetSiteResponse;
this.state.admins = res.admins;
this.state.admins = data.admins;
this.setState(this.state);
} else if (op == UserOperation.TransferCommunity) {
let res: GetCommunityResponse = msg;
this.state.community = res.community;
this.state.moderators = res.moderators;
this.state.admins = res.admins;
} else if (res.op == UserOperation.TransferCommunity) {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.setState(this.state);
}
}

View file

@ -0,0 +1,293 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
PrivateMessageForm as PrivateMessageFormI,
EditPrivateMessageForm,
PrivateMessageFormParams,
PrivateMessage,
PrivateMessageResponse,
UserView,
UserOperation,
UserDetailsResponse,
GetUserDetailsForm,
SortType,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import {
capitalizeFirstLetter,
markdownHelpUrl,
mdToHtml,
showAvatars,
pictshareAvatarThumbnail,
wsJsonToRes,
toast,
} from '../utils';
import autosize from 'autosize';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface PrivateMessageFormProps {
privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
params?: PrivateMessageFormParams;
onCancel?(): any;
onCreate?(message: PrivateMessage): any;
onEdit?(message: PrivateMessage): any;
}
interface PrivateMessageFormState {
privateMessageForm: PrivateMessageFormI;
recipient: UserView;
loading: boolean;
previewMode: boolean;
showDisclaimer: boolean;
}
export class PrivateMessageForm extends Component<
PrivateMessageFormProps,
PrivateMessageFormState
> {
private subscription: Subscription;
private emptyState: PrivateMessageFormState = {
privateMessageForm: {
content: null,
recipient_id: null,
},
recipient: null,
loading: false,
previewMode: false,
showDisclaimer: false,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
if (this.props.privateMessage) {
this.state.privateMessageForm = {
content: this.props.privateMessage.content,
recipient_id: this.props.privateMessage.recipient_id,
};
}
if (this.props.params) {
this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
let form: GetUserDetailsForm = {
user_id: this.state.privateMessageForm.recipient_id,
sort: SortType[SortType.New],
saved_only: false,
};
WebSocketService.Instance.getUserDetails(form);
}
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
}
componentDidMount() {
autosize(document.querySelectorAll('textarea'));
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return (
<div>
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
{!this.props.privateMessage && (
<div class="form-group row">
<label class="col-sm-2 col-form-label">
{capitalizeFirstLetter(i18n.t('to'))}
</label>
{this.state.recipient && (
<div class="col-sm-10 form-control-plaintext">
<Link
className="text-info"
to={`/u/${this.state.recipient.name}`}
>
{this.state.recipient.avatar && showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
this.state.recipient.avatar
)}
class="rounded-circle mr-1"
/>
)}
<span>{this.state.recipient.name}</span>
</Link>
</div>
)}
</div>
)}
<div class="form-group row">
<label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
<div class="col-sm-10">
<textarea
value={this.state.privateMessageForm.content}
onInput={linkEvent(this, this.handleContentChange)}
className={`form-control ${this.state.previewMode && 'd-none'}`}
rows={4}
maxLength={10000}
/>
{this.state.previewMode && (
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.privateMessageForm.content
)}
/>
)}
{this.state.privateMessageForm.content && (
<button
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
.previewMode && 'active'}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
<ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
<li class="list-inline-item">
<span
onClick={linkEvent(this, this.handleShowDisclaimer)}
class="pointer"
>
{i18n.t('disclaimer')}
</span>
</li>
<li class="list-inline-item">
<a href={markdownHelpUrl} target="_blank" class="text-muted">
{i18n.t('formatting_help')}
</a>
</li>
</ul>
</div>
</div>
{this.state.showDisclaimer && (
<div class="form-group row">
<div class="col-sm-10">
<div class="alert alert-danger" role="alert">
<T i18nKey="private_message_disclaimer">
#
<a
class="alert-link"
target="_blank"
href="https://about.riot.im/"
>
#
</a>
</T>
</div>
</div>
</div>
)}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2">
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : this.props.privateMessage ? (
capitalizeFirstLetter(i18n.t('save'))
) : (
capitalizeFirstLetter(i18n.t('send_message'))
)}
</button>
{this.props.privateMessage && (
<button
type="button"
class="btn btn-secondary"
onClick={linkEvent(this, this.handleCancel)}
>
{i18n.t('cancel')}
</button>
)}
</div>
</div>
</form>
</div>
);
}
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
event.preventDefault();
if (i.props.privateMessage) {
let editForm: EditPrivateMessageForm = {
edit_id: i.props.privateMessage.id,
content: i.state.privateMessageForm.content,
};
WebSocketService.Instance.editPrivateMessage(editForm);
} else {
WebSocketService.Instance.createPrivateMessage(
i.state.privateMessageForm
);
}
i.state.loading = true;
i.setState(i.state);
}
handleRecipientChange(i: PrivateMessageForm, event: any) {
i.state.recipient = event.target.value;
i.setState(i.state);
}
handleContentChange(i: PrivateMessageForm, event: any) {
i.state.privateMessageForm.content = event.target.value;
i.setState(i.state);
}
handleCancel(i: PrivateMessageForm) {
i.props.onCancel();
}
handlePreviewToggle(i: PrivateMessageForm, event: any) {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
handleShowDisclaimer(i: PrivateMessageForm) {
i.state.showDisclaimer = !i.state.showDisclaimer;
i.setState(i.state);
}
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state.loading = false;
this.setState(this.state);
return;
} else if (res.op == UserOperation.EditPrivateMessage) {
let data = res.data as PrivateMessageResponse;
this.state.loading = false;
this.props.onEdit(data.message);
} else if (res.op == UserOperation.GetUserDetails) {
let data = res.data as UserDetailsResponse;
this.state.recipient = data.user;
this.state.privateMessageForm.recipient_id = data.user.id;
this.setState(this.state);
} else if (res.op == UserOperation.CreatePrivateMessage) {
let data = res.data as PrivateMessageResponse;
this.state.loading = false;
this.props.onCreate(data.message);
this.setState(this.state);
}
}
}

254
ui/src/components/private-message.tsx vendored Normal file
View file

@ -0,0 +1,254 @@
import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import {
PrivateMessage as PrivateMessageI,
EditPrivateMessageForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
mdToHtml,
pictshareAvatarThumbnail,
showAvatars,
toast,
} from '../utils';
import { MomentTime } from './moment-time';
import { PrivateMessageForm } from './private-message-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface PrivateMessageState {
showReply: boolean;
showEdit: boolean;
collapsed: boolean;
viewSource: boolean;
}
interface PrivateMessageProps {
privateMessage: PrivateMessageI;
}
export class PrivateMessage extends Component<
PrivateMessageProps,
PrivateMessageState
> {
private emptyState: PrivateMessageState = {
showReply: false,
showEdit: false,
collapsed: false,
viewSource: false,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
this
);
this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
}
get mine(): boolean {
return UserService.Instance.user.id == this.props.privateMessage.creator_id;
}
render() {
let message = this.props.privateMessage;
return (
<div class="mb-2">
<div>
<ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item">
{this.mine ? i18n.t('to') : i18n.t('from')}
</li>
<li className="list-inline-item">
<Link
className="text-info"
to={
this.mine
? `/u/${message.recipient_name}`
: `/u/${message.creator_name}`
}
>
{(this.mine
? message.recipient_avatar
: message.creator_avatar) &&
showAvatars() && (
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
this.mine
? message.recipient_avatar
: message.creator_avatar
)}
class="rounded-circle mr-1"
/>
)}
<span>
{this.mine ? message.recipient_name : message.creator_name}
</span>
</Link>
</li>
<li className="list-inline-item">
<span>
<MomentTime data={message} />
</span>
</li>
<li className="list-inline-item">
<div
className="pointer text-monospace"
onClick={linkEvent(this, this.handleMessageCollapse)}
>
{this.state.collapsed ? '[+]' : '[-]'}
</div>
</li>
</ul>
{this.state.showEdit && (
<PrivateMessageForm
privateMessage={message}
onEdit={this.handlePrivateMessageEdit}
onCancel={this.handleReplyCancel}
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
<div>
{this.state.viewSource ? (
<pre>{this.messageUnlessRemoved}</pre>
) : (
<div
className="md-div"
dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
/>
)}
<ul class="list-inline mb-1 text-muted small font-weight-bold">
{!this.mine && (
<>
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleMarkRead)}
>
{message.read
? i18n.t('mark_as_unread')
: i18n.t('mark_as_read')}
</span>
</li>
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleReplyClick)}
>
<T i18nKey="reply">#</T>
</span>
</li>
</>
)}
{this.mine && (
<>
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleEditClick)}
>
<T i18nKey="edit">#</T>
</span>
</li>
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleDeleteClick)}
>
{!message.deleted
? i18n.t('delete')
: i18n.t('restore')}
</span>
</li>
</>
)}
<li className="list-inline-item"></li>
<li className="list-inline-item">
<span
className="pointer"
onClick={linkEvent(this, this.handleViewSource)}
>
<T i18nKey="view_source">#</T>
</span>
</li>
</ul>
</div>
)}
</div>
{this.state.showReply && (
<PrivateMessageForm
params={{
recipient_id: this.props.privateMessage.creator_id,
}}
onCreate={this.handlePrivateMessageCreate}
/>
)}
{/* A collapsed clearfix */}
{this.state.collapsed && <div class="row col-12"></div>}
</div>
);
}
get messageUnlessRemoved(): string {
let message = this.props.privateMessage;
return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
}
handleReplyClick(i: PrivateMessage) {
i.state.showReply = true;
i.setState(i.state);
}
handleEditClick(i: PrivateMessage) {
i.state.showEdit = true;
i.setState(i.state);
}
handleDeleteClick(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
edit_id: i.props.privateMessage.id,
deleted: !i.props.privateMessage.deleted,
};
WebSocketService.Instance.editPrivateMessage(form);
}
handleReplyCancel() {
this.state.showReply = false;
this.state.showEdit = false;
this.setState(this.state);
}
handleMarkRead(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
edit_id: i.props.privateMessage.id,
read: !i.props.privateMessage.read,
};
WebSocketService.Instance.editPrivateMessage(form);
}
handleMessageCollapse(i: PrivateMessage) {
i.state.collapsed = !i.state.collapsed;
i.setState(i.state);
}
handleViewSource(i: PrivateMessage) {
i.state.viewSource = !i.state.viewSource;
i.setState(i.state);
}
handlePrivateMessageEdit() {
this.state.showEdit = false;
this.setState(this.state);
}
handlePrivateMessageCreate() {
this.state.showReply = false;
this.setState(this.state);
toast(i18n.t('message_sent'));
}
}

View file

@ -14,15 +14,17 @@ import {
SearchType,
CreatePostLikeResponse,
CommentResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService } from '../services';
import {
msgOp,
wsJsonToRes,
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
pictshareAvatarThumbnail,
showAvatars,
toast,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
@ -47,7 +49,6 @@ export class Search extends Component<any, SearchState> {
sort: this.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props),
searchResponse: {
op: null,
type_: null,
posts: [],
comments: [],
@ -400,7 +401,6 @@ export class Search extends Component<any, SearchState> {
return (
<div>
{res &&
res.op &&
res.posts.length == 0 &&
res.comments.length == 0 &&
res.communities.length == 0 &&
@ -476,44 +476,44 @@ export class Search extends Component<any, SearchState> {
);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (op == UserOperation.Search) {
let res: SearchResponse = msg;
this.state.searchResponse = res;
} else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse;
this.state.searchResponse = data;
this.state.loading = false;
document.title = `${i18n.t('search')} - ${this.state.q} - ${
WebSocketService.Instance.site.name
}`;
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
let found: Comment = this.state.searchResponse.comments.find(
c => c.id === res.comment.id
c => c.id === data.comment.id
);
found.score = res.comment.score;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) {
found.my_vote = res.comment.my_vote;
found.score = data.comment.score;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
if (data.comment.my_vote !== null) {
found.my_vote = data.comment.my_vote;
found.upvoteLoading = false;
found.downvoteLoading = false;
}
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as CreatePostLikeResponse;
let found = this.state.searchResponse.posts.find(
c => c.id == res.post.id
c => c.id == data.post.id
);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
found.my_vote = data.post.my_vote;
found.score = data.post.score;
found.upvotes = data.post.upvotes;
found.downvotes = data.post.downvotes;
this.setState(this.state);
}
}

View file

@ -1,9 +1,14 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
import {
RegisterForm,
LoginResponse,
UserOperation,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { wsJsonToRes, toast } from '../utils';
import { SiteForm } from './site-form';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -35,14 +40,7 @@ export class Setup extends Component<any, State> {
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
@ -188,21 +186,20 @@ export class Setup extends Component<any, State> {
i.setState(i.state);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state.userLoading = false;
this.setState(this.state);
return;
} else if (op == UserOperation.Register) {
} else if (res.op == UserOperation.Register) {
let data = res.data as LoginResponse;
this.state.userLoading = false;
this.state.doneRegisteringUser = true;
let res: LoginResponse = msg;
UserService.Instance.login(res);
console.log(res);
UserService.Instance.login(data);
this.setState(this.state);
} else if (op == UserOperation.CreateSite) {
} else if (res.op == UserOperation.CreateSite) {
this.props.history.push('/');
}
}

View file

@ -19,10 +19,11 @@ import {
AddAdminResponse,
DeleteAccountForm,
CreatePostLikeResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
msgOp,
wsJsonToRes,
fetchLimit,
routeSortTypeToEnum,
capitalizeFirstLetter,
@ -30,6 +31,7 @@ import {
setTheme,
languages,
showAvatars,
toast,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
@ -405,13 +407,30 @@ export class User extends Component<any, UserState> {
</tr>
</table>
</div>
{this.isCurrentUser && (
{this.isCurrentUser ? (
<button
class="btn btn-block btn-secondary mt-3"
onClick={linkEvent(this, this.handleLogoutClick)}
>
<T i18nKey="logout">#</T>
</button>
) : (
<>
<a
className={`btn btn-block btn-secondary mt-3 ${!this.state
.user.matrix_user_id && 'disabled'}`}
target="_blank"
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
>
{i18n.t('send_secure_message')}
</a>
<Link
class="btn btn-block btn-secondary mt-3"
to={`/create_private_message?recipient_id=${this.state.user.id}`}
>
{i18n.t('send_message')}
</Link>
</>
)}
</div>
</div>
@ -539,6 +558,26 @@ export class User extends Component<any, UserState> {
/>
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<a href="https://about.riot.im/" target="_blank">
{i18n.t('matrix_user_id')}
</a>
</label>
<div class="col-lg-7">
<input
type="text"
class="form-control"
placeholder="@user:example.com"
value={this.state.userSettingsForm.matrix_user_id}
onInput={linkEvent(
this,
this.handleUserSettingsMatrixUserIdChange
)}
minLength={3}
/>
</div>
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<T i18nKey="new_password">#</T>
@ -875,6 +914,17 @@ export class User extends Component<any, UserState> {
i.setState(i.state);
}
handleUserSettingsMatrixUserIdChange(i: User, event: any) {
i.state.userSettingsForm.matrix_user_id = event.target.value;
if (
i.state.userSettingsForm.matrix_user_id == '' &&
!i.state.user.matrix_user_id
) {
i.state.userSettingsForm.matrix_user_id = undefined;
}
i.setState(i.state);
}
handleUserSettingsNewPasswordChange(i: User, event: any) {
i.state.userSettingsForm.new_password = event.target.value;
if (i.state.userSettingsForm.new_password == '') {
@ -927,7 +977,7 @@ export class User extends Component<any, UserState> {
.catch(error => {
i.state.avatarLoading = false;
i.setState(i.state);
alert(error);
toast(error, 'danger');
});
}
@ -963,27 +1013,27 @@ export class User extends Component<any, UserState> {
WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
}
parseMessage(msg: any) {
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
let res = wsJsonToRes(msg);
if (res.error) {
toast(i18n.t(msg.error), 'danger');
this.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') {
if (res.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
this.setState(this.state);
return;
} else if (op == UserOperation.GetUserDetails) {
let res: UserDetailsResponse = msg;
this.state.user = res.user;
this.state.comments = res.comments;
this.state.follows = res.follows;
this.state.moderates = res.moderates;
this.state.posts = res.posts;
this.state.admins = res.admins;
} else if (res.op == UserOperation.GetUserDetails) {
let data = res.data as UserDetailsResponse;
this.state.user = data.user;
this.state.comments = data.comments;
this.state.follows = data.follows;
this.state.moderates = data.moderates;
this.state.posts = data.posts;
this.state.admins = data.admins;
this.state.loading = false;
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw =
@ -1001,71 +1051,72 @@ export class User extends Component<any, UserState> {
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
this.state.userSettingsForm.show_avatars =
UserService.Instance.user.show_avatars;
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0);
this.setState(this.state);
} else if (op == UserOperation.EditComment) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.content = res.comment.content;
found.updated = res.comment.updated;
found.removed = res.comment.removed;
found.deleted = res.comment.deleted;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
found.score = res.comment.score;
let found = this.state.comments.find(c => c.id == data.comment.id);
found.content = data.comment.content;
found.updated = data.comment.updated;
found.removed = data.comment.removed;
found.deleted = data.comment.deleted;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
found.score = data.comment.score;
this.setState(this.state);
} else if (op == UserOperation.CreateComment) {
} else if (res.op == UserOperation.CreateComment) {
// let res: CommentResponse = msg;
alert(i18n.t('reply_sent'));
toast(i18n.t('reply_sent'));
// this.state.comments.unshift(res.comment); // TODO do this right
// this.setState(this.state);
} else if (op == UserOperation.SaveComment) {
let res: CommentResponse = msg;
let found = this.state.comments.find(c => c.id == res.comment.id);
found.saved = res.comment.saved;
} else if (res.op == UserOperation.SaveComment) {
let data = res.data as CommentResponse;
let found = this.state.comments.find(c => c.id == data.comment.id);
found.saved = data.comment.saved;
this.setState(this.state);
} else if (op == UserOperation.CreateCommentLike) {
let res: CommentResponse = msg;
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
let found: Comment = this.state.comments.find(
c => c.id === res.comment.id
c => c.id === data.comment.id
);
found.score = res.comment.score;
found.upvotes = res.comment.upvotes;
found.downvotes = res.comment.downvotes;
if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
found.score = data.comment.score;
found.upvotes = data.comment.upvotes;
found.downvotes = data.comment.downvotes;
if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
this.setState(this.state);
} else if (op == UserOperation.CreatePostLike) {
let res: CreatePostLikeResponse = msg;
let found = this.state.posts.find(c => c.id == res.post.id);
found.my_vote = res.post.my_vote;
found.score = res.post.score;
found.upvotes = res.post.upvotes;
found.downvotes = res.post.downvotes;
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as CreatePostLikeResponse;
let found = this.state.posts.find(c => c.id == data.post.id);
found.my_vote = data.post.my_vote;
found.score = data.post.score;
found.upvotes = data.post.upvotes;
found.downvotes = data.post.downvotes;
this.setState(this.state);
} else if (op == UserOperation.BanUser) {
let res: BanUserResponse = msg;
} else if (res.op == UserOperation.BanUser) {
let data = res.data as BanUserResponse;
this.state.comments
.filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned = res.banned));
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.state.posts
.filter(c => c.creator_id == res.user.id)
.forEach(c => (c.banned = res.banned));
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.setState(this.state);
} else if (op == UserOperation.AddAdmin) {
let res: AddAdminResponse = msg;
this.state.admins = res.admins;
} else if (res.op == UserOperation.AddAdmin) {
let data = res.data as AddAdminResponse;
this.state.admins = data.admins;
this.setState(this.state);
} else if (op == UserOperation.SaveUserSettings) {
} else if (res.op == UserOperation.SaveUserSettings) {
let data = res.data as LoginResponse;
this.state = this.emptyState;
this.state.userSettingsLoading = false;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
} else if (op == UserOperation.DeleteAccount) {
UserService.Instance.login(data);
} else if (res.op == UserOperation.DeleteAccount) {
this.state.deleteAccountLoading = false;
this.state.deleteAccountShowConfirm = false;
this.setState(this.state);

1
ui/src/index.html vendored
View file

@ -13,6 +13,7 @@
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />

5
ui/src/index.tsx vendored
View file

@ -7,6 +7,7 @@ import { Footer } from './components/footer';
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
import { CreatePrivateMessage } from './components/create-private-message';
import { PasswordChange } from './components/password_change';
import { Post } from './components/post';
import { Community } from './components/community';
@ -46,6 +47,10 @@ class Index extends Component<any, any> {
<Route path={`/login`} component={Login} />
<Route path={`/create_post`} component={CreatePost} />
<Route path={`/create_community`} component={CreateCommunity} />
<Route
path={`/create_private_message`}
component={CreatePrivateMessage}
/>
<Route path={`/communities/page/:page`} component={Communities} />
<Route path={`/communities`} component={Communities} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} />

115
ui/src/interfaces.ts vendored
View file

@ -38,6 +38,9 @@ export enum UserOperation {
DeleteAccount,
PasswordReset,
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
GetPrivateMessages,
}
export enum CommentSortType {
@ -89,6 +92,7 @@ export interface UserView {
name: string;
avatar?: string;
email?: string;
matrix_user_id?: string;
fedi_name: string;
published: string;
number_of_posts: number;
@ -218,6 +222,21 @@ export interface Site {
enable_nsfw: boolean;
}
export interface PrivateMessage {
id: number;
creator_id: number;
recipient_id: number;
content: string;
deleted: boolean;
read: boolean;
published: string;
updated?: string;
creator_name: string;
creator_avatar?: string;
recipient_name: string;
recipient_avatar?: string;
}
export enum BanType {
Community,
Site,
@ -230,7 +249,6 @@ export interface FollowCommunityForm {
}
export interface GetFollowedCommunitiesResponse {
op: string;
communities: Array<CommunityUser>;
}
@ -245,7 +263,6 @@ export interface GetUserDetailsForm {
}
export interface UserDetailsResponse {
op: string;
user: UserView;
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
@ -263,7 +280,6 @@ export interface GetRepliesForm {
}
export interface GetRepliesResponse {
op: string;
replies: Array<Comment>;
}
@ -276,7 +292,6 @@ export interface GetUserMentionsForm {
}
export interface GetUserMentionsResponse {
op: string;
mentions: Array<Comment>;
}
@ -287,7 +302,6 @@ export interface EditUserMentionForm {
}
export interface UserMentionResponse {
op: string;
mention: Comment;
}
@ -301,7 +315,6 @@ export interface BanFromCommunityForm {
}
export interface BanFromCommunityResponse {
op: string;
user: UserView;
banned: boolean;
}
@ -325,7 +338,6 @@ export interface TransferSiteForm {
}
export interface AddModToCommunityResponse {
op: string;
moderators: Array<CommunityUser>;
}
@ -337,7 +349,6 @@ export interface GetModlogForm {
}
export interface GetModlogResponse {
op: string;
removed_posts: Array<ModRemovePost>;
locked_posts: Array<ModLockPost>;
stickied_posts: Array<ModStickyPost>;
@ -478,7 +489,6 @@ export interface RegisterForm {
}
export interface LoginResponse {
op: string;
jwt: string;
}
@ -490,6 +500,7 @@ export interface UserSettingsForm {
lang: string;
avatar?: string;
email?: string;
matrix_user_id?: string;
new_password?: string;
new_password_verify?: string;
old_password?: string;
@ -513,14 +524,12 @@ export interface CommunityForm {
}
export interface GetCommunityResponse {
op: string;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
}
export interface CommunityResponse {
op: string;
community: Community;
}
@ -532,12 +541,10 @@ export interface ListCommunitiesForm {
}
export interface ListCommunitiesResponse {
op: string;
communities: Array<Community>;
}
export interface ListCategoriesResponse {
op: string;
categories: Array<Category>;
}
@ -566,7 +573,6 @@ export interface PostFormParams {
}
export interface GetPostResponse {
op: string;
post: Post;
comments: Array<Comment>;
community: Community;
@ -581,7 +587,6 @@ export interface SavePostForm {
}
export interface PostResponse {
op: string;
post: Post;
}
@ -605,7 +610,6 @@ export interface SaveCommentForm {
}
export interface CommentResponse {
op: string;
comment: Comment;
}
@ -631,7 +635,6 @@ export interface GetPostsForm {
}
export interface GetPostsResponse {
op: string;
posts: Array<Post>;
}
@ -642,7 +645,6 @@ export interface CreatePostLikeForm {
}
export interface CreatePostLikeResponse {
op: string;
post: Post;
}
@ -656,7 +658,6 @@ export interface SiteForm {
}
export interface GetSiteResponse {
op: string;
site: Site;
admins: Array<UserView>;
banned: Array<UserView>;
@ -664,7 +665,6 @@ export interface GetSiteResponse {
}
export interface SiteResponse {
op: string;
site: Site;
}
@ -677,7 +677,6 @@ export interface BanUserForm {
}
export interface BanUserResponse {
op: string;
user: UserView;
banned: boolean;
}
@ -689,7 +688,6 @@ export interface AddAdminForm {
}
export interface AddAdminResponse {
op: string;
admins: Array<UserView>;
}
@ -704,7 +702,6 @@ export interface SearchForm {
}
export interface SearchResponse {
op: string;
type_: string;
posts?: Array<Post>;
comments?: Array<Comment>;
@ -720,12 +717,78 @@ export interface PasswordResetForm {
email: string;
}
export interface PasswordResetResponse {
op: string;
}
// export interface PasswordResetResponse {
// }
export interface PasswordChangeForm {
token: string;
password: string;
password_verify: string;
}
export interface PrivateMessageForm {
content: string;
recipient_id: number;
auth?: string;
}
export interface PrivateMessageFormParams {
recipient_id: number;
}
export interface EditPrivateMessageForm {
edit_id: number;
content?: string;
deleted?: boolean;
read?: boolean;
auth?: string;
}
export interface GetPrivateMessagesForm {
unread_only: boolean;
page?: number;
limit?: number;
auth?: string;
}
export interface PrivateMessagesResponse {
messages: Array<PrivateMessage>;
}
export interface PrivateMessageResponse {
message: PrivateMessage;
}
type ResponseType =
| SiteResponse
| GetFollowedCommunitiesResponse
| ListCommunitiesResponse
| GetPostsResponse
| CreatePostLikeResponse
| GetRepliesResponse
| GetUserMentionsResponse
| ListCategoriesResponse
| CommunityResponse
| CommentResponse
| UserMentionResponse
| LoginResponse
| GetModlogResponse
| SearchResponse
| BanFromCommunityResponse
| AddModToCommunityResponse
| BanUserResponse
| AddAdminResponse
| PrivateMessageResponse
| PrivateMessagesResponse;
export interface WebSocketResponse {
op: UserOperation;
data: ResponseType;
error?: string;
}
export interface WebSocketJsonResponse {
op: string;
data: ResponseType;
error?: string;
}

View file

@ -32,12 +32,16 @@ import {
DeleteAccountForm,
PasswordResetForm,
PasswordChangeForm,
PrivateMessageForm,
EditPrivateMessageForm,
GetPrivateMessagesForm,
} from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { retryWhen, delay } from 'rxjs/operators';
import { UserService } from './';
import { i18n } from '../i18next';
import { toast } from '../utils';
export class WebSocketService {
private static _instance: WebSocketService;
@ -285,6 +289,27 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
}
public createPrivateMessage(form: PrivateMessageForm) {
this.setAuth(form);
this.subject.next(
this.wsSendWrapper(UserOperation.CreatePrivateMessage, form)
);
}
public editPrivateMessage(form: EditPrivateMessageForm) {
this.setAuth(form);
this.subject.next(
this.wsSendWrapper(UserOperation.EditPrivateMessage, form)
);
}
public getPrivateMessages(form: GetPrivateMessagesForm) {
this.setAuth(form);
this.subject.next(
this.wsSendWrapper(UserOperation.GetPrivateMessages, form)
);
}
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);
@ -294,7 +319,7 @@ export class WebSocketService {
private setAuth(obj: any, throwErr: boolean = true) {
obj.auth = UserService.Instance.auth;
if (obj.auth == null && throwErr) {
alert(i18n.t('not_logged_in'));
toast(i18n.t('not_logged_in'), 'danger');
throw 'Not logged in';
}
}

View file

@ -23,6 +23,10 @@ export const en = {
list_of_communities: 'List of communities',
number_of_communities: '{{count}} Communities',
community_reqs: 'lowercase, underscores, and no spaces.',
create_private_message: 'Create Private Message',
send_secure_message: 'Send Secure Message',
send_message: 'Send Message',
message: 'Message',
edit: 'edit',
reply: 'reply',
cancel: 'Cancel',
@ -109,6 +113,7 @@ export const en = {
replies: 'Replies',
mentions: 'Mentions',
reply_sent: 'Reply sent',
message_sent: 'Message sent',
search: 'Search',
overview: 'Overview',
view: 'View',
@ -119,6 +124,7 @@ export const en = {
notifications_error:
'Desktop notifications not available in your browser. Try Firefox or Chrome.',
unread_messages: 'Unread Messages',
messages: 'Messages',
password: 'Password',
verify_password: 'Verify Password',
old_password: 'Old Password',
@ -128,6 +134,9 @@ export const en = {
new_password: 'New Password',
no_email_setup: "This server hasn't correctly set up email.",
email: 'Email',
matrix_user_id: 'Matrix User',
private_message_disclaimer:
'Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.',
send_notifications_to_email: 'Send notifications to Email',
optional: 'Optional',
expires: 'Expires',
@ -172,6 +181,7 @@ export const en = {
joined: 'Joined',
by: 'by',
to: 'to',
from: 'from',
transfer_community: 'transfer community',
transfer_site: 'transfer site',
are_you_sure: 'are you sure?',
@ -181,6 +191,7 @@ export const en = {
landing_0:
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
not_logged_in: 'Not logged in.',
logged_in: 'Logged in.',
community_ban: 'You have been banned from this community.',
site_ban: 'You have been banned from the site',
couldnt_create_comment: "Couldn't create comment.",
@ -215,5 +226,8 @@ export const en = {
email_already_exists: 'Email already exists.',
couldnt_update_user: "Couldn't update user.",
system_err_login: 'System error. Try logging out and back in.',
couldnt_create_private_message: "Couldn't create private message.",
no_private_message_edit_allowed: 'Not allowed to edit private message.',
couldnt_update_private_message: "Couldn't update private message.",
},
};

23
ui/src/utils.ts vendored
View file

@ -11,10 +11,13 @@ import 'moment/locale/it';
import {
UserOperation,
Comment,
PrivateMessage,
User,
SortType,
ListingType,
SearchType,
WebSocketResponse,
WebSocketJsonResponse,
} from './interfaces';
import { UserService } from './services/UserService';
import markdown_it from 'markdown-it';
@ -22,6 +25,7 @@ import markdownitEmoji from 'markdown-it-emoji/light';
import markdown_it_container from 'markdown-it-container';
import * as twemoji from 'twemoji';
import * as emojiShortName from 'emoji-short-name';
import Toastify from 'toastify-js';
export const repoUrl = 'https://github.com/dessalines/lemmy';
export const markdownHelpUrl = 'https://commonmark.org/help/';
@ -38,9 +42,12 @@ export function randomStr() {
.substr(2, 10);
}
export function msgOp(msg: any): UserOperation {
export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
let opStr: string = msg.op;
return UserOperation[opStr];
return {
op: UserOperation[opStr],
data: msg.data,
};
}
export const md = new markdown_it({
@ -361,3 +368,15 @@ export function imageThumbnailer(url: string): string {
return url;
}
}
export function isCommentType(item: Comment | PrivateMessage): item is Comment {
return (item as Comment).community_id !== undefined;
}
export function toast(text: string, background: string = 'success') {
let backgroundColor = `var(--${background})`;
Toastify({
text: text,
backgroundColor: backgroundColor,
}).showToast();
}

5
ui/yarn.lock vendored
View file

@ -4622,6 +4622,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
toastify-js@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.6.2.tgz#38af35625797d3d3f51fa09851f0bda449271423"
integrity sha512-ECQzgjTjxaElfwp/8e8qoIYx7U5rU2G54e5aiPMv+UtmGOYEitrtNp/Kr8uMgntnQNrDZEQJNGjBtoNnEgR5EA==
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"