diff --git a/README.md b/README.md index e56e8680..061621ce 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins - Lead singer from [motorhead](https://invidio.us/watch?v=pWB5JZRGl0U). - The old school [video game](). +- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). - The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). diff --git a/server/migrations/2019-04-29-175834_add_delete_columns/down.sql b/server/migrations/2019-04-29-175834_add_delete_columns/down.sql new file mode 100644 index 00000000..5e13295b --- /dev/null +++ b/server/migrations/2019-04-29-175834_add_delete_columns/down.sql @@ -0,0 +1,137 @@ +drop view reply_view; +drop view comment_view; +drop view community_view; +drop view post_view; +alter table community drop column deleted; +alter table post drop column deleted; +alter table comment drop column deleted; + +create view community_view as +with all_community as +( + select *, + (select name from user_ u where c.creator_id = u.id) as creator_name, + (select name from category ct where c.category_id = ct.id) as category_name, + (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, + (select count(*) from post p where p.community_id = c.id) as number_of_posts, + (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments + from community c +) + +select +ac.*, +u.id as user_id, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed +from user_ u +cross join all_community ac + +union all + +select +ac.*, +null as user_id, +null as subscribed +from all_community ac +; + +create or replace view post_view as +with all_post as +( + select + p.*, + (select name from user_ where p.creator_id = user_.id) as creator_name, + (select name from community where p.community_id = community.id) as community_name, + (select removed from community c where p.community_id = c.id) as community_removed, + (select count(*) from comment where comment.post_id = p.id) as number_of_comments, + coalesce(sum(pl.score), 0) as score, + count (case when pl.score = 1 then 1 else null end) as upvotes, + count (case when pl.score = -1 then 1 else null end) as downvotes, + hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank + from post p + left join post_like pl on p.id = pl.post_id + group by p.id +) + +select +ap.*, +u.id as user_id, +coalesce(pl.score, 0) as my_vote, +(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed, +(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read, +(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved +from user_ u +cross join all_post ap +left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id + +union all + +select +ap.*, +null as user_id, +null as my_vote, +null as subscribed, +null as read, +null as saved +from all_post ap +; + +create view comment_view as +with all_comment as +( + select + c.*, + (select community_id from post p where p.id = c.post_id), + (select u.banned from user_ u where c.creator_id = u.id) as banned, + (select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community, + (select name from user_ where c.creator_id = user_.id) as creator_name, + coalesce(sum(cl.score), 0) as score, + count (case when cl.score = 1 then 1 else null end) as upvotes, + count (case when cl.score = -1 then 1 else null end) as downvotes + from comment c + left join comment_like cl on c.id = cl.comment_id + group by c.id +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as saved +from all_comment ac +; + +create view reply_view as +with closereply as ( + select + c2.id, + c2.creator_id as sender_id, + c.creator_id as recipient_id + from comment c + inner join comment c2 on c.id = c2.parent_id + where c2.creator_id != c.creator_id + -- Do union where post is null + union + select + c.id, + c.creator_id as sender_id, + p.creator_id as recipient_id + from comment c, post p + where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id +) +select cv.*, +closereply.recipient_id +from comment_view cv, closereply +where closereply.id = cv.id +; + diff --git a/server/migrations/2019-04-29-175834_add_delete_columns/up.sql b/server/migrations/2019-04-29-175834_add_delete_columns/up.sql new file mode 100644 index 00000000..88432dda --- /dev/null +++ b/server/migrations/2019-04-29-175834_add_delete_columns/up.sql @@ -0,0 +1,141 @@ +alter table community add column deleted boolean default false not null; +alter table post add column deleted boolean default false not null; +alter table comment add column deleted boolean default false not null; + +-- The views +drop view community_view; + +create view community_view as +with all_community as +( + select *, + (select name from user_ u where c.creator_id = u.id) as creator_name, + (select name from category ct where c.category_id = ct.id) as category_name, + (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, + (select count(*) from post p where p.community_id = c.id) as number_of_posts, + (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments + from community c +) + +select +ac.*, +u.id as user_id, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed +from user_ u +cross join all_community ac + +union all + +select +ac.*, +null as user_id, +null as subscribed +from all_community ac +; + + +drop view post_view; +create view post_view as +with all_post as +( + select + p.*, + (select name from user_ where p.creator_id = user_.id) as creator_name, + (select name from community where p.community_id = community.id) as community_name, + (select removed from community c where p.community_id = c.id) as community_removed, + (select deleted from community c where p.community_id = c.id) as community_deleted, + (select count(*) from comment where comment.post_id = p.id) as number_of_comments, + coalesce(sum(pl.score), 0) as score, + count (case when pl.score = 1 then 1 else null end) as upvotes, + count (case when pl.score = -1 then 1 else null end) as downvotes, + hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank + from post p + left join post_like pl on p.id = pl.post_id + group by p.id +) + +select +ap.*, +u.id as user_id, +coalesce(pl.score, 0) as my_vote, +(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed, +(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read, +(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved +from user_ u +cross join all_post ap +left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id + +union all + +select +ap.*, +null as user_id, +null as my_vote, +null as subscribed, +null as read, +null as saved +from all_post ap +; + +drop view reply_view; +drop view comment_view; +create view comment_view as +with all_comment as +( + select + c.*, + (select community_id from post p where p.id = c.post_id), + (select u.banned from user_ u where c.creator_id = u.id) as banned, + (select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community, + (select name from user_ where c.creator_id = user_.id) as creator_name, + coalesce(sum(cl.score), 0) as score, + count (case when cl.score = 1 then 1 else null end) as upvotes, + count (case when cl.score = -1 then 1 else null end) as downvotes + from comment c + left join comment_like cl on c.id = cl.comment_id + group by c.id +) + +select +ac.*, +u.id as user_id, +coalesce(cl.score, 0) as my_vote, +(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved +from user_ u +cross join all_comment ac +left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id + +union all + +select + ac.*, + null as user_id, + null as my_vote, + null as saved +from all_comment ac +; + +create view reply_view as +with closereply as ( + select + c2.id, + c2.creator_id as sender_id, + c.creator_id as recipient_id + from comment c + inner join comment c2 on c.id = c2.parent_id + where c2.creator_id != c.creator_id + -- Do union where post is null + union + select + c.id, + c.creator_id as sender_id, + p.creator_id as recipient_id + from comment c, post p + where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id +) +select cv.*, +closereply.recipient_id +from comment_view cv, closereply +where closereply.id = cv.id +; + diff --git a/server/migrations/2019-05-02-051656_community_view_hot_rank/down.sql b/server/migrations/2019-05-02-051656_community_view_hot_rank/down.sql new file mode 100644 index 00000000..0f3a58a8 --- /dev/null +++ b/server/migrations/2019-05-02-051656_community_view_hot_rank/down.sql @@ -0,0 +1,28 @@ +drop view community_view; +create view community_view as +with all_community as +( + select *, + (select name from user_ u where c.creator_id = u.id) as creator_name, + (select name from category ct where c.category_id = ct.id) as category_name, + (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, + (select count(*) from post p where p.community_id = c.id) as number_of_posts, + (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments + from community c +) + +select +ac.*, +u.id as user_id, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed +from user_ u +cross join all_community ac + +union all + +select +ac.*, +null as user_id, +null as subscribed +from all_community ac +; diff --git a/server/migrations/2019-05-02-051656_community_view_hot_rank/up.sql b/server/migrations/2019-05-02-051656_community_view_hot_rank/up.sql new file mode 100644 index 00000000..e7e75366 --- /dev/null +++ b/server/migrations/2019-05-02-051656_community_view_hot_rank/up.sql @@ -0,0 +1,29 @@ +drop view community_view; +create view community_view as +with all_community as +( + select *, + (select name from user_ u where c.creator_id = u.id) as creator_name, + (select name from category ct where c.category_id = ct.id) as category_name, + (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers, + (select count(*) from post p where p.community_id = c.id) as number_of_posts, + (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments, + hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank + from community c +) + +select +ac.*, +u.id as user_id, +(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed +from user_ u +cross join all_community ac + +union all + +select +ac.*, +null as user_id, +null as subscribed +from all_community ac +; diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index 9bb6d018..4bbc7c04 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -25,7 +25,8 @@ pub struct Comment { pub removed: bool, pub read: bool, pub published: chrono::NaiveDateTime, - pub updated: Option + pub updated: Option, + pub deleted: bool, } #[derive(Insertable, AsChangeset, Clone)] @@ -37,7 +38,8 @@ pub struct CommentForm { pub content: String, pub removed: Option, pub read: Option, - pub updated: Option + pub updated: Option, + pub deleted: Option, } impl Crud for Comment { @@ -186,6 +188,7 @@ mod tests { category_id: 1, creator_id: inserted_user.id, removed: None, + deleted: None, updated: None }; @@ -198,6 +201,7 @@ mod tests { body: None, community_id: inserted_community.id, removed: None, + deleted: None, locked: None, updated: None }; @@ -209,6 +213,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, removed: None, + deleted: None, read: None, parent_id: None, updated: None @@ -222,6 +227,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, removed: false, + deleted: false, read: false, parent_id: None, published: inserted_comment.published, @@ -234,6 +240,7 @@ mod tests { post_id: inserted_post.id, parent_id: Some(inserted_comment.id), removed: None, + deleted: None, read: None, updated: None }; diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs index 85ddf587..eb6276cc 100644 --- a/server/src/actions/comment_view.rs +++ b/server/src/actions/comment_view.rs @@ -17,6 +17,7 @@ table! { read -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, community_id -> Int4, banned -> Bool, banned_from_community -> Bool, @@ -42,6 +43,7 @@ pub struct CommentView { pub read: bool, pub published: chrono::NaiveDateTime, pub updated: Option, + pub deleted: bool, pub community_id: i32, pub banned: bool, pub banned_from_community: bool, @@ -115,6 +117,7 @@ impl CommentView { _ => query.order_by(published.desc()) }; + // Note: deleted and removed comments are done on the front side query .limit(limit) .offset(offset) @@ -153,6 +156,7 @@ table! { read -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, community_id -> Int4, banned -> Bool, banned_from_community -> Bool, @@ -179,6 +183,7 @@ pub struct ReplyView { pub read: bool, pub published: chrono::NaiveDateTime, pub updated: Option, + pub deleted: bool, pub community_id: i32, pub banned: bool, pub banned_from_community: bool, @@ -275,6 +280,7 @@ mod tests { category_id: 1, creator_id: inserted_user.id, removed: None, + deleted: None, updated: None }; @@ -287,6 +293,7 @@ mod tests { body: None, community_id: inserted_community.id, removed: None, + deleted: None, locked: None, updated: None }; @@ -299,6 +306,7 @@ mod tests { post_id: inserted_post.id, parent_id: None, removed: None, + deleted: None, read: None, updated: None }; @@ -322,6 +330,7 @@ mod tests { community_id: inserted_community.id, parent_id: None, removed: false, + deleted: false, read: false, banned: false, banned_from_community: false, @@ -344,6 +353,7 @@ mod tests { community_id: inserted_community.id, parent_id: None, removed: false, + deleted: false, read: false, banned: false, banned_from_community: false, diff --git a/server/src/actions/community.rs b/server/src/actions/community.rs index 42c95c7d..db53ceb9 100644 --- a/server/src/actions/community.rs +++ b/server/src/actions/community.rs @@ -16,7 +16,8 @@ pub struct Community { pub creator_id: i32, pub removed: bool, pub published: chrono::NaiveDateTime, - pub updated: Option + pub updated: Option, + pub deleted: bool, } #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)] @@ -28,7 +29,8 @@ pub struct CommunityForm { pub category_id: i32, pub creator_id: i32, pub removed: Option, - pub updated: Option + pub updated: Option, + pub deleted: Option, } impl Crud for Community { @@ -245,6 +247,7 @@ mod tests { description: None, category_id: 1, removed: None, + deleted: None, updated: None, }; @@ -258,6 +261,7 @@ mod tests { description: None, category_id: 1, removed: false, + deleted: false, published: inserted_community.published, updated: None }; diff --git a/server/src/actions/community_view.rs b/server/src/actions/community_view.rs index 8966ee15..9a162746 100644 --- a/server/src/actions/community_view.rs +++ b/server/src/actions/community_view.rs @@ -15,11 +15,13 @@ table! { removed -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, creator_name -> Varchar, category_name -> Varchar, number_of_subscribers -> BigInt, number_of_posts -> BigInt, number_of_comments -> BigInt, + hot_rank -> Int4, user_id -> Nullable, subscribed -> Nullable, } @@ -85,11 +87,13 @@ pub struct CommunityView { pub removed: bool, pub published: chrono::NaiveDateTime, pub updated: Option, + pub deleted: bool, pub creator_name: String, pub category_name: String, pub number_of_subscribers: i64, pub number_of_posts: i64, pub number_of_comments: i64, + pub hot_rank: i32, pub user_id: Option, pub subscribed: Option, } @@ -125,6 +129,7 @@ impl CommunityView { // The view lets you pass a null user_id, if you're not logged in match sort { + SortType::Hot => query = query.order_by(hot_rank.desc()).filter(user_id.is_null()), SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()), SortType::TopAll => { match from_user_id { @@ -139,6 +144,7 @@ impl CommunityView { .limit(limit) .offset(offset) .filter(removed.eq(false)) + .filter(deleted.eq(false)) .load::(conn) } } diff --git a/server/src/actions/moderator.rs b/server/src/actions/moderator.rs index a0d7db6c..794d91e7 100644 --- a/server/src/actions/moderator.rs +++ b/server/src/actions/moderator.rs @@ -442,6 +442,7 @@ mod tests { category_id: 1, creator_id: inserted_user.id, removed: None, + deleted: None, updated: None }; @@ -454,6 +455,7 @@ mod tests { creator_id: inserted_user.id, community_id: inserted_community.id, removed: None, + deleted: None, locked: None, updated: None }; @@ -465,6 +467,7 @@ mod tests { creator_id: inserted_user.id, post_id: inserted_post.id, removed: None, + deleted: None, read: None, parent_id: None, updated: None diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index 4dd4561d..495a8b09 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -17,7 +17,8 @@ pub struct Post { pub removed: bool, pub locked: bool, pub published: chrono::NaiveDateTime, - pub updated: Option + pub updated: Option, + pub deleted: bool, } #[derive(Insertable, AsChangeset, Clone)] @@ -30,7 +31,8 @@ pub struct PostForm { pub community_id: i32, pub removed: Option, pub locked: Option, - pub updated: Option + pub updated: Option, + pub deleted: Option, } impl Crud for Post { @@ -199,6 +201,7 @@ mod tests { category_id: 1, creator_id: inserted_user.id, removed: None, + deleted: None, updated: None }; @@ -211,6 +214,7 @@ mod tests { creator_id: inserted_user.id, community_id: inserted_community.id, removed: None, + deleted: None, locked: None, updated: None }; @@ -227,6 +231,7 @@ mod tests { published: inserted_post.published, removed: false, locked: false, + deleted: false, updated: None }; diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index e24b0ed2..18287651 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -23,9 +23,11 @@ table! { locked -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, creator_name -> Varchar, community_name -> Varchar, community_removed -> Bool, + community_deleted -> Bool, number_of_comments -> BigInt, score -> BigInt, upvotes -> BigInt, @@ -53,9 +55,11 @@ pub struct PostView { pub locked: bool, pub published: chrono::NaiveDateTime, pub updated: Option, + pub deleted: bool, pub creator_name: String, pub community_name: String, pub community_removed: bool, + pub community_deleted: bool, pub number_of_comments: i64, pub score: i64, pub upvotes: i64, @@ -144,7 +148,9 @@ impl PostView { .limit(limit) .offset(offset) .filter(removed.eq(false)) - .filter(community_removed.eq(false)); + .filter(deleted.eq(false)) + .filter(community_removed.eq(false)) + .filter(community_deleted.eq(false)); query.load::(conn) } @@ -206,6 +212,7 @@ mod tests { creator_id: inserted_user.id, category_id: 1, removed: None, + deleted: None, updated: None }; @@ -218,6 +225,7 @@ mod tests { creator_id: inserted_user.id, community_id: inserted_community.id, removed: None, + deleted: None, locked: None, updated: None }; @@ -258,9 +266,11 @@ mod tests { creator_name: user_name.to_owned(), community_id: inserted_community.id, removed: false, + deleted: false, locked: false, community_name: community_name.to_owned(), community_removed: false, + community_deleted: false, number_of_comments: 0, score: 1, upvotes: 1, @@ -281,12 +291,14 @@ mod tests { url: None, body: None, removed: false, + deleted: false, locked: false, creator_id: inserted_user.id, creator_name: user_name.to_owned(), community_id: inserted_community.id, community_name: community_name.to_owned(), community_removed: false, + community_deleted: false, number_of_comments: 0, score: 1, upvotes: 1, diff --git a/server/src/bin/main.rs b/server/src/bin/main.rs index 96f8181d..c2fde341 100644 --- a/server/src/bin/main.rs +++ b/server/src/bin/main.rs @@ -29,7 +29,8 @@ fn chat_route(req: &HttpRequest) -> Result) -> Result Running { // notify chat server - ctx.state().addr.do_send(Disconnect { id: self.id }); + ctx.state().addr.do_send(Disconnect { + id: self.id, + ip: self.ip.to_owned(), + }); Running::Stop } } @@ -111,7 +117,7 @@ impl StreamHandler for WSSession { .addr .send(StandardMessage { id: self.id, - msg: m + msg: m, }) .into_actor(self) .then(|res, _, ctx| { @@ -215,7 +221,7 @@ impl WSSession { // notify chat server ctx.state() .addr - .do_send(Disconnect { id: act.id }); + .do_send(Disconnect { id: act.id, ip: act.ip.to_owned() }); // stop actor ctx.stop(); diff --git a/server/src/schema.rs b/server/src/schema.rs index 65c2ae55..27bc3f94 100644 --- a/server/src/schema.rs +++ b/server/src/schema.rs @@ -16,6 +16,7 @@ table! { read -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, } } @@ -50,6 +51,7 @@ table! { removed -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, } } @@ -182,6 +184,7 @@ table! { locked -> Bool, published -> Timestamp, updated -> Nullable, + deleted -> Bool, } } diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 9c609a47..aaeae132 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -11,6 +11,7 @@ use bcrypt::{verify}; use std::str::FromStr; use diesel::PgConnection; use failure::Error; +use std::time::{SystemTime}; use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs}; use actions::community::*; @@ -25,9 +26,14 @@ use actions::user_view::*; use actions::moderator_views::*; use actions::moderator::*; +const RATE_LIMIT_MESSAGES: i32 = 30; +const RATE_LIMIT_PER_SECOND: i32 = 60; +const RATE_LIMIT_REGISTER_MESSAGES: i32 = 1; +const RATE_LIMIT_REGISTER_PER_SECOND: i32 = 60; + #[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, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search, MarkAllAsRead } #[derive(Fail, Debug)] @@ -48,12 +54,14 @@ pub struct WSMessage(pub String); #[rtype(usize)] pub struct Connect { pub addr: Recipient, + pub ip: String, } /// Session is disconnected #[derive(Message)] pub struct Disconnect { pub id: usize, + pub ip: String, } /// Send message to specific room @@ -219,6 +227,7 @@ pub struct EditComment { creator_id: i32, post_id: i32, removed: Option, + deleted: Option, reason: Option, read: Option, auth: String @@ -268,6 +277,7 @@ pub struct EditPost { url: Option, body: Option, removed: Option, + deleted: Option, locked: Option, reason: Option, auth: String @@ -288,6 +298,7 @@ pub struct EditCommunity { description: Option, category_id: i32, removed: Option, + deleted: Option, reason: Option, expires: Option, auth: String @@ -320,6 +331,7 @@ pub struct GetUserDetails { limit: Option, community_id: Option, saved_only: bool, + auth: Option, } #[derive(Serialize, Deserialize)] @@ -478,10 +490,27 @@ pub struct SearchResponse { posts: Vec, } +#[derive(Serialize, Deserialize)] +pub struct MarkAllAsRead { + auth: String +} + +#[derive(Debug)] +pub struct RateLimitBucket { + last_checked: SystemTime, + allowance: f64 +} + +pub struct SessionInfo { + pub addr: Recipient, + pub ip: String, +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { - sessions: HashMap>, // A map from generated random ID to session addr + sessions: HashMap, // A map from generated random ID to session addr + rate_limits: HashMap, rooms: HashMap>, // A map from room / post name to set of connectionIDs rng: ThreadRng, } @@ -493,6 +522,7 @@ impl Default for ChatServer { ChatServer { sessions: HashMap::new(), + rate_limits: HashMap::new(), rooms: rooms, rng: rand::thread_rng(), } @@ -505,8 +535,8 @@ impl ChatServer { if let Some(sessions) = self.rooms.get(&room) { for id in sessions { if *id != skip_id { - if let Some(addr) = self.sessions.get(id) { - let _ = addr.do_send(WSMessage(message.to_owned())); + if let Some(info) = self.sessions.get(id) { + let _ = info.addr.do_send(WSMessage(message.to_owned())); } } } @@ -531,8 +561,51 @@ impl ChatServer { Ok(()) } + + fn check_rate_limit_register(&mut self, addr: usize) -> Result<(), Error> { + self.check_rate_limit_full(addr, RATE_LIMIT_REGISTER_MESSAGES, RATE_LIMIT_REGISTER_PER_SECOND) + } + + fn check_rate_limit(&mut self, addr: usize) -> Result<(), Error> { + self.check_rate_limit_full(addr, RATE_LIMIT_MESSAGES, RATE_LIMIT_PER_SECOND) + } + + fn check_rate_limit_full(&mut self, addr: usize, rate: i32, per: i32) -> Result<(), Error> { + if let Some(info) = self.sessions.get(&addr) { + if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) { + // The initial value + if rate_limit.allowance == -2f64 { + rate_limit.allowance = rate as f64; + }; + + let current = SystemTime::now(); + let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64; + rate_limit.last_checked = current; + rate_limit.allowance += time_passed * (rate as f64 / per as f64); + if rate_limit.allowance > rate as f64 { + rate_limit.allowance = rate as f64; + } + + if rate_limit.allowance < 1.0 { + println!("Rate limited IP: {}, time_passed: {}, allowance: {}", &info.ip, time_passed, rate_limit.allowance); + Err(ErrorMessage { + op: "Rate Limit".to_string(), + message: format!("Too many requests. {} per {} seconds", rate, per), + })? + } else { + rate_limit.allowance -= 1.0; + Ok(()) + } + } else { + Ok(()) + } + } else { + Ok(()) + } + } } + /// Make actor from `ChatServer` impl Actor for ChatServer { /// We are going to use simple Context, we just need ability to communicate @@ -546,14 +619,30 @@ impl Actor for ChatServer { impl Handler for ChatServer { type Result = usize; - fn handle(&mut self, msg: Connect, _: &mut Context) -> Self::Result { + fn handle(&mut self, msg: Connect, _ctx: &mut Context) -> Self::Result { // notify all users in same room // self.send_room_message(&"Main".to_owned(), "Someone joined", 0); // register session with random id let id = self.rng.gen::(); - self.sessions.insert(id, msg.addr); + println!("{} joined", &msg.ip); + + self.sessions.insert(id, SessionInfo { + addr: msg.addr, + ip: msg.ip.to_owned(), + }); + + if self.rate_limits.get(&msg.ip).is_none() { + self.rate_limits.insert(msg.ip, RateLimitBucket { + last_checked: SystemTime::now(), + allowance: -2f64, + }); + } + + // for (k,v) in &self.rate_limits { + // println!("{}: {:?}", k,v); + // } // auto join session to Main room // self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id); @@ -563,6 +652,7 @@ impl Handler for ChatServer { } } + /// Handler for Disconnect message. impl Handler for ChatServer { type Result = (); @@ -728,6 +818,10 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { + let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?; + mark_all_as_read.perform(chat, msg.id) + }, } } @@ -781,10 +875,12 @@ impl Perform for Register { fn op_type(&self) -> UserOperation { UserOperation::Register } - fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result { let conn = establish_connection(); + chat.check_rate_limit_register(addr)?; + // Make sure passwords match if &self.password != &self.password_verify { return Err(self.error("Passwords do not match."))? @@ -871,10 +967,12 @@ impl Perform for CreateCommunity { UserOperation::CreateCommunity } - fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result { let conn = establish_connection(); + chat.check_rate_limit_register(addr)?; + let claims = match Claims::decode(&self.auth) { Ok(claims) => claims.claims, Err(_e) => { @@ -903,6 +1001,7 @@ impl Perform for CreateCommunity { category_id: self.category_id, creator_id: user_id, removed: None, + deleted: None, updated: None, }; @@ -1016,10 +1115,12 @@ impl Perform for CreatePost { UserOperation::CreatePost } - fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result { let conn = establish_connection(); + chat.check_rate_limit_register(addr)?; + let claims = match Claims::decode(&self.auth) { Ok(claims) => claims.claims, Err(_e) => { @@ -1051,6 +1152,7 @@ impl Perform for CreatePost { community_id: self.community_id, creator_id: user_id, removed: None, + deleted: None, locked: None, updated: None }; @@ -1227,6 +1329,8 @@ impl Perform for CreateComment { let conn = establish_connection(); + chat.check_rate_limit(addr)?; + let claims = match Claims::decode(&self.auth) { Ok(claims) => claims.claims, Err(_e) => { @@ -1255,6 +1359,7 @@ impl Perform for CreateComment { post_id: self.post_id, creator_id: user_id, removed: None, + deleted: None, read: None, updated: None }; @@ -1371,9 +1476,10 @@ impl Perform for EditComment { post_id: self.post_id, creator_id: self.creator_id, removed: self.removed.to_owned(), + deleted: self.deleted.to_owned(), read: self.read.to_owned(), updated: if self.read.is_some() { orig_comment.updated } else {Some(naive_now())} - }; + }; let _updated_comment = match Comment::update(&conn, self.edit_id, &comment_form) { Ok(comment) => comment, @@ -1483,6 +1589,8 @@ impl Perform for CreateCommentLike { let conn = establish_connection(); + chat.check_rate_limit(addr)?; + let claims = match Claims::decode(&self.auth) { Ok(claims) => claims.claims, Err(_e) => { @@ -1611,10 +1719,12 @@ impl Perform for CreatePostLike { UserOperation::CreatePostLike } - fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + fn perform(&self, chat: &mut ChatServer, addr: usize) -> Result { let conn = establish_connection(); + chat.check_rate_limit(addr)?; + let claims = match Claims::decode(&self.auth) { Ok(claims) => claims.claims, Err(_e) => { @@ -1734,6 +1844,7 @@ impl Perform for EditPost { creator_id: self.creator_id.to_owned(), community_id: self.community_id, removed: self.removed.to_owned(), + deleted: self.deleted.to_owned(), locked: self.locked.to_owned(), updated: Some(naive_now()) }; @@ -1899,6 +2010,7 @@ impl Perform for EditCommunity { category_id: self.category_id.to_owned(), creator_id: user_id, removed: self.removed.to_owned(), + deleted: self.deleted.to_owned(), updated: Some(naive_now()) }; @@ -2051,6 +2163,19 @@ impl Perform for GetUserDetails { let conn = establish_connection(); + let user_id: Option = match &self.auth { + Some(auth) => { + match Claims::decode(&auth) { + Ok(claims) => { + let user_id = claims.claims.id; + Some(user_id) + } + Err(_e) => None + } + } + None => None + }; + //TODO add save let sort = SortType::from_str(&self.sort)?; @@ -2081,7 +2206,7 @@ impl Perform for GetUserDetails { self.community_id, Some(user_details_id), None, - None, + user_id, self.saved_only, false, self.page, @@ -2103,7 +2228,7 @@ impl Perform for GetUserDetails { None, Some(user_details_id), None, - None, + user_id, self.saved_only, self.page, self.limit)? @@ -2663,7 +2788,7 @@ impl Perform for Search { }, SearchType::Comments => { comments = CommentView::list(&conn, - &sort, + &sort, None, None, Some(self.q.to_owned()), @@ -2685,7 +2810,7 @@ impl Perform for Search { self.page, self.limit)?; comments = CommentView::list(&conn, - &sort, + &sort, None, None, Some(self.q.to_owned()), @@ -2709,3 +2834,57 @@ impl Perform for Search { ) } } + + +impl Perform for MarkAllAsRead { + fn op_type(&self) -> UserOperation { + UserOperation::MarkAllAsRead + } + + fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + + let conn = establish_connection(); + + let claims = match Claims::decode(&self.auth) { + Ok(claims) => claims.claims, + Err(_e) => { + return Err(self.error("Not logged in."))? + } + }; + + let user_id = claims.id; + + let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?; + + for reply in &replies { + let comment_form = CommentForm { + content: reply.to_owned().content, + parent_id: reply.to_owned().parent_id, + post_id: reply.to_owned().post_id, + creator_id: reply.to_owned().creator_id, + removed: None, + deleted: None, + read: Some(true), + updated: reply.to_owned().updated + }; + + let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) { + Ok(comment) => comment, + Err(_e) => { + return Err(self.error("Couldn't update Comment"))? + } + }; + } + + let replies = ReplyView::get_replies(&conn, user_id, &SortType::New, true, Some(1), Some(999))?; + + Ok( + serde_json::to_string( + &GetRepliesResponse { + op: self.op_type().to_string(), + replies: replies, + } + )? + ) + } +} diff --git a/ui/fuse.js b/ui/fuse.js index 0fdf9a42..26ea7da7 100644 --- a/ui/fuse.js +++ b/ui/fuse.js @@ -45,7 +45,7 @@ Sparky.task('config', _ => { // Sparky.task('version', _ => setVersion()); Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); Sparky.task('env', _ => (isProduction = true)); -Sparky.task('copy-assets', () => Sparky.src('assets/*.svg').dest('dist/')); +Sparky.task('copy-assets', () => Sparky.src('assets/*.*').dest('dist/')); Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => { fuse.dev(); app.hmr().watch(); diff --git a/ui/package.json b/ui/package.json index b5bb14ef..d806575c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,6 +19,7 @@ "@types/js-cookie": "^2.2.1", "@types/jwt-decode": "^2.2.1", "@types/markdown-it": "^0.0.7", + "@types/markdown-it-container": "^2.0.2", "autosize": "^4.0.2", "classcat": "^1.1.3", "dotenv": "^6.1.0", @@ -27,6 +28,7 @@ "js-cookie": "^2.2.0", "jwt-decode": "^2.2.0", "markdown-it": "^8.4.2", + "markdown-it-container": "^2.0.0", "moment": "^2.24.0", "rxjs": "^6.4.0" }, diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index 1b4eda99..a69ae06f 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -56,7 +56,7 @@ export class CommentForm extends Component {
-