]> Untitled Git - lemmy.git/commitdiff
Commiting before I lose everything. I'll do this properly in a merge
authorDessalines <tyhou13@gmx.com>
Mon, 15 Apr 2019 23:12:06 +0000 (16:12 -0700)
committerDessalines <tyhou13@gmx.com>
Mon, 15 Apr 2019 23:12:06 +0000 (16:12 -0700)
49 files changed:
server/migrations/2019-02-26-002946_create_user/down.sql
server/migrations/2019-02-26-002946_create_user/up.sql
server/migrations/2019-02-27-170003_create_community/down.sql
server/migrations/2019-02-27-170003_create_community/up.sql
server/migrations/2019-03-03-163336_create_post/up.sql
server/migrations/2019-03-05-233828_create_comment/up.sql
server/migrations/2019-03-30-212058_create_post_view/up.sql
server/migrations/2019-04-03-155205_create_community_view/down.sql
server/migrations/2019-04-03-155205_create_community_view/up.sql
server/migrations/2019-04-03-155309_create_comment_view/up.sql
server/migrations/2019-04-07-003142_create_moderation_logs/down.sql
server/migrations/2019-04-07-003142_create_moderation_logs/up.sql
server/migrations/2019-04-08-015947_create_user_view/up.sql
server/migrations/2019-04-11-144915_create_mod_views/down.sql [new file with mode: 0644]
server/migrations/2019-04-11-144915_create_mod_views/up.sql [new file with mode: 0644]
server/src/actions/comment.rs
server/src/actions/comment_view.rs
server/src/actions/community.rs
server/src/actions/community_view.rs
server/src/actions/mod.rs
server/src/actions/moderator.rs [new file with mode: 0644]
server/src/actions/moderator_views.rs [new file with mode: 0644]
server/src/actions/post.rs
server/src/actions/post_view.rs
server/src/actions/user.rs
server/src/actions/user_view.rs
server/src/apub.rs
server/src/lib.rs
server/src/schema.rs
server/src/websocket_server/server.rs
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/communities.tsx
ui/src/components/community.tsx
ui/src/components/modlog.tsx [new file with mode: 0644]
ui/src/components/moment-time.tsx
ui/src/components/navbar.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post.tsx
ui/src/components/sidebar.tsx
ui/src/components/user.tsx
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/main.css
ui/src/services/WebSocketService.ts
ui/src/utils.ts
ui/src/version.ts

index 606be6e1a063a1cf0e06718212b58a457e13be85..67a280d62463d3d0abce0f6b5765c28248a1fd88 100644 (file)
@@ -1 +1,2 @@
-drop table user_
+drop table user_ban;
+drop table user_;
index 80e6e92a3802ca344cbbe1cf3e9126c7da972e94..83112e9ba0c0d11a8e65003694a021d2a3f9f402 100644 (file)
@@ -6,9 +6,18 @@ create table user_ (
   password_encrypted text not null,
   email text unique,
   icon bytea,
+  admin boolean default false,
+  banned boolean default false,
   published timestamp not null default now(),
   updated timestamp,
   unique(name, fedi_name)
 );
 
+create table user_ban (
+  id serial primary key,
+  user_id int references user_ on update cascade on delete cascade not null,
+  published timestamp not null default now(),
+  unique (user_id)
+);
+
 insert into user_ (name, fedi_name, password_encrypted) values ('admin', 'TBD', 'TBD');
index f293dfad420f7fc9edb8a915feabeeaaf2a37748..d5bd3994083099be47fee0a660324e387aa7a6b1 100644 (file)
@@ -1,3 +1,4 @@
+drop table community_user_ban;;
 drop table community_moderator;
 drop table community_follower;
 drop table community;
index 46b4df52d38a75c5bcc8687bb3a99db2242b6912..ad47adfe857078bc5547c604f8b948eefa9301b5 100644 (file)
@@ -38,6 +38,7 @@ create table community (
   description text,
   category_id int references category on update cascade on delete cascade not null,
   creator_id int references user_ on update cascade on delete cascade not null,
+  removed boolean default false,
   published timestamp not null default now(),
   updated timestamp
 );
@@ -46,14 +47,24 @@ create table community_moderator (
   id serial primary key,
   community_id int references community on update cascade on delete cascade not null,
   user_id int references user_ on update cascade on delete cascade not null,
-  published timestamp not null default now()
+  published timestamp not null default now(),
+  unique (community_id, user_id)
 );
 
 create table community_follower (
   id serial primary key,
   community_id int references community on update cascade on delete cascade not null,
   user_id int references user_ on update cascade on delete cascade not null,
-  published timestamp not null default now()
+  published timestamp not null default now(),
+  unique (community_id, user_id)
+);
+
+create table community_user_ban (
+  id serial primary key,
+  community_id int references community on update cascade on delete cascade not null,
+  user_id int references user_ on update cascade on delete cascade not null,
+  published timestamp not null default now(),
+  unique (community_id, user_id)
 );
 
 insert into community (name, title, category_id, creator_id) values ('main', 'The Default Community', 1, 1);
index aaa6911ebd12b87e5cd702656a9c5466b7b07420..c3b7c0b8bc8876b368ba86549d73d9de391bd86b 100644 (file)
@@ -5,6 +5,8 @@ create table post (
   body text,
   creator_id int references user_ on update cascade on delete cascade not null,
   community_id int references community on update cascade on delete cascade not null,
+  removed boolean default false,
+  locked boolean default false,
   published timestamp not null default now(),
   updated timestamp
 );
index aa20d358846a8c1ace04acd24295c27fe9097c67..214d50a6a43d55e40a6ec8b38fdcc82a56ac0f54 100644 (file)
@@ -4,6 +4,7 @@ create table comment (
   post_id int references post on update cascade on delete cascade not null,
   parent_id int references comment on update cascade on delete cascade,
   content text not null,
+  removed boolean default false,
   published timestamp not null default now(),
   updated timestamp
 );
index 95789c734b6927a2df10599cb05ebf01ef2f4e46..ecf3280a4d7299a3919f01296b8df1f3c5142fd5 100644 (file)
@@ -30,7 +30,8 @@ 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 cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ap.community_id) as am_mod
 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
@@ -41,33 +42,7 @@ select
 ap.*,
 null as user_id,
 null as my_vote,
-null as subscribed
+null as subscribed,
+null as am_mod
 from all_post ap
 ;
-
-/* The old post view */
-/* create view post_view as */
-/* select */ 
-/* u.id as user_id, */
-/* pl.score as my_vote, */
-/* p.id as id, */ 
-/* p.name as name, */
-/* p.url, */
-/* p.body, */
-/* p.creator_id, */
-/* (select name from user_ where p.creator_id = user_.id) creator_name, */
-/* p.community_id, */
-/* (select name from community where p.community_id = community.id) as community_name, */
-/* (select count(*) from comment where comment.post_id = p.id) as number_of_comments, */
-/* coalesce(sum(pl.score) over (partition by p.id), 0) as score, */
-/* count (case when pl.score = 1 then 1 else null end) over (partition by p.id) as upvotes, */
-/* count (case when pl.score = -1 then 1 else null end) over (partition by p.id) as downvotes, */
-/* hot_rank(coalesce(sum(pl.score) over (partition by p.id) , 0), p.published) as hot_rank, */
-/* p.published, */
-/* p.updated */
-/* from user_ u */
-/* cross join post p */
-/* left join post_like pl on u.id = pl.user_id and p.id = pl.post_id; */
-
-
-
index 6c7e87084b0ad17b7d57c3e7ae450b140940de5c..0c7a33c80fd3d407bd3af24cd3684d041629c813 100644 (file)
@@ -1,3 +1,4 @@
 drop view community_view;
 drop view community_moderator_view;
 drop view community_follower_view;
+drop view community_user_ban_view;
index 7c60874288e0540b2ff7ebedfaf4cc691c0e3f5b..510fd0f2133b3847ecfd95f758abdcede637e92f 100644 (file)
@@ -13,7 +13,8 @@ with all_community as
 select
 ac.*,
 u.id as user_id,
-cf.id::boolean as subscribed
+cf.id::boolean as subscribed,
+u.admin or (select cm.id::bool from community_moderator cm where u.id = cm.user_id and cm.community_id = ac.id) as am_mod
 from user_ u
 cross join all_community ac
 left join community_follower cf on u.id = cf.user_id and ac.id = cf.community_id
@@ -23,7 +24,8 @@ union all
 select 
 ac.*,
 null as user_id,
-null as subscribed
+null as subscribed,
+null as am_mod
 from all_community ac
 ;
 
@@ -38,3 +40,9 @@ select *,
 (select name from user_ u where cf.user_id = u.id) as user_name,
 (select name from community c where cf.community_id = c.id) as community_name
 from community_follower cf;
+
+create view community_user_ban_view as 
+select *,
+(select name from user_ u where cm.user_id = u.id) as user_name,
+(select name from community c where cm.community_id = c.id) as community_name
+from community_user_ban cm;
index a4d2be9f2ec42969b4440942e1f0c58795bb8880..a73b61825fae4cdc64875e3247ab9bc73c162d3d 100644 (file)
@@ -3,7 +3,9 @@ with all_comment as
 (
   select        
   c.*,
-  (select name from user_ where c.creator_id = user_.id) creator_name,
+  (select community_id from post p where p.id = c.post_id),
+  (select cb.id::bool from community_user_ban cb where c.creator_id = cb.user_id) as banned,
+  (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
@@ -15,7 +17,8 @@ with all_comment as
 select
 ac.*,
 u.id as user_id,
-coalesce(cl.score, 0) as my_vote
+coalesce(cl.score, 0) as my_vote,
+u.admin or (select cm.id::bool from community_moderator cm, post p where u.id = cm.user_id and ac.post_id = p.id and p.community_id = cm.community_id) as am_mod
 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
@@ -25,6 +28,7 @@ union all
 select 
     ac.*,
     null as user_id, 
-    null as my_vote
+    null as my_vote,
+    null as am_mod
 from all_comment ac
 ;
index 15718917a38f044ff3004bf10bbddfd7aea31350..888a87feb014dc2a83008e3f3e9f5dd354531282 100644 (file)
@@ -3,4 +3,6 @@ drop table mod_lock_post;
 drop table mod_remove_comment;
 drop table mod_remove_community;
 drop table mod_ban;
-drop table mod_add_mod;
+drop table mod_ban_from_community;
+drop table mod_add;
+drop table mod_add_community;
index 41929e50ffd9025aacf0f02af49e8da86f39875c..3b320d810d70a3ba664767ecfd39088c9dc41ecb 100644 (file)
@@ -1,4 +1,3 @@
-
 create table mod_remove_post (
   id serial primary key,
   mod_user_id int references user_ on update cascade on delete cascade not null,
@@ -12,6 +11,7 @@ create table mod_lock_post (
   id serial primary key,
   mod_user_id int references user_ on update cascade on delete cascade not null,
   post_id int references post on update cascade on delete cascade not null,
+  locked boolean default true,
   when_ timestamp not null default now()
 );
 
@@ -30,25 +30,47 @@ create table mod_remove_community (
   community_id int references community on update cascade on delete cascade not null,
   reason text,
   removed boolean default true,
+  expires timestamp,
   when_ timestamp not null default now()
 );
 
 -- TODO make sure you can't ban other mods
+create table mod_ban_from_community (
+  id serial primary key,
+  mod_user_id int references user_ on update cascade on delete cascade not null,
+  other_user_id int references user_ on update cascade on delete cascade not null,
+  community_id int references community on update cascade on delete cascade not null,
+  reason text,
+  banned boolean default true,
+  expires timestamp,
+  when_ timestamp not null default now()
+);
+
 create table mod_ban (
   id serial primary key,
   mod_user_id int references user_ on update cascade on delete cascade not null,
   other_user_id int references user_ on update cascade on delete cascade not null,
   reason text,
-  removed boolean default true,
+  banned boolean default true,
   expires timestamp,
   when_ timestamp not null default now()
 );
 
+create table mod_add_community (
+  id serial primary key,
+  mod_user_id int references user_ on update cascade on delete cascade not null,
+  other_user_id int references user_ on update cascade on delete cascade not null,
+  community_id int references community on update cascade on delete cascade not null,
+  removed boolean default false,
+  when_ timestamp not null default now()
+);
+
 -- When removed is false that means kicked
-create table mod_add_mod (
+create table mod_add (
   id serial primary key,
   mod_user_id int references user_ on update cascade on delete cascade not null,
   other_user_id int references user_ on update cascade on delete cascade not null,
   removed boolean default false,
   when_ timestamp not null default now()
-)
+);
+
index 69d052de1adffeb3b361d36597c3f7c0552fc5cf..08eb56ca08dc6046b4a7002715b05138fc23aabf 100644 (file)
@@ -2,10 +2,11 @@ create view user_view as
 select id,
 name,
 fedi_name,
+admin,
+banned,
 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;
-
diff --git a/server/migrations/2019-04-11-144915_create_mod_views/down.sql b/server/migrations/2019-04-11-144915_create_mod_views/down.sql
new file mode 100644 (file)
index 0000000..95018f3
--- /dev/null
@@ -0,0 +1,8 @@
+drop view mod_remove_post_view;
+drop view mod_lock_post_view;
+drop view mod_remove_comment_view;
+drop view mod_remove_community_view;
+drop view mod_ban_from_community_view;
+drop view mod_ban_view;
+drop view mod_add_community_view;
+drop view mod_add_view;
diff --git a/server/migrations/2019-04-11-144915_create_mod_views/up.sql b/server/migrations/2019-04-11-144915_create_mod_views/up.sql
new file mode 100644 (file)
index 0000000..908028d
--- /dev/null
@@ -0,0 +1,61 @@
+create view mod_remove_post_view as 
+select mrp.*,
+(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mrp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_remove_post mrp;
+
+create view mod_lock_post_view as 
+select mlp.*,
+(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
+(select name from post p where mlp.post_id = p.id) as post_name,
+(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
+(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
+from mod_lock_post mlp;
+
+create view mod_remove_comment_view as 
+select mrc.*,
+(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
+(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
+(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
+(select content from comment c where mrc.comment_id = c.id) as comment_content,
+(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
+(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
+(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id, 
+(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
+from mod_remove_comment mrc;
+
+create view mod_remove_community_view as 
+select mrc.*,
+(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
+(select c.name from community c where mrc.community_id = c.id) as community_name
+from mod_remove_community mrc;
+
+create view mod_ban_from_community_view as 
+select mb.*,
+(select name from user_ u where mb.mod_user_id = u.id) as mod_user_name,
+(select name from user_ u where mb.other_user_id = u.id) as other_user_name,
+(select name from community c where mb.community_id = c.id) as community_name
+from mod_ban_from_community mb;
+
+create view mod_ban_view as 
+select mb.*,
+(select name from user_ u where mb.mod_user_id = u.id) as mod_user_name,
+(select name from user_ u where mb.other_user_id = u.id) as other_user_name
+from mod_ban_from_community mb;
+
+
+create view mod_add_community_view as 
+select ma.*,
+(select name from user_ u where ma.mod_user_id = u.id) as mod_user_name,
+(select name from user_ u where ma.other_user_id = u.id) as other_user_name,
+(select name from community c where ma.community_id = c.id) as community_name
+from mod_add_community ma;
+
+
+create view mod_add_view as 
+select ma.*,
+(select name from user_ u where ma.mod_user_id = u.id) as mod_user_name,
+(select name from user_ u where ma.other_user_id = u.id) as other_user_name
+from mod_add ma;
index ff5028503c8151e05706ed3d00524847f96635cf..2c5c570eeeee38207ceb02207cd4ce7150e94ffb 100644 (file)
@@ -22,6 +22,7 @@ pub struct Comment {
   pub post_id: i32,
   pub parent_id: Option<i32>,
   pub content: String,
+  pub removed: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -33,6 +34,7 @@ pub struct CommentForm {
   pub post_id: i32,
   pub parent_id: Option<i32>,
   pub content: String,
+  pub removed: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
@@ -135,6 +137,8 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      admin: None,
+      banned: None,
       updated: None
     };
 
@@ -146,6 +150,7 @@ mod tests {
       description: None,
       category_id: 1,
       creator_id: inserted_user.id,
+      removed: None,
       updated: None
     };
 
@@ -157,6 +162,8 @@ mod tests {
       url: None,
       body: None,
       community_id: inserted_community.id,
+      removed: None,
+      locked: None,
       updated: None
     };
 
@@ -166,6 +173,7 @@ mod tests {
       content: "A test comment".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      removed: None,
       parent_id: None,
       updated: None
     };
@@ -177,6 +185,7 @@ mod tests {
       content: "A test comment".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      removed: Some(false),
       parent_id: None,
       published: inserted_comment.published,
       updated: None
@@ -187,6 +196,7 @@ mod tests {
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
       parent_id: Some(inserted_comment.id),
+      removed: None,
       updated: None
     };
 
index 3b4e00bb87d1e2405d28932ba64a38ebd61b19a0..e1cc41170716425f1319761c3ec24a71335da669 100644 (file)
@@ -13,14 +13,18 @@ table! {
     post_id -> Int4,
     parent_id -> Nullable<Int4>,
     content -> Text,
+    removed -> Nullable<Bool>,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
+    community_id -> Int4,
+    banned -> Nullable<Bool>,
     creator_name -> Varchar,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
+    am_mod -> Nullable<Bool>,
   }
 }
 
@@ -32,14 +36,18 @@ pub struct CommentView {
   pub post_id: i32,
   pub parent_id: Option<i32>,
   pub content: String,
+  pub removed: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
+  pub community_id: i32,
+  pub banned: Option<bool>,
   pub creator_name: String,
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
+  pub am_mod: Option<bool>,
 }
 
 impl CommentView {
@@ -130,6 +138,8 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      admin: None,
+      banned: None,
       updated: None
     };
 
@@ -141,6 +151,7 @@ mod tests {
       description: None,
       category_id: 1,
       creator_id: inserted_user.id,
+      removed: None,
       updated: None
     };
 
@@ -152,6 +163,8 @@ mod tests {
       url: None,
       body: None,
       community_id: inserted_community.id,
+      removed: None,
+      locked: None,
       updated: None
     };
 
@@ -162,6 +175,7 @@ mod tests {
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
       parent_id: None,
+      removed: None,
       updated: None
     };
 
@@ -181,7 +195,10 @@ mod tests {
       content: "A test comment 32".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      community_id: inserted_community.id,
       parent_id: None,
+      removed: Some(false),
+      banned: None,
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
@@ -189,7 +206,8 @@ mod tests {
       downvotes: 0,
       upvotes: 1,
       user_id: None,
-      my_vote: None
+      my_vote: None,
+      am_mod: None,
     };
 
     let expected_comment_view_with_user = CommentView {
@@ -197,7 +215,10 @@ mod tests {
       content: "A test comment 32".into(),
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
+      community_id: inserted_community.id,
       parent_id: None,
+      removed: Some(false),
+      banned: None,
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
@@ -206,6 +227,7 @@ mod tests {
       upvotes: 1,
       user_id: Some(inserted_user.id),
       my_vote: Some(1),
+      am_mod: None,
     };
 
     let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, 999).unwrap();
index 1c6343d0bfb9c7b1e22ae754a01a6e6438b25e3a..d179b9a261bc69d5a4e169fcfef9d21545a33c25 100644 (file)
@@ -1,9 +1,9 @@
 extern crate diesel;
-use schema::{community, community_moderator, community_follower};
+use schema::{community, community_moderator, community_follower, community_user_ban};
 use diesel::*;
 use diesel::result::Error;
 use serde::{Deserialize, Serialize};
-use {Crud, Followable, Joinable};
+use {Crud, Followable, Joinable, Bannable};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name="community"]
@@ -14,6 +14,7 @@ pub struct Community {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
+  pub removed: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -26,6 +27,7 @@ pub struct CommunityForm {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
+  pub removed: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
@@ -46,6 +48,23 @@ pub struct CommunityModeratorForm {
   pub user_id: i32,
 }
 
+#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
+#[belongs_to(Community)]
+#[table_name = "community_user_ban"]
+pub struct CommunityUserBan {
+  pub id: i32,
+  pub community_id: i32,
+  pub user_id: i32,
+  pub published: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone)]
+#[table_name="community_user_ban"]
+pub struct CommunityUserBanForm {
+  pub community_id: i32,
+  pub user_id: i32,
+}
+
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
 #[belongs_to(Community)]
 #[table_name = "community_follower"]
@@ -125,6 +144,23 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
   }
 }
 
+impl Bannable<CommunityUserBanForm> for CommunityUserBan {
+  fn ban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<Self, Error> {
+    use schema::community_user_ban::dsl::*;
+    insert_into(community_user_ban)
+      .values(community_user_ban_form)
+      .get_result::<Self>(conn)
+  }
+
+  fn unban(conn: &PgConnection, community_user_ban_form: &CommunityUserBanForm) -> Result<usize, Error> {
+    use schema::community_user_ban::dsl::*;
+    diesel::delete(community_user_ban
+      .filter(community_id.eq(community_user_ban_form.community_id))
+      .filter(user_id.eq(community_user_ban_form.user_id)))
+      .execute(conn)
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use establish_connection;
@@ -136,11 +172,13 @@ mod tests {
     let conn = establish_connection();
 
     let new_user = UserForm {
-      name: "bob".into(),
+      name: "bobbee".into(),
       fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      admin: None,
+      banned: None,
       updated: None
     };
 
@@ -152,6 +190,7 @@ mod tests {
       title: "nada".to_owned(),
       description: None,
       category_id: 1,
+      removed: None,
       updated: None,
     };
 
@@ -164,11 +203,11 @@ mod tests {
       title: "nada".to_owned(),
       description: None,
       category_id: 1,
+      removed: Some(false),
       published: inserted_community.published,
       updated: None
     };
 
-
     let community_follower_form = CommunityFollowerForm {
       community_id: inserted_community.id,
       user_id: inserted_user.id
@@ -176,6 +215,7 @@ mod tests {
 
     let inserted_community_follower = CommunityFollower::follow(&conn, &community_follower_form).unwrap();
 
+
     let expected_community_follower = CommunityFollower {
       id: inserted_community_follower.id,
       community_id: inserted_community.id,
@@ -197,10 +237,25 @@ mod tests {
       published: inserted_community_user.published
     };
 
+    let community_user_ban_form = CommunityUserBanForm {
+      community_id: inserted_community.id,
+      user_id: inserted_user.id
+    };
+
+    let inserted_community_user_ban = CommunityUserBan::ban(&conn, &community_user_ban_form).unwrap();
+
+    let expected_community_user_ban = CommunityUserBan {
+      id: inserted_community_user_ban.id,
+      community_id: inserted_community.id,
+      user_id: inserted_user.id,
+      published: inserted_community_user_ban.published
+    };
+
     let read_community = Community::read(&conn, inserted_community.id).unwrap();
     let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap();
     let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
     let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
+    let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
     let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
     User_::delete(&conn, inserted_user.id).unwrap();
 
@@ -209,8 +264,10 @@ mod tests {
     assert_eq!(expected_community, updated_community);
     assert_eq!(expected_community_follower, inserted_community_follower);
     assert_eq!(expected_community_user, inserted_community_user);
+    assert_eq!(expected_community_user_ban, inserted_community_user_ban);
     assert_eq!(1, ignored_community);
     assert_eq!(1, left_community);
+    assert_eq!(1, unban);
     // assert_eq!(2, loaded_count);
     assert_eq!(1, num_deleted);
 
index cb89b2264390cb09dd8a620723e22df23ce318a4..14f38302fc9ef71cce8a0a67cbe45068492becc4 100644 (file)
@@ -12,6 +12,7 @@ table! {
     description -> Nullable<Text>,
     category_id -> Int4,
     creator_id -> Int4,
+    removed -> Nullable<Bool>,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     creator_name -> Varchar,
@@ -21,6 +22,7 @@ table! {
     number_of_comments -> BigInt,
     user_id -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
+    am_mod -> Nullable<Bool>,
   }
 }
 
@@ -46,6 +48,17 @@ table! {
   }
 }
 
+table! {
+  community_user_ban_view (id) {
+    id -> Int4,
+    community_id -> Int4,
+    user_id -> Int4,
+    published -> Timestamp,
+    user_name -> Varchar,
+    community_name -> Varchar,
+  }
+}
+
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
 #[table_name="community_view"]
 pub struct CommunityView {
@@ -55,6 +68,7 @@ pub struct CommunityView {
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
+  pub removed: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub creator_name: String,
@@ -64,6 +78,7 @@ pub struct CommunityView {
   pub number_of_comments: i64,
   pub user_id: Option<i32>,
   pub subscribed: Option<bool>,
+  pub am_mod: Option<bool>,
 }
 
 impl CommunityView {
@@ -107,7 +122,7 @@ impl CommunityView {
       query = query.limit(limit);
     };
 
-    query.load::<Self>(conn) 
+    query.filter(removed.eq(false)).load::<Self>(conn) 
   }
 }
 
@@ -157,3 +172,35 @@ impl CommunityFollowerView {
     community_follower_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
   }
 }
+
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="community_user_ban_view"]
+pub struct CommunityUserBanView {
+  pub id: i32,
+  pub community_id: i32,
+  pub user_id: i32,
+  pub published: chrono::NaiveDateTime,
+  pub user_name : String,
+  pub community_name: String,
+}
+
+impl CommunityUserBanView {
+  pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
+    use actions::community_view::community_user_ban_view::dsl::*;
+    community_user_ban_view.filter(community_id.eq(from_community_id)).load::<Self>(conn)
+  }
+
+  pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
+    use actions::community_view::community_user_ban_view::dsl::*;
+    community_user_ban_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
+  }
+
+  pub fn get(conn: &PgConnection, from_user_id: i32, from_community_id: i32) -> Result<Self, Error> {
+    use actions::community_view::community_user_ban_view::dsl::*;
+    community_user_ban_view
+      .filter(user_id.eq(from_user_id))
+      .filter(community_id.eq(from_community_id))
+      .first::<Self>(conn)
+  }
+}
index 819d5cdaf13db76ae123357731dc5080274d2fe6..ece1e885a1a5978bda8998aa8fe91cc3a9e42e49 100644 (file)
@@ -7,3 +7,5 @@ pub mod comment_view;
 pub mod category;
 pub mod community_view;
 pub mod user_view;
+pub mod moderator;
+pub mod moderator_views;
diff --git a/server/src/actions/moderator.rs b/server/src/actions/moderator.rs
new file mode 100644 (file)
index 0000000..089c7ce
--- /dev/null
@@ -0,0 +1,655 @@
+extern crate diesel;
+use schema::{mod_remove_post, mod_lock_post, mod_remove_comment, mod_remove_community, mod_ban_from_community, mod_ban, mod_add_community, mod_add};
+use diesel::*;
+use diesel::result::Error;
+use serde::{Deserialize, Serialize};
+use {Crud};
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_remove_post"]
+pub struct ModRemovePost {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_remove_post"]
+pub struct ModRemovePostForm {
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+}
+
+impl Crud<ModRemovePostForm> for ModRemovePost {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_remove_post::dsl::*;
+    mod_remove_post.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_remove_post::dsl::*;
+    diesel::delete(mod_remove_post.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModRemovePostForm) -> Result<Self, Error> {
+    use schema::mod_remove_post::dsl::*;
+      insert_into(mod_remove_post)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModRemovePostForm) -> Result<Self, Error> {
+    use schema::mod_remove_post::dsl::*;
+    diesel::update(mod_remove_post.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_lock_post"]
+pub struct ModLockPost {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub locked: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_lock_post"]
+pub struct ModLockPostForm {
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub locked: Option<bool>,
+}
+
+impl Crud<ModLockPostForm> for ModLockPost {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_lock_post::dsl::*;
+    mod_lock_post.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_lock_post::dsl::*;
+    diesel::delete(mod_lock_post.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModLockPostForm) -> Result<Self, Error> {
+    use schema::mod_lock_post::dsl::*;
+      insert_into(mod_lock_post)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModLockPostForm) -> Result<Self, Error> {
+    use schema::mod_lock_post::dsl::*;
+    diesel::update(mod_lock_post.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_remove_comment"]
+pub struct ModRemoveComment {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub comment_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_remove_comment"]
+pub struct ModRemoveCommentForm {
+  pub mod_user_id: i32,
+  pub comment_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+}
+
+impl Crud<ModRemoveCommentForm> for ModRemoveComment {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_remove_comment::dsl::*;
+    mod_remove_comment.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_remove_comment::dsl::*;
+    diesel::delete(mod_remove_comment.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModRemoveCommentForm) -> Result<Self, Error> {
+    use schema::mod_remove_comment::dsl::*;
+      insert_into(mod_remove_comment)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommentForm) -> Result<Self, Error> {
+    use schema::mod_remove_comment::dsl::*;
+    diesel::update(mod_remove_comment.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_remove_community"]
+pub struct ModRemoveCommunity {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_remove_community"]
+pub struct ModRemoveCommunityForm {
+  pub mod_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<ModRemoveCommunityForm> for ModRemoveCommunity {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_remove_community::dsl::*;
+    mod_remove_community.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_remove_community::dsl::*;
+    diesel::delete(mod_remove_community.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
+    use schema::mod_remove_community::dsl::*;
+      insert_into(mod_remove_community)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
+    use schema::mod_remove_community::dsl::*;
+    diesel::update(mod_remove_community.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_ban_from_community"]
+pub struct ModBanFromCommunity {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_ban_from_community"]
+pub struct ModBanFromCommunityForm {
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<ModBanFromCommunityForm> for ModBanFromCommunity {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_ban_from_community::dsl::*;
+    mod_ban_from_community.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_ban_from_community::dsl::*;
+    diesel::delete(mod_ban_from_community.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
+    use schema::mod_ban_from_community::dsl::*;
+      insert_into(mod_ban_from_community)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
+    use schema::mod_ban_from_community::dsl::*;
+    diesel::update(mod_ban_from_community.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_ban"]
+pub struct ModBan {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_ban"]
+pub struct ModBanForm {
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<ModBanForm> for ModBan {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_ban::dsl::*;
+    mod_ban.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_ban::dsl::*;
+    diesel::delete(mod_ban.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModBanForm) -> Result<Self, Error> {
+    use schema::mod_ban::dsl::*;
+      insert_into(mod_ban)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModBanForm) -> Result<Self, Error> {
+    use schema::mod_ban::dsl::*;
+    diesel::update(mod_ban.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_add_community"]
+pub struct ModAddCommunity {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_add_community"]
+pub struct ModAddCommunityForm {
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub removed: Option<bool>,
+}
+
+impl Crud<ModAddCommunityForm> for ModAddCommunity {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_add_community::dsl::*;
+    mod_add_community.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_add_community::dsl::*;
+    diesel::delete(mod_add_community.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModAddCommunityForm) -> Result<Self, Error> {
+    use schema::mod_add_community::dsl::*;
+      insert_into(mod_add_community)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModAddCommunityForm) -> Result<Self, Error> {
+    use schema::mod_add_community::dsl::*;
+    diesel::update(mod_add_community.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name="mod_add"]
+pub struct ModAdd {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name="mod_add"]
+pub struct ModAddForm {
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub removed: Option<bool>,
+}
+
+impl Crud<ModAddForm> for ModAdd {
+  fn read(conn: &PgConnection, from_id: i32) -> Result<Self, Error> {
+    use schema::mod_add::dsl::*;
+    mod_add.find(from_id)
+      .first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
+    use schema::mod_add::dsl::*;
+    diesel::delete(mod_add.find(from_id))
+      .execute(conn)
+  }
+
+  fn create(conn: &PgConnection, form: &ModAddForm) -> Result<Self, Error> {
+    use schema::mod_add::dsl::*;
+      insert_into(mod_add)
+        .values(form)
+        .get_result::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, from_id: i32, form: &ModAddForm) -> Result<Self, Error> {
+    use schema::mod_add::dsl::*;
+    diesel::update(mod_add.find(from_id))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use establish_connection;
+  use super::*;
+  use actions::user::*;
+  use actions::post::*;
+  use actions::community::*;
+  use actions::comment::*;
+  // use Crud;
+ #[test]
+  fn test_crud() {
+    let conn = establish_connection();
+
+    let new_mod = UserForm {
+      name: "the mod".into(),
+      fedi_name: "rrf".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      admin: None,
+      banned: None,
+      updated: None
+    };
+
+    let inserted_mod = User_::create(&conn, &new_mod).unwrap();
+
+    let new_user = UserForm {
+      name: "jim2".into(),
+      fedi_name: "rrf".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      admin: None,
+      banned: None,
+      updated: None
+    };
+
+    let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+    let new_community = CommunityForm {
+      name: "mod_community".to_string(),
+      title: "nada".to_owned(),
+      description: None,
+      category_id: 1,
+      creator_id: inserted_user.id,
+      removed: None,
+      updated: None
+    };
+
+    let inserted_community = Community::create(&conn, &new_community).unwrap();
+    
+    let new_post = PostForm {
+      name: "A test post thweep".into(),
+      url: None,
+      body: None,
+      creator_id: inserted_user.id,
+      community_id: inserted_community.id,
+      removed: None,
+      locked: None,
+      updated: None
+    };
+
+    let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+    let comment_form = CommentForm {
+      content: "A test comment".into(),
+      creator_id: inserted_user.id,
+      post_id: inserted_post.id,
+      removed: None,
+      parent_id: None,
+      updated: None
+    };
+
+    let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+    // Now the actual tests
+
+    // remove post
+    let mod_remove_post_form = ModRemovePostForm {
+      mod_user_id: inserted_mod.id,
+      post_id: inserted_post.id,
+      reason: None,
+      removed: None,
+    };
+    let inserted_mod_remove_post = ModRemovePost::create(&conn, &mod_remove_post_form).unwrap();
+    let read_moderator_remove_post = ModRemovePost::read(&conn, inserted_mod_remove_post.id).unwrap();
+    let expected_moderator_remove_post = ModRemovePost {
+      id: inserted_mod_remove_post.id,
+      post_id: inserted_post.id,
+      mod_user_id: inserted_mod.id,
+      reason: None,
+      removed: Some(true),
+      when_: inserted_mod_remove_post.when_,
+    };
+
+    // lock post
+
+    let mod_lock_post_form = ModLockPostForm {
+      mod_user_id: inserted_mod.id,
+      post_id: inserted_post.id,
+      locked: None,
+    };
+    let inserted_mod_lock_post = ModLockPost::create(&conn, &mod_lock_post_form).unwrap();
+    let read_moderator_lock_post = ModLockPost::read(&conn, inserted_mod_lock_post.id).unwrap();
+    let expected_moderator_lock_post = ModLockPost {
+      id: inserted_mod_lock_post.id,
+      post_id: inserted_post.id,
+      mod_user_id: inserted_mod.id,
+      locked: Some(true),
+      when_: inserted_mod_lock_post.when_,
+    };
+
+    // comment
+
+    let mod_remove_comment_form = ModRemoveCommentForm {
+      mod_user_id: inserted_mod.id,
+      comment_id: inserted_comment.id,
+      reason: None,
+      removed: None,
+    };
+    let inserted_mod_remove_comment = ModRemoveComment::create(&conn, &mod_remove_comment_form).unwrap();
+    let read_moderator_remove_comment = ModRemoveComment::read(&conn, inserted_mod_remove_comment.id).unwrap();
+    let expected_moderator_remove_comment = ModRemoveComment {
+      id: inserted_mod_remove_comment.id,
+      comment_id: inserted_comment.id,
+      mod_user_id: inserted_mod.id,
+      reason: None,
+      removed: Some(true),
+      when_: inserted_mod_remove_comment.when_,
+    };
+
+    // community
+
+    let mod_remove_community_form = ModRemoveCommunityForm {
+      mod_user_id: inserted_mod.id,
+      community_id: inserted_community.id,
+      reason: None,
+      removed: None,
+      expires: None,
+    };
+    let inserted_mod_remove_community = ModRemoveCommunity::create(&conn, &mod_remove_community_form).unwrap();
+    let read_moderator_remove_community = ModRemoveCommunity::read(&conn, inserted_mod_remove_community.id).unwrap();
+    let expected_moderator_remove_community = ModRemoveCommunity {
+      id: inserted_mod_remove_community.id,
+      community_id: inserted_community.id,
+      mod_user_id: inserted_mod.id,
+      reason: None,
+      removed: Some(true),
+      expires: None,
+      when_: inserted_mod_remove_community.when_,
+    };
+
+    // ban from community
+
+    let mod_ban_from_community_form = ModBanFromCommunityForm {
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      community_id: inserted_community.id,
+      reason: None,
+      banned: None,
+      expires: None,
+    };
+    let inserted_mod_ban_from_community = ModBanFromCommunity::create(&conn, &mod_ban_from_community_form).unwrap();
+    let read_moderator_ban_from_community = ModBanFromCommunity::read(&conn, inserted_mod_ban_from_community.id).unwrap();
+    let expected_moderator_ban_from_community = ModBanFromCommunity {
+      id: inserted_mod_ban_from_community.id,
+      community_id: inserted_community.id,
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      reason: None,
+      banned: Some(true),
+      expires: None,
+      when_: inserted_mod_ban_from_community.when_,
+    };
+
+    // ban
+
+    let mod_ban_form = ModBanForm {
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      reason: None,
+      banned: None,
+      expires: None,
+    };
+    let inserted_mod_ban = ModBan::create(&conn, &mod_ban_form).unwrap();
+    let read_moderator_ban = ModBan::read(&conn, inserted_mod_ban.id).unwrap();
+    let expected_moderator_ban = ModBan {
+      id: inserted_mod_ban.id,
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      reason: None,
+      banned: Some(true),
+      expires: None,
+      when_: inserted_mod_ban.when_,
+    };
+
+    // mod add community
+
+    let mod_add_community_form = ModAddCommunityForm {
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      community_id: inserted_community.id,
+      removed: None,
+    };
+    let inserted_mod_add_community = ModAddCommunity::create(&conn, &mod_add_community_form).unwrap();
+    let read_moderator_add_community = ModAddCommunity::read(&conn, inserted_mod_add_community.id).unwrap();
+    let expected_moderator_add_community = ModAddCommunity {
+      id: inserted_mod_add_community.id,
+      community_id: inserted_community.id,
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      removed: Some(false),
+      when_: inserted_mod_add_community.when_,
+    };
+
+    // mod add
+
+    let mod_add_form = ModAddForm {
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      removed: None,
+    };
+    let inserted_mod_add = ModAdd::create(&conn, &mod_add_form).unwrap();
+    let read_moderator_add = ModAdd::read(&conn, inserted_mod_add.id).unwrap();
+    let expected_moderator_add = ModAdd {
+      id: inserted_mod_add.id,
+      mod_user_id: inserted_mod.id,
+      other_user_id: inserted_user.id,
+      removed: Some(false),
+      when_: inserted_mod_add.when_,
+    };
+
+    ModRemovePost::delete(&conn, inserted_mod_remove_post.id).unwrap();
+    ModLockPost::delete(&conn, inserted_mod_lock_post.id).unwrap();
+    ModRemoveComment::delete(&conn, inserted_mod_remove_comment.id).unwrap();
+    ModRemoveCommunity::delete(&conn, inserted_mod_remove_community.id).unwrap();
+    ModBanFromCommunity::delete(&conn, inserted_mod_ban_from_community.id).unwrap();
+    ModBan::delete(&conn, inserted_mod_ban.id).unwrap();
+    ModAddCommunity::delete(&conn, inserted_mod_add_community.id).unwrap();
+    ModAdd::delete(&conn, inserted_mod_add.id).unwrap();
+
+    Comment::delete(&conn, inserted_comment.id).unwrap();
+    Post::delete(&conn, inserted_post.id).unwrap();
+    Community::delete(&conn, inserted_community.id).unwrap();
+    User_::delete(&conn, inserted_user.id).unwrap();
+    User_::delete(&conn, inserted_mod.id).unwrap();
+
+    assert_eq!(expected_moderator_remove_post, read_moderator_remove_post);
+    assert_eq!(expected_moderator_lock_post, read_moderator_lock_post);
+    assert_eq!(expected_moderator_remove_comment, read_moderator_remove_comment);
+    assert_eq!(expected_moderator_remove_community, read_moderator_remove_community);
+    assert_eq!(expected_moderator_ban_from_community, read_moderator_ban_from_community);
+    assert_eq!(expected_moderator_ban, read_moderator_ban);
+    assert_eq!(expected_moderator_add_community, read_moderator_add_community);
+    assert_eq!(expected_moderator_add, read_moderator_add);
+  }
+}
diff --git a/server/src/actions/moderator_views.rs b/server/src/actions/moderator_views.rs
new file mode 100644 (file)
index 0000000..2e24356
--- /dev/null
@@ -0,0 +1,427 @@
+extern crate diesel;
+use diesel::*;
+use diesel::result::Error;
+use serde::{Deserialize, Serialize};
+
+table! {
+  mod_remove_post_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    post_id -> Int4,
+    reason -> Nullable<Text>,
+    removed -> Nullable<Bool>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    post_name -> Varchar,
+    community_id -> Int4,
+    community_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_remove_post_view"]
+pub struct ModRemovePostView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub post_name: String,
+  pub community_id: i32,
+  pub community_name: String,
+}
+
+impl ModRemovePostView {
+  pub fn list(conn: &PgConnection, 
+              from_community_id: Option<i32>, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_remove_post_view::dsl::*;
+    let mut query = mod_remove_post_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_community_id) = from_community_id {
+      query = query.filter(community_id.eq(from_community_id));
+    };
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_lock_post_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    post_id -> Int4,
+    locked -> Nullable<Bool>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    post_name -> Varchar,
+    community_id -> Int4,
+    community_name -> Varchar,
+  }
+}
+
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_lock_post_view"]
+pub struct ModLockPostView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub post_id: i32,
+  pub locked: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub post_name: String,
+  pub community_id: i32,
+  pub community_name: String,
+}
+
+impl ModLockPostView {
+  pub fn list(conn: &PgConnection, 
+              from_community_id: Option<i32>, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_lock_post_view::dsl::*;
+    let mut query = mod_lock_post_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_community_id) = from_community_id {
+      query = query.filter(community_id.eq(from_community_id));
+    };
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_remove_comment_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    comment_id -> Int4,
+    reason -> Nullable<Text>,
+    removed -> Nullable<Bool>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    comment_user_id -> Int4,
+    comment_user_name -> Varchar,
+    comment_content -> Text,
+    post_id -> Int4,
+    post_name -> Varchar,
+    community_id -> Int4,
+    community_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_remove_comment_view"]
+pub struct ModRemoveCommentView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub comment_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub comment_user_id: i32,
+  pub comment_user_name: String,
+  pub comment_content: String,
+  pub post_id: i32,
+  pub post_name: String,
+  pub community_id: i32,
+  pub community_name: String,
+}
+
+impl ModRemoveCommentView {
+  pub fn list(conn: &PgConnection, 
+              from_community_id: Option<i32>, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_remove_comment_view::dsl::*;
+    let mut query = mod_remove_comment_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_community_id) = from_community_id {
+      query = query.filter(community_id.eq(from_community_id));
+    };
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_remove_community_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    community_id -> Int4,
+    reason -> Nullable<Text>,
+    removed -> Nullable<Bool>,
+    expires -> Nullable<Timestamp>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    community_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_remove_community_view"]
+pub struct ModRemoveCommunityView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub removed: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub community_name: String,
+}
+
+impl ModRemoveCommunityView {
+  pub fn list(conn: &PgConnection, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_remove_community_view::dsl::*;
+    let mut query = mod_remove_community_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+
+table! {
+  mod_ban_from_community_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    other_user_id -> Int4,
+    community_id -> Int4,
+    reason -> Nullable<Text>,
+    banned -> Nullable<Bool>,
+    expires -> Nullable<Timestamp>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    other_user_name -> Varchar,
+    community_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_ban_from_community_view"]
+pub struct ModBanFromCommunityView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub other_user_name: String,
+  pub community_name: String,
+}
+
+impl ModBanFromCommunityView {
+  pub fn list(conn: &PgConnection, 
+              from_community_id: Option<i32>, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_ban_from_community_view::dsl::*;
+    let mut query = mod_ban_from_community_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_community_id) = from_community_id {
+      query = query.filter(community_id.eq(from_community_id));
+    };
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_ban_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    other_user_id -> Int4,
+    reason -> Nullable<Text>,
+    banned -> Nullable<Bool>,
+    expires -> Nullable<Timestamp>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    other_user_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_ban_view"]
+pub struct ModBanView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub reason: Option<String>,
+  pub banned: Option<bool>,
+  pub expires: Option<chrono::NaiveDateTime>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub other_user_name: String,
+}
+
+impl ModBanView {
+  pub fn list(conn: &PgConnection, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_ban_view::dsl::*;
+    let mut query = mod_ban_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_add_community_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    other_user_id -> Int4,
+    community_id -> Int4,
+    removed -> Nullable<Bool>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    other_user_name -> Varchar,
+    community_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_add_community_view"]
+pub struct ModAddCommunityView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub community_id: i32,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub other_user_name: String,
+  pub community_name: String,
+}
+
+impl ModAddCommunityView {
+  pub fn list(conn: &PgConnection, 
+              from_community_id: Option<i32>, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_add_community_view::dsl::*;
+    let mut query = mod_add_community_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_community_id) = from_community_id {
+      query = query.filter(community_id.eq(from_community_id));
+    };
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
+
+table! {
+  mod_add_view (id) {
+    id -> Int4,
+    mod_user_id -> Int4,
+    other_user_id -> Int4,
+    removed -> Nullable<Bool>,
+    when_ -> Timestamp,
+    mod_user_name -> Varchar,
+    other_user_name -> Varchar,
+  }
+}
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="mod_add_view"]
+pub struct ModAddView {
+  pub id: i32,
+  pub mod_user_id: i32,
+  pub other_user_id: i32,
+  pub removed: Option<bool>,
+  pub when_: chrono::NaiveDateTime,
+  pub mod_user_name: String,
+  pub other_user_name: String,
+}
+
+impl ModAddView {
+  pub fn list(conn: &PgConnection, 
+              from_mod_user_id: Option<i32>, 
+              limit: Option<i64>, 
+              page: Option<i64>) -> Result<Vec<Self>, Error> {
+    use actions::moderator_views::mod_add_view::dsl::*;
+    let mut query = mod_add_view.into_boxed();
+
+    let page = page.unwrap_or(1);
+    let limit = limit.unwrap_or(10);
+    let offset = limit * (page - 1);
+
+    if let Some(from_mod_user_id) = from_mod_user_id {
+      query = query.filter(mod_user_id.eq(from_mod_user_id));
+    };
+
+    query.limit(limit).offset(offset).order_by(when_.desc()).load::<Self>(conn) 
+  }
+}
index b53aae4635b98139fc35355edfc8930171eccebb..b811bf326b0271dcfb02ad73e61c770b61d90352 100644 (file)
@@ -14,6 +14,8 @@ pub struct Post {
   pub body: Option<String>,
   pub creator_id: i32,
   pub community_id: i32,
+  pub removed: Option<bool>,
+  pub locked: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -26,6 +28,8 @@ pub struct PostForm {
   pub body: Option<String>,
   pub creator_id: i32,
   pub community_id: i32,
+  pub removed: Option<bool>,
+  pub locked: Option<bool>,
   pub updated: Option<chrono::NaiveDateTime>
 }
 
@@ -115,17 +119,20 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      admin: None,
+      banned: None,
       updated: None
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
 
     let new_community = CommunityForm {
-      name: "test community_2".to_string(),
+      name: "test community_3".to_string(),
       title: "nada".to_owned(),
       description: None,
       category_id: 1,
       creator_id: inserted_user.id,
+      removed: None,
       updated: None
     };
 
@@ -137,6 +144,8 @@ mod tests {
       body: None,
       creator_id: inserted_user.id,
       community_id: inserted_community.id,
+      removed: None,
+      locked: None,
       updated: None
     };
 
@@ -150,6 +159,8 @@ mod tests {
       creator_id: inserted_user.id,
       community_id: inserted_community.id,
       published: inserted_post.published,
+      removed: Some(false),
+      locked: Some(false),
       updated: None
     };
 
index 6ca85c3419c390f528a18f44fd10621ded8dec03..9b4395d3fc4f92313fda89a0979650abebaf2f90 100644 (file)
@@ -19,6 +19,8 @@ table! {
     body -> Nullable<Text>,
     creator_id -> Int4,
     community_id -> Int4,
+    removed -> Nullable<Bool>,
+    locked -> Nullable<Bool>,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     creator_name -> Varchar,
@@ -31,6 +33,7 @@ table! {
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
+    am_mod -> Nullable<Bool>,
   }
 }
 
@@ -44,6 +47,8 @@ pub struct PostView {
   pub body: Option<String>,
   pub creator_id: i32,
   pub community_id: i32,
+  pub removed: Option<bool>,
+  pub locked: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub creator_name: String,
@@ -56,6 +61,7 @@ pub struct PostView {
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub subscribed: Option<bool>,
+  pub am_mod: Option<bool>,
 }
 
 impl PostView {
@@ -110,6 +116,8 @@ impl PostView {
               .order_by(score.desc())
     };
 
+    query = query.filter(removed.eq(false));
+
     query.load::<Self>(conn) 
   }
 
@@ -156,7 +164,9 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
-      updated: None
+      updated: None,
+      admin: None,
+      banned: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -167,6 +177,7 @@ mod tests {
       description: None,
       creator_id: inserted_user.id,
       category_id: 1,
+      removed: None,
       updated: None
     };
 
@@ -178,6 +189,8 @@ mod tests {
       body: None,
       creator_id: inserted_user.id,
       community_id: inserted_community.id,
+      removed: None,
+      locked: None,
       updated: None
     };
 
@@ -216,6 +229,8 @@ mod tests {
       creator_id: inserted_user.id,
       creator_name: user_name.to_owned(),
       community_id: inserted_community.id,
+      removed: Some(false),
+      locked: Some(false),
       community_name: community_name.to_owned(),
       number_of_comments: 0,
       score: 1,
@@ -224,7 +239,8 @@ mod tests {
       hot_rank: 864,
       published: inserted_post.published,
       updated: None,
-      subscribed: None
+      subscribed: None,
+      am_mod: None,
     };
 
     let expected_post_listing_with_user = PostView {
@@ -234,6 +250,8 @@ mod tests {
       name: post_name.to_owned(),
       url: None,
       body: None,
+      removed: Some(false),
+      locked: Some(false),
       creator_id: inserted_user.id,
       creator_name: user_name.to_owned(),
       community_id: inserted_community.id,
@@ -245,7 +263,8 @@ mod tests {
       hot_rank: 864,
       published: inserted_post.published,
       updated: None,
-      subscribed: None
+      subscribed: None,
+      am_mod: None,
     };
 
 
@@ -274,6 +293,5 @@ mod tests {
     assert_eq!(expected_post_like, inserted_post_like);
     assert_eq!(1, like_removed);
     assert_eq!(1, num_deleted);
-
   }
 }
index d646adcba599bd13062ddf624a6f838597529bc6..524fb66d44884a10750ff5c664504597562ed1fc 100644 (file)
@@ -17,6 +17,8 @@ pub struct User_ {
   pub password_encrypted: String,
   pub email: Option<String>,
   pub icon: Option<Vec<u8>>,
+  pub admin: Option<bool>,
+  pub banned: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>
 }
@@ -28,6 +30,8 @@ pub struct UserForm {
     pub fedi_name: String,
     pub preferred_username: Option<String>,
     pub password_encrypted: String,
+    pub admin: Option<bool>,
+    pub banned: Option<bool>,
     pub email: Option<String>,
     pub updated: Option<chrono::NaiveDateTime>
 }
@@ -122,6 +126,8 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      admin: None,
+      banned: None,
       updated: None
     };
 
@@ -135,6 +141,8 @@ mod tests {
       password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(),
       email: None,
       icon: None,
+      admin: Some(false),
+      banned: Some(false),
       published: inserted_user.published,
       updated: None
     };
index 5873a5c86b452da95af12f37eb65f459b9d5dc7b..4457e08a7d777fdb1131fafc70074b34358b2b4b 100644 (file)
@@ -8,6 +8,8 @@ table! {
     id -> Int4,
     name -> Varchar,
     fedi_name -> Varchar,
+    admin -> Nullable<Bool>,
+    banned -> Nullable<Bool>,
     published -> Timestamp,
     number_of_posts -> BigInt,
     post_score -> BigInt,
@@ -22,6 +24,8 @@ pub struct UserView {
   pub id: i32,
   pub name: String,
   pub fedi_name: String,
+  pub admin: Option<bool>,
+  pub banned: Option<bool>,
   pub published: chrono::NaiveDateTime,
   pub number_of_posts: i64,
   pub post_score: i64,
index b24562614c35caede8f26ed5ce10e8bfe22643cf..9a535c0b0a6d2b413a61761261c11f4d339edad9 100644 (file)
@@ -44,6 +44,8 @@ mod tests {
       email: None,
       icon: None,
       published: naive_now(),
+      admin: None,
+      banned: None,
       updated: None
     };
 
index 814363b49bc6b5a35e1812c9792dc9aad086d80c..ab971edd9f215b98548678449ab9eab0d79df7b0 100644 (file)
@@ -50,6 +50,11 @@ pub trait Likeable<T> {
   fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
 }
 
+pub trait Bannable<T> {
+  fn ban(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn unban(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
+}
+
 pub fn establish_connection() -> PgConnection {
   let db_url = Settings::get().db_url;
   PgConnection::establish(&db_url)
@@ -88,6 +93,10 @@ pub fn naive_now() -> NaiveDateTime {
   chrono::prelude::Utc::now().naive_utc()
 }
 
+pub fn naive_from_unix(time: i64)  ->  NaiveDateTime {
+  NaiveDateTime::from_timestamp(time, 0)
+}
+
 pub fn is_email_regex(test: &str) -> bool {
   EMAIL_REGEX.is_match(test)
 }
index fe11a46bd77f9ca0b223b2ad38bb7d3fd1efbe07..873f32e86bc2ff252babcae9d3b5fc76a1801b85 100644 (file)
@@ -12,6 +12,7 @@ table! {
         post_id -> Int4,
         parent_id -> Nullable<Int4>,
         content -> Text,
+        removed -> Nullable<Bool>,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -36,6 +37,7 @@ table! {
         description -> Nullable<Text>,
         category_id -> Int4,
         creator_id -> Int4,
+        removed -> Nullable<Bool>,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -59,6 +61,105 @@ table! {
     }
 }
 
+table! {
+    community_user_ban (id) {
+        id -> Int4,
+        community_id -> Int4,
+        user_id -> Int4,
+        published -> Timestamp,
+    }
+}
+
+table! {
+    mod_add (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        other_user_id -> Int4,
+        removed -> Nullable<Bool>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_add_community (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        other_user_id -> Int4,
+        community_id -> Int4,
+        removed -> Nullable<Bool>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_ban (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        other_user_id -> Int4,
+        reason -> Nullable<Text>,
+        banned -> Nullable<Bool>,
+        expires -> Nullable<Timestamp>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_ban_from_community (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        other_user_id -> Int4,
+        community_id -> Int4,
+        reason -> Nullable<Text>,
+        banned -> Nullable<Bool>,
+        expires -> Nullable<Timestamp>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_lock_post (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        post_id -> Int4,
+        locked -> Nullable<Bool>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_remove_comment (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        comment_id -> Int4,
+        reason -> Nullable<Text>,
+        removed -> Nullable<Bool>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_remove_community (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        community_id -> Int4,
+        reason -> Nullable<Text>,
+        removed -> Nullable<Bool>,
+        expires -> Nullable<Timestamp>,
+        when_ -> Timestamp,
+    }
+}
+
+table! {
+    mod_remove_post (id) {
+        id -> Int4,
+        mod_user_id -> Int4,
+        post_id -> Int4,
+        reason -> Nullable<Text>,
+        removed -> Nullable<Bool>,
+        when_ -> Timestamp,
+    }
+}
+
 table! {
     post (id) {
         id -> Int4,
@@ -67,6 +168,8 @@ table! {
         body -> Nullable<Text>,
         creator_id -> Int4,
         community_id -> Int4,
+        removed -> Nullable<Bool>,
+        locked -> Nullable<Bool>,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
@@ -91,11 +194,21 @@ table! {
         password_encrypted -> Text,
         email -> Nullable<Text>,
         icon -> Nullable<Bytea>,
+        admin -> Nullable<Bool>,
+        banned -> Nullable<Bool>,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
     }
 }
 
+table! {
+    user_ban (id) {
+        id -> Int4,
+        user_id -> Int4,
+        published -> Timestamp,
+    }
+}
+
 joinable!(comment -> post (post_id));
 joinable!(comment -> user_ (creator_id));
 joinable!(comment_like -> comment (comment_id));
@@ -107,10 +220,23 @@ joinable!(community_follower -> community (community_id));
 joinable!(community_follower -> user_ (user_id));
 joinable!(community_moderator -> community (community_id));
 joinable!(community_moderator -> user_ (user_id));
+joinable!(community_user_ban -> community (community_id));
+joinable!(community_user_ban -> user_ (user_id));
+joinable!(mod_add_community -> community (community_id));
+joinable!(mod_ban_from_community -> community (community_id));
+joinable!(mod_lock_post -> post (post_id));
+joinable!(mod_lock_post -> user_ (mod_user_id));
+joinable!(mod_remove_comment -> comment (comment_id));
+joinable!(mod_remove_comment -> user_ (mod_user_id));
+joinable!(mod_remove_community -> community (community_id));
+joinable!(mod_remove_community -> user_ (mod_user_id));
+joinable!(mod_remove_post -> post (post_id));
+joinable!(mod_remove_post -> user_ (mod_user_id));
 joinable!(post -> community (community_id));
 joinable!(post -> user_ (creator_id));
 joinable!(post_like -> post (post_id));
 joinable!(post_like -> user_ (user_id));
+joinable!(user_ban -> user_ (user_id));
 
 allow_tables_to_appear_in_same_query!(
     category,
@@ -119,7 +245,17 @@ allow_tables_to_appear_in_same_query!(
     community,
     community_follower,
     community_moderator,
+    community_user_ban,
+    mod_add,
+    mod_add_community,
+    mod_ban,
+    mod_ban_from_community,
+    mod_lock_post,
+    mod_remove_comment,
+    mod_remove_community,
+    mod_remove_post,
     post,
     post_like,
     user_,
+    user_ban,
 );
index 137761ab377d318cba833dafdfa6302c56767064..b3bdf78d76e17525f5ca5fe00fcaacf93dd6685a 100644 (file)
@@ -9,8 +9,9 @@ use serde::{Deserialize, Serialize};
 use serde_json::{Value};
 use bcrypt::{verify};
 use std::str::FromStr;
+use diesel::PgConnection;
 
-use {Crud, Joinable, Likeable, Followable, establish_connection, naive_now, SortType, has_slurs, remove_slurs};
+use {Crud, Joinable, Likeable, Followable, Bannable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs};
 use actions::community::*;
 use actions::user::*;
 use actions::post::*;
@@ -20,10 +21,12 @@ use actions::comment_view::*;
 use actions::category::*;
 use actions::community_view::*;
 use actions::user_view::*;
+use actions::moderator_views::*;
+use actions::moderator::*;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -204,7 +207,10 @@ pub struct EditComment {
   content: String,
   parent_id: Option<i32>,
   edit_id: i32,
+  creator_id: i32,
   post_id: i32,
+  removed: Option<bool>,
+  reason: Option<String>,
   auth: String
 }
 
@@ -222,7 +228,6 @@ pub struct CreateCommentLike {
   auth: String
 }
 
-
 #[derive(Serialize, Deserialize)]
 pub struct CreatePostLike {
   post_id: i32,
@@ -240,10 +245,14 @@ pub struct CreatePostLikeResponse {
 #[derive(Serialize, Deserialize)]
 pub struct EditPost {
   edit_id: i32,
+  creator_id: i32,
   community_id: i32,
   name: String,
   url: Option<String>,
   body: Option<String>,
+  removed: Option<bool>,
+  reason: Option<String>,
+  locked: Option<bool>,
   auth: String
 }
 
@@ -254,6 +263,9 @@ pub struct EditCommunity {
   title: String,
   description: Option<String>,
   category_id: i32,
+  removed: Option<bool>,
+  reason: Option<String>,
+  expires: Option<i64>,
   auth: String
 }
 
@@ -296,6 +308,59 @@ pub struct GetUserDetailsResponse {
   saved_comments: Vec<CommentView>,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct GetModlog {
+  mod_user_id: Option<i32>,
+  community_id: Option<i32>,
+  limit: Option<i64>,
+  page: Option<i64>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetModlogResponse {
+  op: String,
+  removed_posts: Vec<ModRemovePostView>,
+  locked_posts: Vec<ModLockPostView>,
+  removed_comments: Vec<ModRemoveCommentView>,
+  removed_communities: Vec<ModRemoveCommunityView>,
+  banned_from_community: Vec<ModBanFromCommunityView>,
+  banned: Vec<ModBanView>,
+  added_to_community: Vec<ModAddCommunityView>,
+  added: Vec<ModAddView>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct BanFromCommunity {
+  community_id: i32,
+  user_id: i32,
+  ban: bool,
+  reason: Option<String>,
+  expires: Option<i64>,
+  auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct BanFromCommunityResponse {
+  op: String,
+  user: UserView,
+  banned: bool,
+}
+
+
+#[derive(Serialize, Deserialize)]
+pub struct AddModToCommunity {
+  community_id: i32,
+  user_id: i32,
+  added: bool,
+  auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct AddModToCommunityResponse {
+  op: String,
+  moderators: Vec<CommunityModeratorView>,
+}
+
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
 /// session. implementation is super primitive
 pub struct ChatServer {
@@ -330,6 +395,19 @@ impl ChatServer {
       }
     }
   }
+
+  fn send_community_message(&self, conn: &PgConnection, community_id: i32, message: &str, skip_id: usize) {
+    let posts = PostView::list(conn,
+                               PostListingType::Community, 
+                               &SortType::New, 
+                               Some(community_id), 
+                               None,
+                               None, 
+                               999).unwrap();
+    for post in posts {
+      self.send_room_message(post.id, message, skip_id);
+    }
+  }
 }
 
 /// Make actor from `ChatServer`
@@ -397,6 +475,9 @@ impl Handler<StandardMessage> for ChatServer {
     let op = &json["op"].as_str().unwrap();
     let user_operation: UserOperation = UserOperation::from_str(&op).unwrap();
 
+
+    // TODO figure out how to do proper error handling here, instead of just returning
+    // error strings
     let res: String = match user_operation {
       UserOperation::Login => {
         let login: Login = serde_json::from_str(data).unwrap();
@@ -470,13 +551,18 @@ impl Handler<StandardMessage> for ChatServer {
         let get_user_details: GetUserDetails = serde_json::from_str(data).unwrap();
         get_user_details.perform(self, msg.id)
       },
-      // _ => {
-      //   let e = ErrorMessage { 
-      //     op: "Unknown".to_string(),
-      //     error: "Unknown User Operation".to_string()
-      //   };
-      //   serde_json::to_string(&e).unwrap()
-      // }
+      UserOperation::GetModlog => {
+        let get_modlog: GetModlog = serde_json::from_str(data).unwrap();
+        get_modlog.perform(self, msg.id)
+      },
+      UserOperation::BanFromCommunity => {
+        let ban_from_community: BanFromCommunity = serde_json::from_str(data).unwrap();
+        ban_from_community.perform(self, msg.id)
+      },
+      UserOperation::AddModToCommunity => {
+        let mod_add_to_community: AddModToCommunity = serde_json::from_str(data).unwrap();
+        mod_add_to_community.perform(self, msg.id)
+      },
     };
 
     MessageResult(res)
@@ -554,7 +640,9 @@ impl Perform for Register {
       email: self.email.to_owned(),
       password_encrypted: self.password.to_owned(),
       preferred_username: None,
-      updated: None
+      updated: None,
+      admin: None,
+      banned: None,
     };
 
     // Create the user
@@ -609,7 +697,8 @@ impl Perform for CreateCommunity {
       description: self.description.to_owned(),
       category_id: self.category_id,
       creator_id: user_id,
-      updated: None
+      removed: None,
+      updated: None,
     };
 
     let inserted_community = match Community::create(&conn, &community_form) {
@@ -737,12 +826,19 @@ impl Perform for CreatePost {
 
     let user_id = claims.id;
 
+    // Check for a ban
+    if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
+      return self.error("You have been banned from this community");
+    }
+
     let post_form = PostForm {
       name: self.name.to_owned(),
       url: self.url.to_owned(),
       body: self.body.to_owned(),
       community_id: self.community_id,
       creator_id: user_id,
+      removed: None,
+      locked: None,
       updated: None
     };
 
@@ -913,6 +1009,12 @@ impl Perform for CreateComment {
 
     let user_id = claims.id;
 
+    // Check for a ban
+    let post = Post::read(&conn, self.post_id).unwrap();
+    if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
+      return self.error("You have been banned from this community");
+    }
+
     let content_slurs_removed = remove_slurs(&self.content.to_owned());
 
     let comment_form = CommentForm {
@@ -920,6 +1022,7 @@ impl Perform for CreateComment {
       parent_id: self.parent_id.to_owned(),
       post_id: self.post_id,
       creator_id: user_id,
+      removed: None,
       updated: None
     };
 
@@ -991,10 +1094,22 @@ impl Perform for EditComment {
 
     let user_id = claims.id;
 
-    // Verify its the creator
-    let orig_comment = Comment::read(&conn, self.edit_id).unwrap();
-    if user_id != orig_comment.creator_id {
-      return self.error("Incorrect creator.");
+
+    // Verify its the creator or a mod
+    let orig_comment = CommentView::read(&conn, self.edit_id, None).unwrap();
+    let mut editors: Vec<i32> = CommunityModeratorView::for_community(&conn, orig_comment.community_id)
+      .unwrap()
+      .into_iter()
+      .map(|m| m.user_id)
+      .collect();
+    editors.push(self.creator_id);
+    if !editors.contains(&user_id) {
+      return self.error("Not allowed to edit comment.");
+    }
+
+    // Check for a ban
+    if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
+      return self.error("You have been banned from this community");
     }
 
     let content_slurs_removed = remove_slurs(&self.content.to_owned());
@@ -1003,7 +1118,8 @@ impl Perform for EditComment {
       content: content_slurs_removed,
       parent_id: self.parent_id,
       post_id: self.post_id,
-      creator_id: user_id,
+      creator_id: self.creator_id,
+      removed: self.removed.to_owned(),
       updated: Some(naive_now())
     };
 
@@ -1014,6 +1130,17 @@ impl Perform for EditComment {
       }
     };
 
+    // Mod tables
+    if let Some(removed) = self.removed.to_owned() {
+      let form = ModRemoveCommentForm {
+        mod_user_id: user_id,
+        comment_id: self.edit_id,
+        removed: Some(removed),
+        reason: self.reason.to_owned(),
+      };
+      ModRemoveComment::create(&conn, &form).unwrap();
+    }
+
 
     let comment_view = CommentView::read(&conn, self.edit_id, Some(user_id)).unwrap();
 
@@ -1061,6 +1188,12 @@ impl Perform for CreateCommentLike {
 
     let user_id = claims.id;
 
+    // Check for a ban
+    let post = Post::read(&conn, self.post_id).unwrap();
+    if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
+      return self.error("You have been banned from this community");
+    }
+
     let like_form = CommentLikeForm {
       comment_id: self.comment_id,
       post_id: self.post_id,
@@ -1173,6 +1306,12 @@ impl Perform for CreatePostLike {
 
     let user_id = claims.id;
 
+    // Check for a ban
+    let post = Post::read(&conn, self.post_id).unwrap();
+    if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
+      return self.error("You have been banned from this community");
+    }
+
     let like_form = PostLikeForm {
       post_id: self.post_id,
       user_id: user_id,
@@ -1236,18 +1375,30 @@ impl Perform for EditPost {
 
     let user_id = claims.id;
 
-    // Verify its the creator
-    let orig_post = Post::read(&conn, self.edit_id).unwrap();
-    if user_id != orig_post.creator_id {
-      return self.error("Incorrect creator.");
+    // Verify its the creator or a mod
+    let mut editors: Vec<i32> = CommunityModeratorView::for_community(&conn, self.community_id)
+      .unwrap()
+      .into_iter()
+      .map(|m| m.user_id)
+      .collect();
+    editors.push(self.creator_id);
+    if !editors.contains(&user_id) {
+      return self.error("Not allowed to edit comment.");
+    }
+
+    // Check for a ban
+    if CommunityUserBanView::get(&conn, user_id, self.community_id).is_ok() {
+      return self.error("You have been banned from this community");
     }
 
     let post_form = PostForm {
       name: self.name.to_owned(),
       url: self.url.to_owned(),
       body: self.body.to_owned(),
-      creator_id: user_id,
+      creator_id: self.creator_id.to_owned(),
       community_id: self.community_id,
+      removed: self.removed.to_owned(),
+      locked: self.locked.to_owned(),
       updated: Some(naive_now())
     };
 
@@ -1258,6 +1409,26 @@ impl Perform for EditPost {
       }
     };
 
+    // Mod tables
+    if let Some(removed) = self.removed.to_owned() {
+      let form = ModRemovePostForm {
+        mod_user_id: user_id,
+        post_id: self.edit_id,
+        removed: Some(removed),
+        reason: self.reason.to_owned(),
+      };
+      ModRemovePost::create(&conn, &form).unwrap();
+    }
+
+    if let Some(locked) = self.locked.to_owned() {
+      let form = ModLockPostForm {
+        mod_user_id: user_id,
+        post_id: self.edit_id,
+        locked: Some(locked),
+      };
+      ModLockPost::create(&conn, &form).unwrap();
+    }
+
     let post_view = PostView::read(&conn, self.edit_id, Some(user_id)).unwrap();
 
     let mut post_sent = post_view.clone();
@@ -1307,7 +1478,6 @@ impl Perform for EditCommunity {
 
     let user_id = claims.id;
 
-
     // Verify its a mod
     let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id).unwrap();
     let mod_ids: Vec<i32> = moderator_view.into_iter().map(|m| m.user_id).collect();
@@ -1321,6 +1491,7 @@ impl Perform for EditCommunity {
       description: self.description.to_owned(),
       category_id: self.category_id.to_owned(),
       creator_id: user_id,
+      removed: self.removed.to_owned(),
       updated: Some(naive_now())
     };
 
@@ -1331,11 +1502,23 @@ impl Perform for EditCommunity {
       }
     };
 
-    let community_view = CommunityView::read(&conn, self.edit_id, Some(user_id)).unwrap();
+    // Mod tables
+    if let Some(removed) = self.removed.to_owned() {
+      let expires = match self.expires {
+        Some(time) => Some(naive_from_unix(time)),
+        None => None
+      };
+      let form = ModRemoveCommunityForm {
+        mod_user_id: user_id,
+        community_id: self.edit_id,
+        removed: Some(removed),
+        reason: self.reason.to_owned(),
+        expires: expires
+      };
+      ModRemoveCommunity::create(&conn, &form).unwrap();
+    }
 
-    // Do the subscriber stuff here
-    // let mut community_sent = post_view.clone();
-    // community_sent.my_vote = None;
+    let community_view = CommunityView::read(&conn, self.edit_id, Some(user_id)).unwrap();
 
     let community_out = serde_json::to_string(
       &CommunityResponse {
@@ -1345,15 +1528,17 @@ impl Perform for EditCommunity {
       )
       .unwrap();
 
-    // let post_sent_out = serde_json::to_string(
-    //   &PostResponse {
-    //     op: self.op_type().to_string(), 
-    //     post: post_sent
-    //   }
-    //   )
-    //   .unwrap();
+    let community_view_sent = CommunityView::read(&conn, self.edit_id, None).unwrap();
+
+    let community_sent = serde_json::to_string(
+      &CommunityResponse {
+        op: self.op_type().to_string(), 
+        community: community_view_sent
+      }
+      )
+      .unwrap();
 
-    chat.send_room_message(self.edit_id, &community_out, addr);
+    chat.send_community_message(&conn, self.edit_id, &community_sent, addr);
 
     community_out
   }
@@ -1492,3 +1677,178 @@ impl Perform for GetUserDetails {
   }
 }
 
+impl Perform for GetModlog {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::GetModlog
+  }
+
+  fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
+
+    let conn = establish_connection();
+
+    let removed_posts = ModRemovePostView::list(&conn, self.community_id, self.mod_user_id, self.limit, self.page).unwrap();
+    let locked_posts = ModLockPostView::list(&conn, self.community_id, self.mod_user_id, self.limit, self.page).unwrap();
+    let removed_comments = ModRemoveCommentView::list(&conn, self.community_id, self.mod_user_id, self.limit, self.page).unwrap();
+    let removed_communities = ModRemoveCommunityView::list(&conn, self.mod_user_id, self.limit, self.page).unwrap();
+    let banned_from_community = ModBanFromCommunityView::list(&conn, self.community_id, self.mod_user_id, self.limit, self.page).unwrap();
+    let banned = ModBanView::list(&conn, self.mod_user_id, self.limit, self.page).unwrap();
+    let added_to_community = ModAddCommunityView::list(&conn, self.community_id, self.mod_user_id, self.limit, self.page).unwrap();
+    let added = ModAddView::list(&conn, self.mod_user_id, self.limit, self.page).unwrap();
+
+    // Return the jwt
+    serde_json::to_string(
+      &GetModlogResponse {
+        op: self.op_type().to_string(),
+        removed_posts: removed_posts,
+        locked_posts: locked_posts,
+        removed_comments: removed_comments,
+        removed_communities: removed_communities,
+        banned_from_community: banned_from_community,
+        banned: banned,
+        added_to_community: added_to_community,
+        added: added,
+      }
+      )
+      .unwrap()
+  }
+}
+
+impl Perform for BanFromCommunity {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::BanFromCommunity
+  }
+
+  fn perform(&self, chat: &mut ChatServer, addr: usize) -> String {
+
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&self.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return self.error("Not logged in.");
+      }
+    };
+
+    let user_id = claims.id;
+
+    let community_user_ban_form = CommunityUserBanForm {
+      community_id: self.community_id,
+      user_id: self.user_id,
+    };
+
+    if self.ban {
+      match CommunityUserBan::ban(&conn, &community_user_ban_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community user ban already exists");
+        }
+      };
+    } else {
+      match CommunityUserBan::unban(&conn, &community_user_ban_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community user ban already exists");
+        }
+      };
+    }
+
+    // Mod tables
+    let expires = match self.expires {
+      Some(time) => Some(naive_from_unix(time)),
+      None => None
+    };
+
+    let form = ModBanFromCommunityForm {
+      mod_user_id: user_id,
+      other_user_id: self.user_id,
+      community_id: self.community_id,
+      reason: self.reason.to_owned(),
+      banned: Some(self.ban),
+      expires: expires,
+    };
+    ModBanFromCommunity::create(&conn, &form).unwrap();
+
+    let user_view = UserView::read(&conn, self.user_id).unwrap();
+
+    let res = serde_json::to_string(
+      &BanFromCommunityResponse {
+        op: self.op_type().to_string(), 
+        user: user_view,
+        banned: self.ban
+      }
+      )
+      .unwrap();
+
+
+    chat.send_community_message(&conn, self.community_id, &res, addr);
+
+    res
+  }
+}
+
+impl Perform for AddModToCommunity {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::AddModToCommunity
+  }
+
+  fn perform(&self, chat: &mut ChatServer, addr: usize) -> String {
+
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&self.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return self.error("Not logged in.");
+      }
+    };
+
+    let user_id = claims.id;
+
+    let community_moderator_form = CommunityModeratorForm {
+      community_id: self.community_id,
+      user_id: self.user_id
+    };
+
+    if self.added {
+      match CommunityModerator::join(&conn, &community_moderator_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community moderator already exists.");
+        }
+      };
+    } else {
+      match CommunityModerator::leave(&conn, &community_moderator_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community moderator already exists.");
+        }
+      };
+    }
+
+    // Mod tables
+    let form = ModAddCommunityForm {
+      mod_user_id: user_id,
+      other_user_id: self.user_id,
+      community_id: self.community_id,
+      removed: Some(!self.added),
+    };
+    ModAddCommunity::create(&conn, &form).unwrap();
+
+    let moderators = CommunityModeratorView::for_community(&conn, self.community_id).unwrap();
+
+    let res = serde_json::to_string(
+      &AddModToCommunityResponse {
+        op: self.op_type().to_string(), 
+        moderators: moderators,
+      }
+      )
+      .unwrap();
+
+
+    chat.send_community_message(&conn, self.community_id, &res, addr);
+
+    res
+
+  }
+}
+
index a87dd3567208067c49db70487637e0f4763afcf5..66f3094eefcfe549f9b35caf2f9f76a736f96eb6 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, linkEvent } from 'inferno';
 import { CommentNode as CommentNodeI, CommentForm as CommentFormI } from '../interfaces';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
 import * as autosize from 'autosize';
 
 interface CommentFormProps {
@@ -8,6 +8,7 @@ interface CommentFormProps {
   node?: CommentNodeI;
   onReplyCancel?(): any;
   edit?: boolean;
+  disabled?: boolean;
 }
 
 interface CommentFormState {
@@ -21,9 +22,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     commentForm: {
       auth: null,
       content: null,
-      post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId
+      post_id: this.props.node ? this.props.node.comment.post_id : this.props.postId,
+      creator_id: UserService.Instance.loggedIn ? UserService.Instance.user.id : null,
     },
-    buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply"
+    buttonTitle: !this.props.node ? "Post" : this.props.edit ? "Edit" : "Reply",
   }
 
   constructor(props: any, context: any) {
@@ -36,6 +38,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
         this.state.commentForm.edit_id = this.props.node.comment.id;
         this.state.commentForm.parent_id = this.props.node.comment.parent_id;
         this.state.commentForm.content = this.props.node.comment.content;
+        this.state.commentForm.creator_id = this.props.node.comment.creator_id;
       } else {
         // A reply gets a new parent id
         this.state.commentForm.parent_id = this.props.node.comment.id;
@@ -53,12 +56,12 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
         <form onSubmit={linkEvent(this, this.handleCommentSubmit)}>
           <div class="form-group row">
             <div class="col-sm-12">
-              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required />
+              <textarea class="form-control" value={this.state.commentForm.content} onInput={linkEvent(this, this.handleCommentContentChange)} placeholder="Comment here" required disabled={this.props.disabled}/>
             </div>
           </div>
           <div class="row">
             <div class="col-sm-12">
-              <button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
+              <button type="submit" class="btn btn-sm btn-secondary mr-2" disabled={this.props.disabled}>{this.state.buttonTitle}</button>
               {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
             </div>
           </div>
index 1e5376f2272dd05a7d1edf4a6cf9f296f64201df..eba36009c2a4afb7c9280a925007c473184758a5 100644 (file)
@@ -1,8 +1,8 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI } from '../interfaces';
+import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, BanFromCommunityForm, CommunityUser, AddModToCommunityForm } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { mdToHtml } from '../utils';
+import { mdToHtml, getUnixTime } from '../utils';
 import { MomentTime } from './moment-time';
 import { CommentForm } from './comment-form';
 import { CommentNodes } from './comment-nodes';
@@ -10,19 +10,31 @@ import { CommentNodes } from './comment-nodes';
 interface CommentNodeState {
   showReply: boolean;
   showEdit: boolean;
+  showRemoveDialog: boolean;
+  removeReason: string;
+  showBanDialog: boolean;
+  banReason: string;
+  banExpires: string;
 }
 
 interface CommentNodeProps {
   node: CommentNodeI;
   noIndent?: boolean;
   viewOnly?: boolean;
+  locked?: boolean;
+  moderators: Array<CommunityUser>;
 }
 
 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
   private emptyState: CommentNodeState = {
     showReply: false,
-    showEdit: false
+    showEdit: false,
+    showRemoveDialog: false,
+    removeReason: null,
+    showBanDialog: false,
+    banReason: null,
+    banExpires: null,
   }
 
   constructor(props: any, context: any) {
@@ -60,10 +72,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               <span><MomentTime data={node.comment} /></span>
             </li>
           </ul>
-          {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} />}
+          {this.state.showEdit && <CommentForm node={node} edit onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
           {!this.state.showEdit &&
             <div>
-              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.content)} />
+              <div className="md-div" dangerouslySetInnerHTML={mdToHtml(node.comment.removed ? '*removed*' : node.comment.content)} />
               <ul class="list-inline mb-1 text-muted small font-weight-bold">
                 {!this.props.viewOnly && 
                   <span class="mr-2">
@@ -71,14 +83,39 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                       <span class="pointer" onClick={linkEvent(this, this.handleReplyClick)}>reply</span>
                     </li>
                     {this.myComment && 
+                      <>
                       <li className="list-inline-item">
                         <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
                       </li>
-                    }
-                    {this.myComment &&
                       <li className="list-inline-item">
                         <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
                       </li>
+                    </>
+                    }
+                    {this.canMod &&
+                      <>
+                      <li className="list-inline-item">
+                        {!this.props.node.comment.removed ? 
+                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
+                        <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+                        }
+                      </li>
+                      {!this.isMod &&
+                        <>
+                          <li className="list-inline-item">
+                            {!this.props.node.comment.banned ? 
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanShow)}>ban</span> :
+                            <span class="pointer" onClick={linkEvent(this, this.handleModBanSubmit)}>unban</span>
+                            }
+                          </li>
+                        </>
+                      }
+                      {!this.props.node.comment.banned &&
+                        <li className="list-inline-item">
+                          <span class="pointer" onClick={linkEvent(this, this.handleAddModToCommunity)}>{`${this.isMod ? 'remove' : 'appoint'} as mod`}</span>
+                        </li>
+                      }
+                    </>
                     }
                   </span>
                 }
@@ -89,16 +126,61 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             </div>
           }
         </div>
-        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
-        {this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
+        {this.state.showRemoveDialog && 
+          <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+            <input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
+            <button type="submit" class="btn btn-secondary">Remove Comment</button>
+          </form>
+        }
+        {this.state.showBanDialog && 
+        <form onSubmit={linkEvent(this, this.handleModBanSubmit)}>
+          <div class="form-group row">
+            <label class="col-form-label">Reason</label>
+            <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.banReason} onInput={linkEvent(this, this.handleModBanReasonChange)} />
+          </div>
+          <div class="form-group row">
+            <label class="col-form-label">Expires</label>
+            <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} />
+          </div>
+          <div class="form-group row">
+            <button type="submit" class="btn btn-secondary">Ban {this.props.node.comment.creator_name}</button>
+          </div>
+        </form>
+        }
+        {this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} disabled={this.props.locked} />}
+        {this.props.node.children && <CommentNodes nodes={this.props.node.children} locked={this.props.locked} moderators={this.props.moderators}/>}
       </div>
     )
   }
 
-  private get myComment(): boolean {
+  get myComment(): boolean {
     return UserService.Instance.loggedIn && this.props.node.comment.creator_id == UserService.Instance.user.id;
   }
 
+  get canMod(): boolean {
+
+    // You can do moderator actions only on the mods added after you.
+    if (UserService.Instance.loggedIn) {
+      let modIds = this.props.moderators.map(m => m.user_id);
+      let yourIndex = modIds.findIndex(id => id == UserService.Instance.user.id);
+      if (yourIndex == -1) {
+        return false;
+      } else { 
+        console.log(modIds);
+        modIds = modIds.slice(0, yourIndex+1); // +1 cause you cant mod yourself
+        console.log(modIds);
+        return !modIds.includes(this.props.node.comment.creator_id);
+      }
+    } else {
+      return false;
+    }
+
+  }
+
+  get isMod(): boolean {
+    return this.props.moderators.map(m => m.user_id).includes(this.props.node.comment.creator_id);
+  }
+
   handleReplyClick(i: CommentNode) {
     i.state.showReply = true;
     i.setState(i.state);
@@ -113,6 +195,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     let deleteForm: CommentFormI = {
       content: "*deleted*",
       edit_id: i.props.node.comment.id,
+      creator_id: i.props.node.comment.creator_id,
       post_id: i.props.node.comment.post_id,
       parent_id: i.props.node.comment.parent_id,
       auth: null
@@ -145,4 +228,70 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     };
     WebSocketService.Instance.likeComment(form);
   }
+
+  handleModRemoveShow(i: CommentNode) {
+    i.state.showRemoveDialog = true;
+    i.setState(i.state);
+  }
+
+  handleModRemoveReasonChange(i: CommentNode, event: any) {
+    i.state.removeReason = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModRemoveSubmit(i: CommentNode) {
+    let form: CommentFormI = {
+      content: i.props.node.comment.content,
+      edit_id: i.props.node.comment.id,
+      creator_id: i.props.node.comment.creator_id,
+      post_id: i.props.node.comment.post_id,
+      parent_id: i.props.node.comment.parent_id,
+      removed: !i.props.node.comment.removed,
+      reason: i.state.removeReason,
+      auth: null
+    };
+    WebSocketService.Instance.editComment(form);
+
+    i.state.showRemoveDialog = false;
+    i.setState(i.state);
+  }
+
+  handleModBanShow(i: CommentNode) {
+    i.state.showBanDialog = true;
+    i.setState(i.state);
+  }
+
+  handleModBanReasonChange(i: CommentNode, event: any) {
+    i.state.banReason = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModBanExpiresChange(i: CommentNode, event: any) {
+    i.state.banExpires = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModBanSubmit(i: CommentNode) {
+    let form: BanFromCommunityForm = {
+      user_id: i.props.node.comment.creator_id,
+      community_id: i.props.node.comment.community_id,
+      ban: !i.props.node.comment.banned,
+      reason: i.state.banReason,
+      expires: getUnixTime(i.state.banExpires),
+    };
+    WebSocketService.Instance.banFromCommunity(form);
+
+    i.state.showBanDialog = false;
+    i.setState(i.state);
+  }
+
+  handleAddModToCommunity(i: CommentNode) {
+    let form: AddModToCommunityForm = {
+      user_id: i.props.node.comment.creator_id,
+      community_id: i.props.node.comment.community_id,
+      added: !i.isMod,
+    };
+    WebSocketService.Instance.addModToCommunity(form);
+    i.setState(i.state);
+  }
 }
index 76d5c57e37da074efa0a2c7c1571d2591b3b2ba7..19ba30c6704e439fb8c2f24ad6a07de54082d474 100644 (file)
@@ -1,5 +1,5 @@
 import { Component } from 'inferno';
-import { CommentNode as CommentNodeI } from '../interfaces';
+import { CommentNode as CommentNodeI, CommunityUser } from '../interfaces';
 import { CommentNode } from './comment-node';
 
 interface CommentNodesState {
@@ -7,8 +7,10 @@ interface CommentNodesState {
 
 interface CommentNodesProps {
   nodes: Array<CommentNodeI>;
+  moderators: Array<CommunityUser>;
   noIndent?: boolean;
   viewOnly?: boolean;
+  locked?: boolean;
 }
 
 export class CommentNodes extends Component<CommentNodesProps, CommentNodesState> {
@@ -21,10 +23,15 @@ export class CommentNodes extends Component<CommentNodesProps, CommentNodesState
     return (
       <div className="comments">
         {this.props.nodes.map(node =>
-          <CommentNode node={node} noIndent={this.props.noIndent} viewOnly={this.props.viewOnly}/>
+          <CommentNode node={node} 
+            noIndent={this.props.noIndent} 
+            viewOnly={this.props.viewOnly} 
+            locked={this.props.locked} 
+            moderators={this.props.moderators}/>
         )}
       </div>
     )
   }
+
 }
 
index 4d2512cccd53d6671a7725b1ef4abac170e26f1b..f8cce2c00aae0cae03c3b18c7fb5f3bdedc8c177 100644 (file)
@@ -102,7 +102,6 @@ export class Communities extends Component<any, CommunitiesState> {
     WebSocketService.Instance.followCommunity(form);
   }
 
-
   handleSubscribe(communityId: number) {
     let form: FollowCommunityForm = {
       community_id: communityId,
@@ -129,6 +128,6 @@ export class Communities extends Component<any, CommunitiesState> {
       found.subscribed = res.community.subscribed;
       found.number_of_subscribers = res.community.number_of_subscribers;
       this.setState(this.state);
-    }
+    } 
   }
 }
index 13f6f68e17cc9df0c2684beeda17e04e221700c6..cd95f991325043327d621c5836fb336853a7734f 100644 (file)
@@ -63,7 +63,11 @@ export class Community extends Component<any, State> {
         <h4><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
         <div class="row">
           <div class="col-12 col-md-9">
-            <h4>{this.state.community.title}</h4>
+            <h4>{this.state.community.title}
+            {this.state.community.removed &&
+              <small className="ml-2 text-muted font-italic">removed</small>
+            }
+          </h4>
             <PostListings communityId={this.state.communityId} />
           </div>
           <div class="col-12 col-md-3">
diff --git a/ui/src/components/modlog.tsx b/ui/src/components/modlog.tsx
new file mode 100644 (file)
index 0000000..356cbc9
--- /dev/null
@@ -0,0 +1,184 @@
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, GetModlogForm, GetModlogResponse, ModRemovePost, ModLockPost, ModRemoveComment, ModRemoveCommunity, ModBanFromCommunity, ModBan, ModAddCommunity, ModAdd } from '../interfaces';
+import { WebSocketService } from '../services';
+import { msgOp, addTypeInfo } from '../utils';
+import { MomentTime } from './moment-time';
+import * as moment from 'moment';
+
+interface ModlogState {
+  removed_posts: Array<ModRemovePost>,
+  locked_posts: Array<ModLockPost>,
+  removed_comments: Array<ModRemoveComment>,
+  removed_communities: Array<ModRemoveCommunity>,
+  banned_from_community: Array<ModBanFromCommunity>,
+  banned: Array<ModBan>,
+  added_to_community: Array<ModAddCommunity>,
+  added: Array<ModAdd>,
+  loading: boolean;
+}
+
+export class Modlog extends Component<any, ModlogState> {
+  private subscription: Subscription;
+  private emptyState: ModlogState = {
+    removed_posts: [],
+    locked_posts: [],
+    removed_comments: [],
+    removed_communities: [],
+    banned_from_community: [],
+    banned: [],
+    added_to_community: [],
+    added: [],
+    loading: true
+  }
+
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = this.emptyState;
+    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')
+    );
+
+    let modlogForm: GetModlogForm = {
+
+    };
+    WebSocketService.Instance.getModlog(modlogForm);
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  combined() {
+    let combined: Array<{type_: string, data: ModRemovePost | ModLockPost | ModRemoveCommunity}> = [];
+    let removed_posts = addTypeInfo(this.state.removed_posts, "removed_posts");
+    let locked_posts = addTypeInfo(this.state.locked_posts, "locked_posts");
+    let removed_comments = addTypeInfo(this.state.removed_comments, "removed_comments");
+    let removed_communities = addTypeInfo(this.state.removed_communities, "removed_communities");
+    let banned_from_community = addTypeInfo(this.state.banned_from_community, "banned_from_community");
+    let added_to_community = addTypeInfo(this.state.added_to_community, "added_to_community");
+
+    combined.push(...removed_posts);
+    combined.push(...locked_posts);
+    combined.push(...removed_comments);
+    combined.push(...removed_communities);
+    combined.push(...banned_from_community);
+    combined.push(...added_to_community);
+
+    // Sort them by time
+    combined.sort((a, b) => b.data.when_.localeCompare(a.data.when_));
+
+    console.log(combined);
+
+    return (
+      <tbody>
+        {combined.map(i =>
+          <tr>
+            <td><MomentTime data={i.data} /></td>
+            <td><Link to={`/user/${i.data.mod_user_id}`}>{i.data.mod_user_name}</Link></td>
+            <td>
+              {i.type_ == 'removed_posts' && 
+                <>
+                  {(i.data as ModRemovePost).removed? 'Removed' : 'Restored'} 
+                  <span> Post <Link to={`/post/${(i.data as ModRemovePost).post_id}`}>{(i.data as ModRemovePost).post_name}</Link></span>
+                  <div>{(i.data as ModRemovePost).reason && ` reason: ${(i.data as ModRemovePost).reason}`}</div>
+                </>
+              }
+              {i.type_ == 'locked_posts' && 
+                <>
+                  {(i.data as ModLockPost).locked? 'Locked' : 'Unlocked'} 
+                  <span> Post <Link to={`/post/${(i.data as ModLockPost).post_id}`}>{(i.data as ModLockPost).post_name}</Link></span>
+                </>
+              }
+              {i.type_ == 'removed_comments' && 
+                <>
+                  {(i.data as ModRemoveComment).removed? 'Removed' : 'Restored'} 
+                  <span> Comment <Link to={`/post/${(i.data as ModRemoveComment).post_id}/comment/${(i.data as ModRemoveComment).comment_id}`}>{(i.data as ModRemoveComment).comment_content}</Link></span>
+                  <div>{(i.data as ModRemoveComment).reason && ` reason: ${(i.data as ModRemoveComment).reason}`}</div>
+                </>
+              }
+              {i.type_ == 'removed_communities' && 
+                <>
+                  {(i.data as ModRemoveCommunity).removed ? 'Removed' : 'Restored'} 
+                  <span> Community <Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
+                  <div>{(i.data as ModRemoveCommunity).reason && ` reason: ${(i.data as ModRemoveCommunity).reason}`}</div>
+                  <div>{(i.data as ModRemoveCommunity).expires && ` expires: ${moment.utc((i.data as ModRemoveCommunity).expires).fromNow()}`}</div>
+                </>
+              }
+              {i.type_ == 'banned_from_community' && 
+                <>
+                  <span>{(i.data as ModBanFromCommunity).banned ? 'Banned ' : 'Unbanned '} </span>
+                  <span><Link to={`/user/${(i.data as ModBanFromCommunity).other_user_id}`}>{(i.data as ModBanFromCommunity).other_user_name}</Link></span>
+                  <div>{(i.data as ModBanFromCommunity).reason && ` reason: ${(i.data as ModBanFromCommunity).reason}`}</div>
+                  <div>{(i.data as ModBanFromCommunity).expires && ` expires: ${moment.utc((i.data as ModBanFromCommunity).expires).fromNow()}`}</div>
+                </>
+              }
+              {i.type_ == 'added_to_community' && 
+                <>
+                  <span>{(i.data as ModAddCommunity).removed ? 'Removed ' : 'Appointed '} </span>
+                  <span><Link to={`/user/${(i.data as ModAddCommunity).other_user_id}`}>{(i.data as ModAddCommunity).other_user_name}</Link></span>
+                  <span> as a mod to the community </span>
+                  <span><Link to={`/community/${i.data.community_id}`}>{i.data.community_name}</Link></span>
+                </>
+              }
+            </td>
+          </tr>
+                     )
+        }
+
+      </tbody>
+    );
+
+  }
+
+  render() {
+    return (
+      <div class="container">
+        {this.state.loading ? 
+        <h4 class=""><svg class="icon icon-spinner spin"><use xlinkHref="#icon-spinner"></use></svg></h4> : 
+        <div>
+          <h4>Modlog</h4>
+          <div class="table-responsive">
+            <table id="modlog_table" class="table table-sm table-hover">
+              <thead class="pointer">
+                <tr>
+                  <th>Time</th>
+                  <th>Mod</th>
+                  <th>Action</th>
+                </tr>
+              </thead>
+              {this.combined()}
+            </table>
+          </div>
+        </div>
+        }
+      </div>
+    );
+  }
+
+  parseMessage(msg: any) {
+    console.log(msg);
+    let op: UserOperation = msgOp(msg);
+    if (msg.error) {
+      alert(msg.error);
+      return;
+    } else if (op == UserOperation.GetModlog) {
+      let res: GetModlogResponse = msg;
+      this.state.loading = false;
+      this.state.removed_posts = res.removed_posts;
+      this.state.locked_posts = res.locked_posts;
+      this.state.removed_comments = res.removed_comments;
+      this.state.removed_communities = res.removed_communities;
+      this.state.banned_from_community = res.banned_from_community;
+      this.state.added_to_community = res.added_to_community;
+    
+      this.setState(this.state);
+    } 
+  }
+}
index b7402e7ec9c02f41b08a72b884ade8e93a1526e0..c882669530e30103b8055e02a911f34bab776bd3 100644 (file)
@@ -3,7 +3,8 @@ import * as moment from 'moment';
 
 interface MomentTimeProps {
   data: {
-    published: string;
+    published?: string;
+    when_?: string;
     updated?: string;
   }
 }
@@ -20,8 +21,9 @@ export class MomentTime extends Component<MomentTimeProps, any> {
         <span title={this.props.data.updated} className="font-italics">modified {moment.utc(this.props.data.updated).fromNow()}</span>
       )
     } else {
+      let str = this.props.data.published || this.props.data.when_;
       return (
-        <span title={this.props.data.published}>{moment.utc(this.props.data.published).fromNow()}</span>
+        <span title={str}>{moment.utc(str).fromNow()}</span>
       )
     }
   }
index 5c51b699c0fb324b35e2c3fdfb8ece7400b071e9..64bf6e01f5786e664eeae643ec5837b5ee5a6bad 100644 (file)
@@ -56,6 +56,9 @@ export class Navbar extends Component<any, NavbarState> {
             <li class="nav-item">
               <Link class="nav-link" to="/communities">Forums</Link>
             </li>
+            <li class="nav-item">
+              <Link class="nav-link" to="/modlog">Modlog</Link>
+            </li>
             <li class="nav-item">
               <Link class="nav-link" to="/create_post">Create Post</Link>
             </li>
index 67a3f42e082c0b60fe7f2ba3bcf27a199027d373..b2042c1f4f39b68b92c97209af61609650c1d3d6 100644 (file)
@@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PostForm as PostFormI, Post, PostResponse, UserOperation, Community, ListCommunitiesResponse, ListCommunitiesForm, SortType } from '../interfaces';
-import { WebSocketService } from '../services';
+import { WebSocketService, UserService } from '../services';
 import { msgOp } from '../utils';
 import * as autosize from 'autosize';
 
@@ -26,7 +26,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     postForm: {
       name: null,
       auth: null,
-      community_id: null
+      community_id: null,
+      creator_id: UserService.Instance.loggedIn ? UserService.Instance.user.id : null
     },
     communities: [],
     loading: false
@@ -43,6 +44,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
         name: this.props.post.name,
         community_id: this.props.post.community_id,
         edit_id: this.props.post.id,
+        creator_id: this.props.post.creator_id,
         url: this.props.post.url,
         auth: null
       }
index d52dc937ef7d257934c2124a8b4d1c85b2c203cf..d2bfc7bd85a1c9d3c018c839e37ab9fe823272fa 100644 (file)
@@ -8,6 +8,8 @@ import { mdToHtml } from '../utils';
 
 interface PostListingState {
   showEdit: boolean;
+  showRemoveDialog: boolean;
+  removeReason: string;
   iframeExpanded: boolean;
 }
 
@@ -23,6 +25,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
   private emptyState: PostListingState = {
     showEdit: false,
+    showRemoveDialog: false,
+    removeReason: null,
     iframeExpanded: false
   }
 
@@ -59,20 +63,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
         <div className="ml-4">
           {post.url 
             ? <div className="mb-0">
-            <h4 className="d-inline"><a className="text-white" href={post.url}>{post.name}</a></h4>
-            <small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
-            { !this.state.iframeExpanded
-              ? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
-              : 
-              <span>
-                <span class="pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleIframeExpandClick)}>-</span>
-                <div class="embed-responsive embed-responsive-1by1">
-                  <iframe scrolling="yes" class="embed-responsive-item" src={post.url}></iframe>
-                </div>
-              </span>
+            <h4 className="d-inline"><a className="text-white" href={post.url}>{post.name}</a>
+            {post.removed &&
+              <small className="ml-2 text-muted font-italic">removed</small>
+            }
+            {post.locked &&
+              <small className="ml-2 text-muted font-italic">locked</small>
             }
-          </div> 
-            : <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link></h4>
+          </h4>
+          <small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
+          { !this.state.iframeExpanded
+            ? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
+            : 
+            <span>
+              <span class="pointer ml-2 text-muted small" onClick={linkEvent(this, this.handleIframeExpandClick)}>-</span>
+              <div class="embed-responsive embed-responsive-1by1">
+                <iframe scrolling="yes" class="embed-responsive-item" src={post.url}></iframe>
+              </div>
+            </span>
+          }
+        </div> 
+          : <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link>
+          {post.removed &&
+            <small className="ml-2 text-muted font-italic">removed</small>
+          }
+          {post.locked &&
+            <small className="ml-2 text-muted font-italic">locked</small>
+          }
+        </h4>
           }
         </div>
         <div className="details ml-4 mb-1">
@@ -102,16 +120,39 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
               <Link className="text-muted" to={`/post/${post.id}`}>{post.number_of_comments} Comments</Link>
             </li>
           </ul>
-          {this.myPost && 
+          {this.props.editable &&
             <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
-              <li className="list-inline-item">
-                <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-              </li>
-              <li className="list-inline-item">
-                <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
-              </li>
+              {this.myPost && 
+                <span>
+                  <li className="list-inline-item">
+                    <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+                  </li>
+                  <li className="list-inline-item mr-2">
+                    <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span>
+                  </li>
+                </span>
+              }
+              {this.props.post.am_mod &&
+                <span>
+                  <li className="list-inline-item">
+                    {!this.props.post.removed ? 
+                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
+                    <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+                    }
+                  </li>
+                  <li className="list-inline-item">
+                    <span class="pointer" onClick={linkEvent(this, this.handleModLock)}>{this.props.post.locked ? 'unlock' : 'lock'}</span>
+                  </li>
+                </span>
+              }
             </ul>
           }
+          {this.state.showRemoveDialog && 
+            <form class="form-inline" onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+              <input type="text" class="form-control mr-2" placeholder="Reason" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
+              <button type="submit" class="btn btn-secondary">Remove Post</button>
+            </form>
+          }
           {this.props.showBody && this.props.post.body && <div className="md-div" dangerouslySetInnerHTML={mdToHtml(post.body)} />}
         </div>
       </div>
@@ -119,7 +160,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   private get myPost(): boolean {
-    return this.props.editable && UserService.Instance.loggedIn && this.props.post.creator_id == UserService.Instance.user.id;
+    return UserService.Instance.loggedIn && this.props.post.creator_id == UserService.Instance.user.id;
   }
 
   handlePostLike(i: PostListing) {
@@ -162,11 +203,50 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       name: "deleted",
       url: '',
       edit_id: i.props.post.id,
+      creator_id: i.props.post.creator_id,
       auth: null
     };
     WebSocketService.Instance.editPost(deleteForm);
   }
 
+  handleModRemoveShow(i: PostListing) {
+    i.state.showRemoveDialog = true;
+    i.setState(i.state);
+  }
+
+  handleModRemoveReasonChange(i: PostListing, event: any) {
+    i.state.removeReason = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModRemoveSubmit(i: PostListing) {
+    let form: PostFormI = {
+      name: i.props.post.name,
+      community_id: i.props.post.community_id,
+      edit_id: i.props.post.id,
+      creator_id: i.props.post.creator_id,
+      removed: !i.props.post.removed,
+      reason: i.state.removeReason,
+      auth: null,
+    };
+    WebSocketService.Instance.editPost(form);
+
+    i.state.showRemoveDialog = false;
+    i.setState(i.state);
+  }
+
+  handleModLock(i: PostListing) {
+    let form: PostFormI = {
+      name: i.props.post.name,
+      community_id: i.props.post.community_id,
+      edit_id: i.props.post.id,
+      creator_id: i.props.post.creator_id,
+      locked: !i.props.post.locked,
+      auth: null,
+    };
+    WebSocketService.Instance.editPost(form);
+  }
+
   handleIframeExpandClick(i: PostListing) {
     i.state.iframeExpanded = !i.state.iframeExpanded;
     i.setState(i.state);
index 5ca3f77070fad6dc9d6b223cab7b507bdfb90f2d..c65f463d184a11412238b39b9acf6afc0cf20f43 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment,  CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI } from '../interfaces';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment,  CommentResponse, CommentSortType, CreatePostLikeResponse, CommunityUser, CommunityResponse, CommentNode as CommentNodeI, BanFromCommunityResponse, AddModToCommunityResponse } from '../interfaces';
 import { WebSocketService } from '../services';
 import { msgOp, hotRank } from '../utils';
 import { PostListing } from './post-listing';
@@ -82,7 +82,7 @@ export class Post extends Component<any, PostState> {
             <div class="col-12 col-sm-8 col-lg-7 mb-3">
               <PostListing post={this.state.post} showBody showCommunity editable />
               <div className="mb-2" />
-              <CommentForm postId={this.state.post.id} />
+              <CommentForm postId={this.state.post.id} disabled={this.state.post.locked} />
               {this.sortRadios()}
               {this.commentsTree()}
             </div>
@@ -125,7 +125,7 @@ export class Post extends Component<any, PostState> {
       <div class="sticky-top">
         <h4>New Comments</h4>
         {this.state.comments.map(comment => 
-          <CommentNodes nodes={[{comment: comment}]} noIndent />
+          <CommentNodes nodes={[{comment: comment}]} noIndent locked={this.state.post.locked} moderators={this.state.moderators} />
         )}
       </div>
     )
@@ -188,7 +188,7 @@ export class Post extends Component<any, PostState> {
     let nodes = this.buildCommentsTree();
     return (
       <div className="">
-        <CommentNodes nodes={nodes} />
+        <CommentNodes nodes={nodes} locked={this.state.post.locked} moderators={this.state.moderators} />
       </div>
     );
   }
@@ -216,6 +216,11 @@ export class Post extends Component<any, PostState> {
       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.upvotes = res.comment.upvotes;
+      found.downvotes = res.comment.downvotes;
+      found.score = res.comment.score;
+
       this.setState(this.state);
     }
     else if (op == UserOperation.CreateCommentLike) {
@@ -249,6 +254,15 @@ export class Post extends Component<any, PostState> {
       this.state.community.subscribed = res.community.subscribed;
       this.state.community.number_of_subscribers = res.community.number_of_subscribers;
       this.setState(this.state);
+    } else if (op == UserOperation.BanFromCommunity) {
+      let res: BanFromCommunityResponse = msg;
+      this.state.comments.filter(c => c.creator_id == res.user.id)
+      .forEach(c => c.banned = res.banned);
+      this.setState(this.state);
+    } else if (op == UserOperation.AddModToCommunity) {
+      let res: AddModToCommunityResponse = msg;
+      this.state.moderators = res.moderators;
+      this.setState(this.state);
     }
 
   }
index 68fc458b0bab69e0744e7b59d0553549fb6c4422..6640d84a05072201ec9839cc04f2fadd66caa43a 100644 (file)
@@ -1,8 +1,8 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { Community, CommunityUser, FollowCommunityForm } from '../interfaces';
+import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { mdToHtml } from '../utils';
+import { mdToHtml, getUnixTime } from '../utils';
 import { CommunityForm } from './community-form';
 
 interface SidebarProps {
@@ -12,12 +12,18 @@ interface SidebarProps {
 
 interface SidebarState {
   showEdit: boolean;
+  showRemoveDialog: boolean;
+  removeReason: string;
+  removeExpires: string;
 }
 
 export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   private emptyState: SidebarState = {
-    showEdit: false
+    showEdit: false,
+    showRemoveDialog: false,
+    removeReason: null,
+    removeExpires: null
   }
 
   constructor(props: any, context: any) {
@@ -42,44 +48,71 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     let community = this.props.community;
     return (
       <div>
-        <h4 className="mb-0">{community.title}</h4>
-        <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
-        {this.amMod && 
-            <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
-              <li className="list-inline-item">
-                <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
-              </li>
-              {this.amCreator && 
-                <li className="list-inline-item">
-                {/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
-              </li>
-              }
-            </ul>
+        <h4 className="mb-0">{community.title}
+        {community.removed &&
+          <small className="ml-2 text-muted font-italic">removed</small>
+        }
+      </h4>
+      <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
+      {community.am_mod && 
+        <ul class="list-inline mb-1 text-muted small font-weight-bold"> 
+          <li className="list-inline-item">
+            <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
+          </li>
+          {this.amCreator && 
+            <li className="list-inline-item">
+              {/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
+            </li>
           }
-        <ul class="mt-1 list-inline">
-          <li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
-          <li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li>
-          <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
-          <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
+          <li className="list-inline-item">
+            {!this.props.community.removed ? 
+            <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
+            <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
+            }
+          </li>
         </ul>
-        <div>
-          {community.subscribed 
-            ? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
-            : <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
-          }
-        </div>
-        {community.description && 
-          <div>
-            <hr />
-            <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
+      }
+      {this.state.showRemoveDialog && 
+        <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+          <div class="form-group row">
+            <label class="col-form-label">Reason</label>
+            <input type="text" class="form-control mr-2" placeholder="Optional" value={this.state.removeReason} onInput={linkEvent(this, this.handleModRemoveReasonChange)} />
           </div>
+          <div class="form-group row">
+            <label class="col-form-label">Expires</label>
+            <input type="date" class="form-control mr-2" placeholder="Expires" value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} />
+          </div>
+          <div class="form-group row">
+            <button type="submit" class="btn btn-secondary">Remove Community</button>
+          </div>
+        </form>
+      }
+      <ul class="mt-1 list-inline">
+        <li className="list-inline-item"><Link className="badge badge-light" to="/communities">{community.category_name}</Link></li>
+        <li className="list-inline-item badge badge-light">{community.number_of_subscribers} Subscribers</li>
+        <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
+        <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
+      </ul>
+      <div>
+        {community.subscribed 
+          ? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
+          : <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
         }
-        <hr />
-        <h4>Moderators</h4>
+      </div>
+      {community.description && 
+        <div>
+          <hr />
+          <div className="md-div" dangerouslySetInnerHTML={mdToHtml(community.description)} />
+        </div>
+      }
+      <hr />
+      <h4>Moderators</h4>
+      <ul class="list-inline"> 
         {this.props.moderators.map(mod =>
-          <Link to={`/user/${mod.user_id}`}>{mod.user_name}</Link>
+          <li class="list-inline-item"><Link to={`/user/${mod.user_id}`}>{mod.user_name}</Link></li>
         )}
-      </div>
+      </ul>
+    </div>
     );
   }
 
@@ -122,10 +155,48 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id;
   }
 
-  private get amMod(): boolean {
-    console.log(this.props.moderators);
-    console.log(this.props);
-    return UserService.Instance.loggedIn && 
-      this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
+  // private get amMod(): boolean {
+  //   return UserService.Instance.loggedIn && 
+  //     this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
+  // }
+
+  handleDeleteClick() {
+  }
+
+  handleModRemoveShow(i: Sidebar) {
+    i.state.showRemoveDialog = true;
+    i.setState(i.state);
   }
+
+  handleModRemoveReasonChange(i: Sidebar, event: any) {
+    i.state.removeReason = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModRemoveExpiresChange(i: Sidebar, event: any) {
+    console.log(event.target.value);
+    i.state.removeExpires = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleModRemoveSubmit(i: Sidebar) {
+
+    let deleteForm: CommunityFormI = {
+      name: i.props.community.name,
+      title: i.props.community.title,
+      category_id: i.props.community.category_id,
+      edit_id: i.props.community.id,
+      removed: !i.props.community.removed,
+      reason: i.state.removeReason,
+      expires: getUnixTime(i.state.removeExpires),
+      auth: null,
+    };
+    WebSocketService.Instance.editCommunity(deleteForm);
+
+    i.state.showRemoveDialog = false;
+    i.setState(i.state);
+  }
+
+
+
 }
index 5dd3ac6a26573703113ee026b7a67751b27b77c9..b899686a69ba851a1302065e60c541e8ed2ee1cc 100644 (file)
@@ -125,24 +125,27 @@ export class User extends Component<any, UserState> {
   }
 
   overview() {
-    let combined: Array<any> = [];
-    combined.push(...this.state.comments);
-    combined.push(...this.state.posts);
+    let combined: Array<{type_: string, data: Comment | Post}> = [];
+    let comments = this.state.comments.map(e => {return {type_: "comments", data: e}});
+    let posts = this.state.posts.map(e => {return {type_: "posts", data: e}});
+
+    combined.push(...comments);
+    combined.push(...posts);
 
     // Sort it
     if (this.state.sort == SortType.New) {
-      combined.sort((a, b) => b.published.localeCompare(a.published));
+      combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
     } else {
-      combined.sort((a, b) => b.score - a.score);
+      combined.sort((a, b) => b.data.score - a.data.score);
     }
 
     return (
       <div>
         {combined.map(i =>
           <div>
-            {i.community_id 
-              ? <PostListing post={i} showCommunity viewOnly />
-              : <CommentNodes nodes={[{comment: i}]} noIndent viewOnly />
+            {i.type_ == "posts"
+              ? <PostListing post={i.data as Post} showCommunity viewOnly />
+              : <CommentNodes nodes={[{comment: i.data as Comment}]} noIndent viewOnly />
             }
           </div>
                      )
index 608e487a3ebd4d4cedd96447f18695d19c160eac..c2f2c92e096ec2e8f5f2f5f969fef885b908c057 100644 (file)
@@ -10,6 +10,7 @@ import { Post } from './components/post';
 import { Community } from './components/community';
 import { Communities } from './components/communities';
 import { User } from './components/user';
+import { Modlog } from './components/modlog';
 import { Symbols } from './components/symbols';
 
 import './main.css';
@@ -42,6 +43,7 @@ class Index extends Component<any, any> {
             <Route path={`/community/:id`} component={Community} />
             <Route path={`/user/:id/:heading`} component={User} />
             <Route path={`/user/:id`} component={User} />
+            <Route path={`/modlog`} component={Modlog} />
           </Switch>
           <Symbols />
         </div>
index b6139134d4e0ed80a6bf8c851c6bd2f273b601a1..d8a570d11f448177f4523b9330c72d0595a9b94e 100644 (file)
@@ -1,5 +1,17 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetModlog, BanFromCommunity, AddModToCommunity
+}
+
+export enum CommentSortType {
+  Hot, Top, New
+}
+
+export enum ListingType {
+  All, Subscribed, Community
+}
+
+export enum SortType {
+  Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
 }
 
 export interface User {
@@ -31,6 +43,8 @@ export interface CommunityUser {
 export interface Community {
   user_id?: number;
   subscribed?: boolean;
+  am_mod?: boolean;
+  removed?: boolean;
   id: number;
   name: string;
   title: string;
@@ -46,12 +60,258 @@ export interface Community {
   updated?: string;
 }
 
+export interface Post {
+  user_id?: number;
+  my_vote?: number;
+  am_mod?: boolean;
+  removed?: boolean;
+  locked?: boolean;
+  id: number;
+  name: string;
+  url?: string;
+  body?: string;
+  creator_id: number;
+  creator_name: string;
+  community_id: number;
+  community_name: string;
+  number_of_comments: number;
+  score: number;
+  upvotes: number;
+  downvotes: number;
+  hot_rank: number;
+  published: string;
+  updated?: string;
+}
+
+export interface Comment {
+  id: number;
+  content: string;
+  creator_id: number;
+  creator_name: string;
+  post_id: number,
+  community_id: number,
+  parent_id?: number;
+  published: string;
+  updated?: string;
+  score: number;
+  upvotes: number;
+  downvotes: number;
+  my_vote?: number;
+  am_mod?: boolean;
+  removed?: boolean;
+  banned?: boolean;
+}
+
+export interface Category {
+  id: number;
+  name: string;
+}
+
+export interface FollowCommunityForm {
+  community_id: number;
+  follow: boolean;
+  auth?: string;
+}
+
+export interface GetFollowedCommunitiesResponse {
+  op: string;
+  communities: Array<CommunityUser>;
+}
+
+export interface GetUserDetailsForm {
+  user_id: number;
+  sort: string; // TODO figure this one out
+  limit: number;
+  community_id?: number;
+  auth?: string;
+}
+
+export interface UserDetailsResponse {
+  op: string;
+  user: UserView;
+  follows: Array<CommunityUser>;
+  moderates: Array<CommunityUser>;
+  comments: Array<Comment>;
+  posts: Array<Post>;
+  saved?: Array<Post>;
+}
+
+export interface BanFromCommunityForm {
+  community_id: number;
+  user_id: number;
+  ban: boolean;
+  reason?: string,
+  expires?: number,
+  auth?: string;
+}
+
+export interface BanFromCommunityResponse {
+  op: string;
+  user: UserView,
+  banned: boolean,
+}
+
+export interface AddModToCommunityForm {
+  community_id: number;
+  user_id: number;
+  added: boolean;
+  auth?: string;
+}
+
+export interface AddModToCommunityResponse {
+  op: string;
+  moderators: Array<CommunityUser>;
+}
+
+export interface GetModlogForm {
+  mod_user_id?: number;
+  community_id?: number;
+  limit?: number;
+  page?: number;
+}
+
+export interface GetModlogResponse {
+  op: string;
+  removed_posts: Array<ModRemovePost>,
+  locked_posts: Array<ModLockPost>,
+  removed_comments: Array<ModRemoveComment>,
+  removed_communities: Array<ModRemoveCommunity>,
+  banned_from_community: Array<ModBanFromCommunity>,
+  banned: Array<ModBan>,
+  added_to_community: Array<ModAddCommunity>,
+  added: Array<ModAdd>,
+}
+
+export interface ModRemovePost {
+    id: number;
+    mod_user_id: number;
+    post_id: number;
+    reason?: string;
+    removed?: boolean;
+    when_: string
+    mod_user_name: string;
+    post_name: string;
+    community_id: number;
+    community_name: string;
+}
+
+export interface ModLockPost {
+  id: number,
+  mod_user_id: number,
+  post_id: number,
+  locked?: boolean,
+  when_: string,
+  mod_user_name: string,
+  post_name: string,
+  community_id: number,
+  community_name: string,
+}
+
+export interface ModRemoveComment {
+  id: number,
+  mod_user_id: number,
+  comment_id: number,
+  reason?: string,
+  removed?: boolean,
+  when_: string,
+  mod_user_name: string,
+  comment_user_id: number,
+  comment_user_name: string,
+  comment_content: string,
+  post_id: number,
+  post_name: string,
+  community_id: number,
+  community_name: string,
+}
+
+export interface ModRemoveCommunity {
+  id: number,
+  mod_user_id: number,
+  community_id: number,
+  reason?: string,
+  removed?: boolean,
+  expires?: number,
+  when_: string,
+  mod_user_name: string,
+  community_name: string,
+}
+
+export interface ModBanFromCommunity {
+  id: number,
+  mod_user_id: number,
+  other_user_id: number,
+  community_id: number,
+  reason?: string,
+  banned?: boolean,
+  expires?: number,
+  when_: string,
+  mod_user_name: string,
+  other_user_name: string,
+  community_name: string,
+}
+
+export interface ModBan {
+  id: number,
+  mod_user_id: number,
+  other_user_id: number,
+  reason?: string,
+  banned?: boolean,
+  expires?: number,
+  when_: string,
+  mod_user_name: string,
+  other_user_name: string,
+}
+
+export interface ModAddCommunity {
+  id: number,
+  mod_user_id: number,
+  other_user_id: number,
+  community_id: number,
+  removed?: boolean,
+  when_: string,
+  mod_user_name: string,
+  other_user_name: string,
+  community_name: string,
+}
+
+export interface ModAdd {
+  id: number,
+  mod_user_id: number,
+  other_user_id: number,
+  removed?: boolean,
+  when_: string,
+  mod_user_name: string,
+  other_user_name: string,
+}
+
+export interface LoginForm {
+  username_or_email: string;
+  password: string;
+}
+
+export interface RegisterForm {
+  username: string;
+  email?: string;
+  password: string;
+  password_verify: string;
+}
+
+export interface LoginResponse {
+  op: string;
+  jwt: string;
+}
+
+
+
 export interface CommunityForm {
   name: string;
   title: string;
   description?: string,
   category_id: number,
   edit_id?: number;
+  removed?: boolean;
+  reason?: string;
+  expires?: number;
   auth?: string;
 }
 
@@ -82,27 +342,6 @@ export interface ListCategoriesResponse {
   op: string;
   categories: Array<Category>;
 }
-
-export interface Post {
-  user_id?: number;
-  my_vote?: number;
-  id: number;
-  name: string;
-  url?: string;
-  body?: string;
-  creator_id: number;
-  creator_name: string;
-  community_id: number;
-  community_name: string;
-  number_of_comments: number;
-  score: number;
-  upvotes: number;
-  downvotes: number;
-  hot_rank: number;
-  published: string;
-  updated?: string;
-}
-
 export interface PostForm {
   name: string;
   url?: string;
@@ -110,6 +349,10 @@ export interface PostForm {
   community_id: number;
   updated?: number;
   edit_id?: number;
+  creator_id: number;
+  removed?: boolean;
+  reason?: string;
+  locked?: boolean;
   auth: string;
 }
 
@@ -126,26 +369,14 @@ export interface PostResponse {
   post: Post;
 }
 
-export interface Comment {
-  id: number;
-  content: string;
-  creator_id: number;
-  creator_name: string;
-  post_id: number,
-  parent_id?: number;
-  published: string;
-  updated?: string;
-  score: number;
-  upvotes: number;
-  downvotes: number;
-  my_vote?: number;
-}
-
 export interface CommentForm {
   content: string;
   post_id: number;
   parent_id?: number;
   edit_id?: number;
+  creator_id: number;
+  removed?: boolean;
+  reason?: string;
   auth: string;
 }
 
@@ -190,70 +421,4 @@ export interface CreatePostLikeResponse {
   post: Post;
 }
 
-export interface Category {
-  id: number;
-  name: string;
-}
-
-export interface FollowCommunityForm {
-  community_id: number;
-  follow: boolean;
-  auth?: string;
-}
-
-export interface GetFollowedCommunitiesResponse {
-  op: string;
-  communities: Array<CommunityUser>;
-}
-
-export interface GetUserDetailsForm {
-  user_id: number;
-  sort: string; // TODO figure this one out
-  limit: number;
-  community_id?: number;
-  auth?: string;
-}
-
-
-
-export interface UserDetailsResponse {
-  op: string;
-  user: UserView;
-  follows: Array<CommunityUser>;
-  moderates: Array<CommunityUser>;
-  comments: Array<Comment>;
-  posts: Array<Post>;
-  saved?: Array<Post>;
-}
-
-
-export interface LoginForm {
-  username_or_email: string;
-  password: string;
-}
-
-export interface RegisterForm {
-  username: string;
-  email?: string;
-  password: string;
-  password_verify: string;
-}
-
-
-export interface LoginResponse {
-  op: string;
-  jwt: string;
-}
-
-export enum CommentSortType {
-  Hot, Top, New
-}
-
-export enum ListingType {
-  All, Subscribed, Community
-}
-
-export enum SortType {
-  Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll
-}
 
index ae817baaa408471397152036d72d055ea80e3698..9e3e78f9919c0964fee2d9122aafea0c8a6dff02 100644 (file)
@@ -24,6 +24,11 @@ body {
   color: #fff;
 }
 
+.form-control:disabled {
+  background-color: var(--secondary);
+  opacity: .5;
+}
+
 .custom-select {
   color: #fff;
   background-color: var(--secondary);
index 99d65adfacc0df776efc6958c8d3213f63489e8a..3596bb46e99f3ce2becd8bba872ffa4ff56cb432 100644 (file)
@@ -1,5 +1,5 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm, GetUserDetailsForm, ListCommunitiesForm, GetModlogForm, BanFromCommunityForm, AddModToCommunityForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -106,11 +106,25 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.EditPost, postForm));
   }
 
+  public banFromCommunity(form: BanFromCommunityForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.BanFromCommunity, form));
+  }
+
+  public addModToCommunity(form: AddModToCommunityForm) {
+    this.setAuth(form);
+    this.subject.next(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
+  }
+
   public getUserDetails(form: GetUserDetailsForm) {
     this.setAuth(form, false);
     this.subject.next(this.wsSendWrapper(UserOperation.GetUserDetails, form));
   }
 
+  public getModlog(form: GetModlogForm) {
+    this.subject.next(this.wsSendWrapper(UserOperation.GetModlog, form));
+  }
+
   private wsSendWrapper(op: UserOperation, data: any) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
index e01e4ce11ae182f8282ab2bf68f3b69c657fe93a..4bd0d90dfc9acd1da8282010202cfa4861ba3893 100644 (file)
@@ -31,3 +31,11 @@ export function hotRank(comment: Comment): number {
 export function mdToHtml(text: string) {
   return {__html: md.render(text)};
 }
+
+export function getUnixTime(text: string): number { 
+  return text ? new Date(text).getTime()/1000 : undefined;
+}
+
+export function addTypeInfo<T>(arr: Array<T>, name: string): Array<{type_: string, data: T}> {  
+  return arr.map(e => {return {type_: name, data: e}});
+}
index c06ed5dd02285495e04295b7a9d08311f539a50f..03cbc5b5ce6f436f4d546f773ba004ba45798ecf 100644 (file)
@@ -1 +1 @@
-export let version: string = "v0.0.2-9-g8e5a5d1";
\ No newline at end of file
+export let version: string = "v0.0.2-13-g1bf0dfd";
\ No newline at end of file