Merge pull request #997 from LemmyNet/api_edit_separation

Api edit separation.
This commit is contained in:
Felix Ableitner 2020-07-22 22:33:39 +02:00 committed by GitHub
commit 227f397d5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2451 additions and 1087 deletions

View file

@ -17,6 +17,7 @@
- [Errors](#errors) - [Errors](#errors)
- [API documentation](#api-documentation) - [API documentation](#api-documentation)
* [Sort Types](#sort-types) * [Sort Types](#sort-types)
* [Undoing actions](#undoing-actions)
* [Websocket vs HTTP](#websocket-vs-http) * [Websocket vs HTTP](#websocket-vs-http)
* [User / Authentication / Admin actions](#user--authentication--admin-actions) * [User / Authentication / Admin actions](#user--authentication--admin-actions)
+ [Login](#login) + [Login](#login)
@ -43,142 +44,198 @@
- [Request](#request-5) - [Request](#request-5)
- [Response](#response-5) - [Response](#response-5)
- [HTTP](#http-6) - [HTTP](#http-6)
+ [Edit User Mention](#edit-user-mention) + [Mark User Mention as read](#mark-user-mention-as-read)
- [Request](#request-6) - [Request](#request-6)
- [Response](#response-6) - [Response](#response-6)
- [HTTP](#http-7) - [HTTP](#http-7)
+ [Mark All As Read](#mark-all-as-read) + [Get Private Messages](#get-private-messages)
- [Request](#request-7) - [Request](#request-7)
- [Response](#response-7) - [Response](#response-7)
- [HTTP](#http-8) - [HTTP](#http-8)
+ [Delete Account](#delete-account) + [Create Private Message](#create-private-message)
- [Request](#request-8) - [Request](#request-8)
- [Response](#response-8) - [Response](#response-8)
- [HTTP](#http-9) - [HTTP](#http-9)
+ [Add admin](#add-admin) + [Edit Private Message](#edit-private-message)
- [Request](#request-9) - [Request](#request-9)
- [Response](#response-9) - [Response](#response-9)
- [HTTP](#http-10) - [HTTP](#http-10)
+ [Ban user](#ban-user) + [Delete Private Message](#delete-private-message)
- [Request](#request-10) - [Request](#request-10)
- [Response](#response-10) - [Response](#response-10)
- [HTTP](#http-11) - [HTTP](#http-11)
* [Site](#site) + [Mark Private Message as Read](#mark-private-message-as-read)
+ [List Categories](#list-categories)
- [Request](#request-11) - [Request](#request-11)
- [Response](#response-11) - [Response](#response-11)
- [HTTP](#http-12) - [HTTP](#http-12)
+ [Search](#search) + [Mark All As Read](#mark-all-as-read)
- [Request](#request-12) - [Request](#request-12)
- [Response](#response-12) - [Response](#response-12)
- [HTTP](#http-13) - [HTTP](#http-13)
+ [Get Modlog](#get-modlog) + [Delete Account](#delete-account)
- [Request](#request-13) - [Request](#request-13)
- [Response](#response-13) - [Response](#response-13)
- [HTTP](#http-14) - [HTTP](#http-14)
+ [Create Site](#create-site) + [Add admin](#add-admin)
- [Request](#request-14) - [Request](#request-14)
- [Response](#response-14) - [Response](#response-14)
- [HTTP](#http-15) - [HTTP](#http-15)
+ [Edit Site](#edit-site) + [Ban user](#ban-user)
- [Request](#request-15) - [Request](#request-15)
- [Response](#response-15) - [Response](#response-15)
- [HTTP](#http-16) - [HTTP](#http-16)
+ [Get Site](#get-site) * [Site](#site)
+ [List Categories](#list-categories)
- [Request](#request-16) - [Request](#request-16)
- [Response](#response-16) - [Response](#response-16)
- [HTTP](#http-17) - [HTTP](#http-17)
+ [Transfer Site](#transfer-site) + [Search](#search)
- [Request](#request-17) - [Request](#request-17)
- [Response](#response-17) - [Response](#response-17)
- [HTTP](#http-18) - [HTTP](#http-18)
+ [Get Site Config](#get-site-config) + [Get Modlog](#get-modlog)
- [Request](#request-18) - [Request](#request-18)
- [Response](#response-18) - [Response](#response-18)
- [HTTP](#http-19) - [HTTP](#http-19)
+ [Save Site Config](#save-site-config) + [Create Site](#create-site)
- [Request](#request-19) - [Request](#request-19)
- [Response](#response-19) - [Response](#response-19)
- [HTTP](#http-20) - [HTTP](#http-20)
* [Community](#community) + [Edit Site](#edit-site)
+ [Get Community](#get-community)
- [Request](#request-20) - [Request](#request-20)
- [Response](#response-20) - [Response](#response-20)
- [HTTP](#http-21) - [HTTP](#http-21)
+ [Create Community](#create-community) + [Get Site](#get-site)
- [Request](#request-21) - [Request](#request-21)
- [Response](#response-21) - [Response](#response-21)
- [HTTP](#http-22) - [HTTP](#http-22)
+ [List Communities](#list-communities) + [Transfer Site](#transfer-site)
- [Request](#request-22) - [Request](#request-22)
- [Response](#response-22) - [Response](#response-22)
- [HTTP](#http-23) - [HTTP](#http-23)
+ [Ban from Community](#ban-from-community) + [Get Site Config](#get-site-config)
- [Request](#request-23) - [Request](#request-23)
- [Response](#response-23) - [Response](#response-23)
- [HTTP](#http-24) - [HTTP](#http-24)
+ [Add Mod to Community](#add-mod-to-community) + [Save Site Config](#save-site-config)
- [Request](#request-24) - [Request](#request-24)
- [Response](#response-24) - [Response](#response-24)
- [HTTP](#http-25) - [HTTP](#http-25)
+ [Edit Community](#edit-community) * [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-25) - [Request](#request-25)
- [Response](#response-25) - [Response](#response-25)
- [HTTP](#http-26) - [HTTP](#http-26)
+ [Follow Community](#follow-community) + [Create Community](#create-community)
- [Request](#request-26) - [Request](#request-26)
- [Response](#response-26) - [Response](#response-26)
- [HTTP](#http-27) - [HTTP](#http-27)
+ [Get Followed Communities](#get-followed-communities) + [List Communities](#list-communities)
- [Request](#request-27) - [Request](#request-27)
- [Response](#response-27) - [Response](#response-27)
- [HTTP](#http-28) - [HTTP](#http-28)
+ [Transfer Community](#transfer-community) + [Ban from Community](#ban-from-community)
- [Request](#request-28) - [Request](#request-28)
- [Response](#response-28) - [Response](#response-28)
- [HTTP](#http-29) - [HTTP](#http-29)
* [Post](#post) + [Add Mod to Community](#add-mod-to-community)
+ [Create Post](#create-post)
- [Request](#request-29) - [Request](#request-29)
- [Response](#response-29) - [Response](#response-29)
- [HTTP](#http-30) - [HTTP](#http-30)
+ [Get Post](#get-post) + [Edit Community](#edit-community)
- [Request](#request-30) - [Request](#request-30)
- [Response](#response-30) - [Response](#response-30)
- [HTTP](#http-31) - [HTTP](#http-31)
+ [Get Posts](#get-posts) + [Delete Community](#delete-community)
- [Request](#request-31) - [Request](#request-31)
- [Response](#response-31) - [Response](#response-31)
- [HTTP](#http-32) - [HTTP](#http-32)
+ [Create Post Like](#create-post-like) + [Remove Community](#remove-community)
- [Request](#request-32) - [Request](#request-32)
- [Response](#response-32) - [Response](#response-32)
- [HTTP](#http-33) - [HTTP](#http-33)
+ [Edit Post](#edit-post) + [Follow Community](#follow-community)
- [Request](#request-33) - [Request](#request-33)
- [Response](#response-33) - [Response](#response-33)
- [HTTP](#http-34) - [HTTP](#http-34)
+ [Save Post](#save-post) + [Get Followed Communities](#get-followed-communities)
- [Request](#request-34) - [Request](#request-34)
- [Response](#response-34) - [Response](#response-34)
- [HTTP](#http-35) - [HTTP](#http-35)
* [Comment](#comment) + [Transfer Community](#transfer-community)
+ [Create Comment](#create-comment)
- [Request](#request-35) - [Request](#request-35)
- [Response](#response-35) - [Response](#response-35)
- [HTTP](#http-36) - [HTTP](#http-36)
+ [Edit Comment](#edit-comment) * [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-36) - [Request](#request-36)
- [Response](#response-36) - [Response](#response-36)
- [HTTP](#http-37) - [HTTP](#http-37)
+ [Save Comment](#save-comment) + [Get Post](#get-post)
- [Request](#request-37) - [Request](#request-37)
- [Response](#response-37) - [Response](#response-37)
- [HTTP](#http-38) - [HTTP](#http-38)
+ [Create Comment Like](#create-comment-like) + [Get Posts](#get-posts)
- [Request](#request-38) - [Request](#request-38)
- [Response](#response-38) - [Response](#response-38)
- [HTTP](#http-39) - [HTTP](#http-39)
+ [Create Post Like](#create-post-like)
- [Request](#request-39)
- [Response](#response-39)
- [HTTP](#http-40)
+ [Edit Post](#edit-post)
- [Request](#request-40)
- [Response](#response-40)
- [HTTP](#http-41)
+ [Delete Post](#delete-post)
- [Request](#request-41)
- [Response](#response-41)
- [HTTP](#http-42)
+ [Remove Post](#remove-post)
- [Request](#request-42)
- [Response](#response-42)
- [HTTP](#http-43)
+ [Lock Post](#lock-post)
- [Request](#request-43)
- [Response](#response-43)
- [HTTP](#http-44)
+ [Sticky Post](#sticky-post)
- [Request](#request-44)
- [Response](#response-44)
- [HTTP](#http-45)
+ [Save Post](#save-post)
- [Request](#request-45)
- [Response](#response-45)
- [HTTP](#http-46)
* [Comment](#comment)
+ [Create Comment](#create-comment)
- [Request](#request-46)
- [Response](#response-46)
- [HTTP](#http-47)
+ [Edit Comment](#edit-comment)
- [Request](#request-47)
- [Response](#response-47)
- [HTTP](#http-48)
+ [Delete Comment](#delete-comment)
- [Request](#request-48)
- [Response](#response-48)
- [HTTP](#http-49)
+ [Remove Comment](#remove-comment)
- [Request](#request-49)
- [Response](#response-49)
- [HTTP](#http-50)
+ [Mark Comment as Read](#mark-comment-as-read)
- [Request](#request-50)
- [Response](#response-50)
- [HTTP](#http-51)
+ [Save Comment](#save-comment)
- [Request](#request-51)
- [Response](#response-51)
- [HTTP](#http-52)
+ [Create Comment Like](#create-comment-like)
- [Request](#request-52)
- [Response](#response-52)
- [HTTP](#http-53)
* [RSS / Atom feeds](#rss--atom-feeds) * [RSS / Atom feeds](#rss--atom-feeds)
+ [All](#all) + [All](#all)
+ [Community](#community-1) + [Community](#community-1)
@ -281,6 +338,10 @@ These go wherever there is a `sort` field. The available sort types are:
- `TopYear` - the most upvoted posts/communities of the current year. - `TopYear` - the most upvoted posts/communities of the current year.
- `TopAll` - the most upvoted posts/communities on the current instance. - `TopAll` - the most upvoted posts/communities on the current instance.
### Undoing actions
Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`.
### Websocket vs HTTP ### Websocket vs HTTP
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`. - Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
@ -464,14 +525,17 @@ Only the first user will be able to be the admin.
`GET /user/mentions` `GET /user/mentions`
#### Edit User Mention #### Mark User Mention as read
Only the recipient can do this.
##### Request ##### Request
```rust ```rust
{ {
op: "EditUserMention", op: "MarkUserMentionAsRead",
data: { data: {
user_mention_id: i32, user_mention_id: i32,
read: Option<bool>, read: bool,
auth: String, auth: String,
} }
} }
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
##### Response ##### Response
```rust ```rust
{ {
op: "EditUserMention", op: "MarkUserMentionAsRead",
data: { data: {
mention: UserMentionView, mention: UserMentionView,
} }
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
``` ```
##### HTTP ##### HTTP
`PUT /user/mention` `POST /user/mention/mark_as_read`
#### Get Private Messages
##### Request
```rust
{
op: "GetPrivateMessages",
data: {
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
auth: String,
}
}
```
##### Response
```rust
{
op: "GetPrivateMessages",
data: {
messages: Vec<PrivateMessageView>,
}
}
```
##### HTTP
`GET /private_message/list`
#### Create Private Message
##### Request
```rust
{
op: "CreatePrivateMessage",
data: {
content: String,
recipient_id: i32,
auth: String,
}
}
```
##### Response
```rust
{
op: "CreatePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message`
#### Edit Private Message
##### Request
```rust
{
op: "EditPrivateMessage",
data: {
edit_id: i32,
content: String,
auth: String,
}
}
```
##### Response
```rust
{
op: "EditPrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`PUT /private_message`
#### Delete Private Message
##### Request
```rust
{
op: "DeletePrivateMessage",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeletePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/delete`
#### Mark Private Message as Read
Only the recipient can do this.
##### Request
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
edit_id: i32,
read: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/mark_as_read`
#### Mark All As Read #### Mark All As Read
@ -856,7 +1054,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
data: { data: {
community: CommunityView, community: CommunityView,
moderators: Vec<CommunityModeratorView>, moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
} }
} }
``` ```
@ -973,7 +1170,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /community/mod` `POST /community/mod`
#### Edit Community #### Edit Community
Mods and admins can remove and lock a community, creators can delete it. Only mods can edit a community.
##### Request ##### Request
```rust ```rust
@ -984,10 +1181,6 @@ Mods and admins can remove and lock a community, creators can delete it.
title: String, title: String,
description: Option<String>, description: Option<String>,
category_id: i32, category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
expires: Option<i64>,
auth: String auth: String
} }
} }
@ -1005,6 +1198,62 @@ Mods and admins can remove and lock a community, creators can delete it.
`PUT /community` `PUT /community`
#### Delete Community
Only a creator can delete a community
##### Request
```rust
{
op: "DeleteCommunity",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeleteCommunity",
data: {
community: CommunityView
}
}
```
##### HTTP
`POST /community/delete`
#### Remove Community
Only admins can remove a community.
##### Request
```rust
{
op: "RemoveCommunity",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemoveCommunity",
data: {
community: CommunityView
}
}
```
##### HTTP
`POST /community/remove`
#### Follow Community #### Follow Community
##### Request ##### Request
```rust ```rust
@ -1090,8 +1339,9 @@ Mods and admins can remove and lock a community, creators can delete it.
name: String, name: String,
url: Option<String>, url: Option<String>,
body: Option<String>, body: Option<String>,
nsfw: bool,
community_id: i32, community_id: i32,
auth: String auth: String,
} }
} }
``` ```
@ -1128,7 +1378,6 @@ Mods and admins can remove and lock a community, creators can delete it.
comments: Vec<CommentView>, comments: Vec<CommentView>,
community: CommunityView, community: CommunityView,
moderators: Vec<CommunityModeratorView>, moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
} }
} }
``` ```
@ -1197,25 +1446,17 @@ Post listing types are `All, Subscribed, Community`
`POST /post/like` `POST /post/like`
#### Edit Post #### Edit Post
Mods and admins can remove and lock a post, creators can delete it.
##### Request ##### Request
```rust ```rust
{ {
op: "EditPost", op: "EditPost",
data: { data: {
edit_id: i32, edit_id: i32,
creator_id: i32,
community_id: i32,
name: String, name: String,
url: Option<String>, url: Option<String>,
body: Option<String>, body: Option<String>,
removed: Option<bool>, nsfw: bool,
deleted: Option<bool>, auth: String,
locked: Option<bool>,
reason: Option<String>,
auth: String
} }
} }
``` ```
@ -1233,6 +1474,120 @@ Mods and admins can remove and lock a post, creators can delete it.
`PUT /post` `PUT /post`
#### Delete Post
##### Request
```rust
{
op: "DeletePost",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeletePost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/delete`
#### Remove Post
Only admins and mods can remove a post.
##### Request
```rust
{
op: "RemovePost",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemovePost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/remove`
#### Lock Post
Only admins and mods can lock a post.
##### Request
```rust
{
op: "LockPost",
data: {
edit_id: i32,
locked: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "LockPost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/lock`
#### Sticky Post
Only admins and mods can sticky a post.
##### Request
```rust
{
op: "StickyPost",
data: {
edit_id: i32,
stickied: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "StickyPost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/sticky`
#### Save Post #### Save Post
##### Request ##### Request
```rust ```rust
@ -1267,8 +1622,8 @@ Mods and admins can remove and lock a post, creators can delete it.
data: { data: {
content: String, content: String,
parent_id: Option<i32>, parent_id: Option<i32>,
edit_id: Option<i32>,
post_id: i32, post_id: i32,
form_id: Option<String>, // An optional form id, so you know which message came back
auth: String auth: String
} }
} }
@ -1289,7 +1644,7 @@ Mods and admins can remove and lock a post, creators can delete it.
#### Edit Comment #### Edit Comment
Mods and admins can remove a comment, creators can delete it. Only the creator can edit the comment.
##### Request ##### Request
```rust ```rust
@ -1297,15 +1652,9 @@ Mods and admins can remove a comment, creators can delete it.
op: "EditComment", op: "EditComment",
data: { data: {
content: String, content: String,
parent_id: Option<i32>,
edit_id: i32, edit_id: i32,
creator_id: i32, form_id: Option<String>,
post_id: i32, auth: String,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
read: Option<bool>,
auth: String
} }
} }
``` ```
@ -1322,6 +1671,92 @@ Mods and admins can remove a comment, creators can delete it.
`PUT /comment` `PUT /comment`
#### Delete Comment
Only the creator can delete the comment.
##### Request
```rust
{
op: "DeleteComment",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeleteComment",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/delete`
#### Remove Comment
Only a mod or admin can remove the comment.
##### Request
```rust
{
op: "RemoveComment",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemoveComment",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/remove`
#### Mark Comment as Read
Only the recipient can do this.
##### Request
```rust
{
op: "MarkCommentAsRead",
data: {
edit_id: i32,
read: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "MarkCommentAsRead",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/mark_as_read`
#### Save Comment #### Save Comment
##### Request ##### Request
```rust ```rust
@ -1357,7 +1792,6 @@ Mods and admins can remove a comment, creators can delete it.
op: "CreateCommentLike", op: "CreateCommentLike",
data: { data: {
comment_id: i32, comment_id: i32,
post_id: i32,
score: i16, score: i16,
auth: String auth: String
} }

2
server/Cargo.lock generated vendored
View file

@ -1559,7 +1559,9 @@ dependencies = [
"bcrypt", "bcrypt",
"chrono", "chrono",
"diesel", "diesel",
"lazy_static",
"log", "log",
"regex",
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
"sha2", "sha2",

View file

@ -14,3 +14,5 @@ log = "0.4.0"
sha2 = "0.9" sha2 = "0.9"
bcrypt = "0.8.0" bcrypt = "0.8.0"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
lazy_static = "1.3.0"
regex = "1.3.5"

View file

@ -97,14 +97,6 @@ impl Comment {
comment.filter(ap_id.eq(object_id)).first::<Self>(conn) comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
} }
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(true))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
@ -116,6 +108,46 @@ impl Comment {
)) ))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
comment_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
comment_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
comment_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
naive_now,
schema::{community, community_follower, community_moderator, community_user_ban}, schema::{community, community_follower, community_moderator, community_user_ban},
Bannable, Bannable,
Crud, Crud,
@ -29,7 +30,6 @@ pub struct Community {
pub last_refreshed_at: chrono::NaiveDateTime, pub last_refreshed_at: chrono::NaiveDateTime,
} }
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)] #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"] #[table_name = "community"]
pub struct CommunityForm { pub struct CommunityForm {
@ -99,6 +99,60 @@ impl Community {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
community.filter(local.eq(true)).load::<Community>(conn) community.filter(local.eq(true)).load::<Community>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
community_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
community_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_creator(
conn: &PgConnection,
community_id: i32,
new_creator_id: i32,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, Error> {
use crate::{community_view::CommunityModeratorView, user_view::UserView};
let mut mods_and_admins: Vec<i32> = Vec::new();
mods_and_admins.append(
&mut CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())?,
);
mods_and_admins
.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
Ok(mods_and_admins)
}
pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
Self::community_mods_and_admins(conn, community_id)
.unwrap_or_default()
.contains(&user_id)
}
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]

View file

@ -295,18 +295,18 @@ pub struct CommunityModeratorView {
} }
impl CommunityModeratorView { impl CommunityModeratorView {
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> { pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
use super::community_view::community_moderator_view::dsl::*; use super::community_view::community_moderator_view::dsl::*;
community_moderator_view community_moderator_view
.filter(community_id.eq(from_community_id)) .filter(community_id.eq(for_community_id))
.order_by(published) .order_by(published)
.load::<Self>(conn) .load::<Self>(conn)
} }
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> { pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
use super::community_view::community_moderator_view::dsl::*; use super::community_view::community_moderator_view::dsl::*;
community_moderator_view community_moderator_view
.filter(user_id.eq(from_user_id)) .filter(user_id.eq(for_user_id))
.order_by(published) .order_by(published)
.load::<Self>(conn) .load::<Self>(conn)
} }

View file

@ -2,9 +2,12 @@
pub extern crate diesel; pub extern crate diesel;
#[macro_use] #[macro_use]
pub extern crate strum_macros; pub extern crate strum_macros;
#[macro_use]
pub extern crate lazy_static;
pub extern crate bcrypt; pub extern crate bcrypt;
pub extern crate chrono; pub extern crate chrono;
pub extern crate log; pub extern crate log;
pub extern crate regex;
pub extern crate serde; pub extern crate serde;
pub extern crate serde_json; pub extern crate serde_json;
pub extern crate sha2; pub extern crate sha2;
@ -12,6 +15,7 @@ pub extern crate strum;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{env, env::VarError}; use std::{env, env::VarError};
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc() chrono::prelude::Utc::now().naive_utc()
} }
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::fuzzy_search; use super::fuzzy_search;
use crate::get_database_url_from_env; use crate::{get_database_url_from_env, is_email_regex};
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection};
pub fn establish_unpooled_connection() -> PgConnection { pub fn establish_unpooled_connection() -> PgConnection {
@ -194,4 +207,10 @@ mod tests {
let test = "This is a fuzzy search"; let test = "This is a fuzzy search";
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string()); assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
} }
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
} }

View file

@ -108,6 +108,50 @@ impl Post {
)) ))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
post_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
post_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(locked.eq(new_locked))
.get_result::<Self>(conn)
}
pub fn update_stickied(
conn: &PgConnection,
post_id: i32,
new_stickied: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(stickied.eq(new_stickied))
.get_result::<Self>(conn)
}
pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
user_id == post_creator_id
}
} }
impl Crud<PostForm> for Post { impl Crud<PostForm> for Post {

View file

@ -1,4 +1,4 @@
use crate::{schema::private_message, Crud}; use crate::{naive_now, schema::private_message, Crud};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -80,6 +80,50 @@ impl PrivateMessage {
.filter(ap_id.eq(object_id)) .filter(ap_id.eq(object_id))
.first::<Self>(conn) .first::<Self>(conn)
} }
pub fn update_content(
conn: &PgConnection,
private_message_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
private_message_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_read(
conn: &PgConnection,
private_message_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(
private_message
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
} }
#[cfg(test)] #[cfg(test)]
@ -180,6 +224,10 @@ mod tests {
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap(); let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
let updated_private_message = let updated_private_message =
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap(); PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
let deleted_private_message =
PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
let marked_read_private_message =
PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap();
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap(); let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
User_::delete(&conn, inserted_creator.id).unwrap(); User_::delete(&conn, inserted_creator.id).unwrap();
User_::delete(&conn, inserted_recipient.id).unwrap(); User_::delete(&conn, inserted_recipient.id).unwrap();
@ -187,6 +235,8 @@ mod tests {
assert_eq!(expected_private_message, read_private_message); assert_eq!(expected_private_message, read_private_message);
assert_eq!(expected_private_message, updated_private_message); assert_eq!(expected_private_message, updated_private_message);
assert_eq!(expected_private_message, inserted_private_message); assert_eq!(expected_private_message, inserted_private_message);
assert!(deleted_private_message.deleted);
assert!(marked_read_private_message.read);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }
} }

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
is_email_regex,
naive_now, naive_now,
schema::{user_, user_::dsl::*}, schema::{user_, user_::dsl::*},
Crud, Crud,
@ -125,9 +126,18 @@ impl User_ {
use crate::schema::user_::dsl::*; use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn) user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
} }
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<Self, Error> {
if is_email_regex(username_or_email) {
Self::find_by_email(conn, username_or_email)
} else {
Self::find_by_username(conn, username_or_email)
}
} }
impl User_ {
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> { pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
user_.filter(name.eq(username)).first::<User_>(conn) user_.filter(name.eq(username)).first::<User_>(conn)
} }

View file

@ -52,6 +52,30 @@ impl Crud<UserMentionForm> for UserMention {
} }
} }
impl UserMention {
pub fn update_read(
conn: &PgConnection,
user_mention_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(user_mention.find(user_mention_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(
user_mention
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View file

@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
DateTime::<FixedOffset>::from_utc(datetime, *now.offset()) DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
} }
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn remove_slurs(test: &str) -> String { pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string() SLUR_REGEX.replace_all(test, "*removed*").to_string()
} }
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
is_email_regex,
is_valid_community_name, is_valid_community_name,
is_valid_post_title, is_valid_post_title,
is_valid_username, is_valid_username,
@ -185,12 +180,6 @@ mod tests {
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string()); assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
} }
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test] #[test]
fn test_valid_register_username() { fn test_valid_register_username() {
assert!(is_valid_username("Hello_98")); assert!(is_valid_username("Hello_98"));

View file

@ -1,7 +1,7 @@
use diesel::{result::Error, PgConnection}; use diesel::{result::Error, PgConnection};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud}; use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings}; use lemmy_utils::settings::Settings;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
type Jwt = String; type Jwt = String;
@ -54,18 +54,6 @@ impl Claims {
.unwrap() .unwrap()
} }
// TODO: move these into user?
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<User_, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> { pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims; let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id) User_::read(&conn, claims.id)

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
websocket::{ websocket::{
@ -15,12 +15,10 @@ use lemmy_db::{
comment_view::*, comment_view::*,
community_view::*, community_view::*,
moderator::*, moderator::*,
naive_now,
post::*, post::*,
site_view::*, site_view::*,
user::*, user::*,
user_mention::*, user_mention::*,
user_view::*,
Crud, Crud,
Likeable, Likeable,
ListingType, ListingType,
@ -44,22 +42,38 @@ use std::str::FromStr;
pub struct CreateComment { pub struct CreateComment {
content: String, content: String,
parent_id: Option<i32>, parent_id: Option<i32>,
edit_id: Option<i32>, // TODO this isn't used
pub post_id: i32, pub post_id: i32,
form_id: Option<String>,
auth: String, auth: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditComment { pub struct EditComment {
content: String, content: String,
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
edit_id: i32, edit_id: i32,
creator_id: i32, form_id: Option<String>,
pub post_id: i32, auth: String,
removed: Option<bool>, }
deleted: Option<bool>,
#[derive(Serialize, Deserialize)]
pub struct DeleteComment {
edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemoveComment {
edit_id: i32,
removed: bool,
reason: Option<String>, reason: Option<String>,
read: Option<bool>, auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkCommentAsRead {
edit_id: i32,
read: bool,
auth: String, auth: String,
} }
@ -74,12 +88,12 @@ pub struct SaveComment {
pub struct CommentResponse { pub struct CommentResponse {
pub comment: CommentView, pub comment: CommentView,
pub recipient_ids: Vec<i32>, pub recipient_ids: Vec<i32>,
pub form_id: Option<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct CreateCommentLike { pub struct CreateCommentLike {
comment_id: i32, comment_id: i32,
pub post_id: i32,
score: i16, score: i16,
auth: String, auth: String,
} }
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Check if post is locked, no new comments
if post.locked {
return Err(APIError::err("locked").into());
}
// Create the comment
let comment_form2 = comment_form.clone(); let comment_form2 = comment_form.clone();
let inserted_comment = let inserted_comment =
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? { match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
@ -157,6 +177,7 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id; let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| { let updated_comment: Comment = match blocking(pool, move |conn| {
let apub_id = let apub_id =
@ -175,8 +196,15 @@ impl Perform for Oper<CreateComment> {
// Scan the comment for user mentions, add those rows // Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content); let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = let recipient_ids = send_local_notifs(
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?; mentions,
updated_comment.clone(),
user.clone(),
post,
pool,
true,
)
.await?;
// You like your own comment by default // You like your own comment by default
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
@ -201,6 +229,7 @@ impl Perform for Oper<CreateComment> {
let mut res = CommentResponse { let mut res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: data.form_id.to_owned(),
}; };
if let Some(ws) = websocket_info { if let Some(ws) = websocket_info {
@ -237,37 +266,14 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id; let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let edit_id = data.edit_id; let edit_id = data.edit_id;
let orig_comment = let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??; blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
let mut editors: Vec<i32> = vec![orig_comment.creator_id]; // Check for a site ban
let mut moderators: Vec<i32> = vec![]; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
let community_id = orig_comment.community_id; return Err(APIError::err("site_ban").into());
moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
editors.extend(&moderators);
// You are allowed to mark the comment as read even if you're banned.
if data.read.is_none() {
// Verify its the creator or a mod, or an admin
if !editors.contains(&user_id) {
return Err(APIError::err("no_comment_edit_allowed").into());
} }
// Check for a community ban // Check for a community ban
@ -278,12 +284,308 @@ impl Perform for Oper<EditComment> {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Verify that only the creator can edit
if user_id != orig_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Do the update
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let edit_id = data.edit_id;
let updated_comment = match blocking(pool, move |conn| {
Comment::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Send the apub update
updated_comment
.send_update(&user, &self.client, pool)
.await?;
// Do the mentions / recipients
let post_id = orig_comment.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let updated_comment_content = updated_comment.content.to_owned();
let mentions = scrape_text_for_mentions(&updated_comment_content);
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::EditComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban // Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the creator can delete
if user_id != orig_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Do the delete
let deleted = data.deleted;
let updated_comment = match blocking(pool, move |conn| {
Comment::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Send the apub message
if deleted {
updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else { } else {
// check that user can mark as read updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = vec![];
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::DeleteComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemoveComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only a mod or admin can remove
is_mod_or_admin(pool, user_id, community_id).await?;
// Do the remove
let removed = data.removed;
let updated_comment = match blocking(pool, move |conn| {
Comment::update_removed(conn, edit_id, removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Mod tables
let form = ModRemoveCommentForm {
mod_user_id: user_id,
comment_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
// Send the apub message
if removed {
updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = vec![];
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::RemoveComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<MarkCommentAsRead> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the recipient can mark as read
// Needs to fetch the parent comment / post to get the recipient
let parent_id = orig_comment.parent_id; let parent_id = orig_comment.parent_id;
match parent_id { match parent_id {
Some(pid) => { Some(pid) => {
@ -301,137 +603,27 @@ impl Perform for Oper<EditComment> {
} }
} }
} }
}
let content_slurs_removed = remove_slurs(&data.content.to_owned()); // Do the mark as read
let read = data.read;
let edit_id = data.edit_id; match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? {
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
let comment_form = {
if data.read.is_none() {
// the ban etc checks should been made and have passed
// the comment can be properly edited
let post_removed = if moderators.contains(&user_id) {
data.removed
} else {
Some(read_comment.removed)
};
CommentForm {
content: content_slurs_removed,
parent_id: read_comment.parent_id,
post_id: read_comment.post_id,
creator_id: read_comment.creator_id,
removed: post_removed.to_owned(),
deleted: data.deleted.to_owned(),
read: Some(read_comment.read),
published: None,
updated: Some(naive_now()),
ap_id: read_comment.ap_id,
local: read_comment.local,
}
} else {
// the only field that can be updated it the read field
CommentForm {
content: read_comment.content,
parent_id: read_comment.parent_id,
post_id: read_comment.post_id,
creator_id: read_comment.creator_id,
removed: Some(read_comment.removed).to_owned(),
deleted: Some(read_comment.deleted).to_owned(),
read: data.read.to_owned(),
published: None,
updated: orig_comment.updated,
ap_id: read_comment.ap_id,
local: read_comment.local,
}
}
};
let edit_id = data.edit_id;
let comment_form2 = comment_form.clone();
let updated_comment = match blocking(pool, move |conn| {
Comment::update(conn, edit_id, &comment_form2)
})
.await?
{
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
}; };
if data.read.is_none() { // Refetch it
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if moderators.contains(&user_id) {
if removed {
updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
}
}
} else {
updated_comment
.send_update(&user, &self.client, pool)
.await?;
}
// Mod tables
if moderators.contains(&user_id) {
if let Some(removed) = data.removed.to_owned() {
let form = ModRemoveCommentForm {
mod_user_id: user_id,
comment_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
}
}
}
let post_id = data.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
let edit_id = data.edit_id; let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| { let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id)) CommentView::read(conn, edit_id, Some(user_id))
}) })
.await??; .await??;
let mut res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids: Vec::new(),
form_id: None,
}; };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::EditComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res) Ok(res)
} }
} }
@ -480,6 +672,7 @@ impl Perform for Oper<SaveComment> {
Ok(CommentResponse { Ok(CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids: Vec::new(), recipient_ids: Vec::new(),
form_id: None,
}) })
} }
} }
@ -512,8 +705,12 @@ impl Perform for Oper<CreateCommentLike> {
} }
} }
let comment_id = data.comment_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??;
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post_id = orig_comment.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id; let community_id = post.community_id;
let is_banned = let is_banned =
@ -550,7 +747,7 @@ impl Perform for Oper<CreateCommentLike> {
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
comment_id: data.comment_id, comment_id: data.comment_id,
post_id: data.post_id, post_id,
user_id, user_id,
score: data.score, score: data.score,
}; };
@ -587,6 +784,7 @@ impl Perform for Oper<CreateCommentLike> {
let mut res = CommentResponse { let mut res = CommentResponse {
comment: liked_comment, comment: liked_comment,
recipient_ids, recipient_ids,
form_id: None,
}; };
if let Some(ws) = websocket_info { if let Some(ws) = websocket_info {
@ -675,9 +873,10 @@ pub async fn send_local_notifs(
user: User_, user: User_,
post: Post, post: Post,
pool: &DbPool, pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<i32>, LemmyError> { ) -> Result<Vec<i32>, LemmyError> {
let ids = blocking(pool, move |conn| { let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &user, &post) do_send_local_notifs(conn, &mentions, &comment, &user, &post, do_send_email)
}) })
.await?; .await?;
@ -690,6 +889,7 @@ fn do_send_local_notifs(
comment: &Comment, comment: &Comment,
user: &User_, user: &User_,
post: &Post, post: &Post,
do_send_email: bool,
) -> Vec<i32> { ) -> Vec<i32> {
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
let hostname = &format!("https://{}", Settings::get().hostname); let hostname = &format!("https://{}", Settings::get().hostname);
@ -720,7 +920,7 @@ fn do_send_local_notifs(
}; };
// Send an email to those users that have notifications on // Send an email to those users that have notifications on
if mention_user.send_notifications_to_email { if do_send_email && mention_user.send_notifications_to_email {
if let Some(mention_email) = mention_user.email { if let Some(mention_email) = mention_user.email {
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,); let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
let html = &format!( let html = &format!(
@ -744,7 +944,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) { if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
recipient_ids.push(parent_user.id); recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email { if do_send_email && parent_user.send_notifications_to_email {
if let Some(comment_reply_email) = parent_user.email { if let Some(comment_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!( let html = &format!(
@ -767,7 +967,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, post.creator_id) { if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
recipient_ids.push(parent_user.id); recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email { if do_send_email && parent_user.send_notifications_to_email {
if let Some(post_reply_email) = parent_user.email { if let Some(post_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!( let html = &format!(

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
apub::ActorType, apub::ActorType,
blocking, blocking,
websocket::{ websocket::{
@ -34,7 +34,6 @@ pub struct GetCommunity {
pub struct GetCommunityResponse { pub struct GetCommunityResponse {
pub community: CommunityView, pub community: CommunityView,
pub moderators: Vec<CommunityModeratorView>, pub moderators: Vec<CommunityModeratorView>,
pub admins: Vec<UserView>,
pub online: usize, pub online: usize,
} }
@ -101,9 +100,21 @@ pub struct EditCommunity {
title: String, title: String,
description: Option<String>, description: Option<String>,
category_id: i32, category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool, nsfw: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeleteCommunity {
pub edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemoveCommunity {
pub edit_id: i32,
removed: bool,
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String, auth: String,
@ -184,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let site_creator_id = site.creator_id;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let online = if let Some(ws) = websocket_info { let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id { if let Some(id) = ws.id {
ws.chatserver.do_send(JoinCommunityRoom { ws.chatserver.do_send(JoinCommunityRoom {
@ -212,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
let res = GetCommunityResponse { let res = GetCommunityResponse {
community: community_view, community: community_view,
moderators, moderators,
admins,
online, online,
}; };
@ -366,24 +369,15 @@ impl Perform for Oper<EditCommunity> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Verify its a mod // Verify its a mod (only mods can edit it)
let edit_id = data.edit_id; let edit_id = data.edit_id;
let mut editors: Vec<i32> = Vec::new(); let mods: Vec<i32> = blocking(pool, move |conn| {
editors.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, edit_id) CommunityModeratorView::for_community(conn, edit_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect()) .map(|v| v.into_iter().map(|m| m.user_id).collect())
}) })
.await??, .await??;
); if !mods.contains(&user_id) {
editors.append( return Err(APIError::err("not_a_moderator").into());
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !editors.contains(&user_id) {
return Err(APIError::err("no_community_edit_allowed").into());
} }
let edit_id = data.edit_id; let edit_id = data.edit_id;
@ -395,8 +389,8 @@ impl Perform for Oper<EditCommunity> {
description: data.description.to_owned(), description: data.description.to_owned(),
category_id: data.category_id.to_owned(), category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id, creator_id: read_community.creator_id,
removed: data.removed.to_owned(), removed: Some(read_community.removed),
deleted: data.deleted.to_owned(), deleted: Some(read_community.deleted),
nsfw: data.nsfw, nsfw: data.nsfw,
updated: Some(naive_now()), updated: Some(naive_now()),
actor_id: read_community.actor_id, actor_id: read_community.actor_id,
@ -408,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
}; };
let edit_id = data.edit_id; let edit_id = data.edit_id;
let updated_community = match blocking(pool, move |conn| { match blocking(pool, move |conn| {
Community::update(conn, edit_id, &community_form) Community::update(conn, edit_id, &community_form)
}) })
.await? .await?
@ -417,23 +411,69 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
}; };
// Mod tables // TODO there needs to be some kind of an apub update
if let Some(removed) = data.removed.to_owned() { // process for communities and users
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)), let edit_id = data.edit_id;
None => None, let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse {
community: community_view,
}; };
let form = ModRemoveCommunityForm {
mod_user_id: user_id, send_community_websocket(&res, websocket_info, UserOperation::EditCommunity);
community_id: data.edit_id,
removed: Some(removed), Ok(res)
reason: data.reason.to_owned(), }
expires,
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
} }
if let Some(deleted) = data.deleted.to_owned() { #[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Verify its the creator (only a creator can delete the community)
let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
if read_community.creator_id != user_id {
return Err(APIError::err("no_community_edit_allowed").into());
}
// Do the delete
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_community = match blocking(pool, move |conn| {
Community::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Send apub messages
if deleted { if deleted {
updated_community updated_community
.send_delete(&user, &self.client, pool) .send_delete(&user, &self.client, pool)
@ -443,17 +483,6 @@ impl Perform for Oper<EditCommunity> {
.send_undo_delete(&user, &self.client, pool) .send_undo_delete(&user, &self.client, pool)
.await?; .await?;
} }
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
}
let edit_id = data.edit_id; let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| { let community_view = blocking(pool, move |conn| {
@ -465,19 +494,87 @@ impl Perform for Oper<EditCommunity> {
community: community_view, community: community_view,
}; };
if let Some(ws) = websocket_info { send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
ws.chatserver.do_send(SendCommunityRoomMessage { Ok(res)
op: UserOperation::EditCommunity,
response: res_sent,
community_id: data.edit_id,
my_id: ws.id,
});
} }
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemoveCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Verify its an admin (only an admin can remove a community)
is_admin(pool, user_id).await?;
// Do the remove
let edit_id = data.edit_id;
let removed = data.removed;
let updated_community = match blocking(pool, move |conn| {
Community::update_removed(conn, edit_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_user_id: user_id,
community_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
// Apub messages
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse {
community: community_view,
};
send_community_websocket(&res, websocket_info, UserOperation::RemoveCommunity);
Ok(res) Ok(res)
} }
@ -654,27 +751,10 @@ impl Perform for Oper<BanFromCommunity> {
let user_id = claims.id; let user_id = claims.id;
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id; let community_id = data.community_id;
community_moderators.append( // Verify that only mods or admins can ban
&mut blocking(pool, move |conn| { is_mod_or_admin(pool, user_id, community_id).await?;
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
let community_user_ban_form = CommunityUserBanForm { let community_user_ban_form = CommunityUserBanForm {
community_id: data.community_id, community_id: data.community_id,
@ -694,6 +774,7 @@ impl Perform for Oper<BanFromCommunity> {
} }
// Mod tables // Mod tables
// TODO eventually do correct expires
let expires = match data.expires { let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)), Some(time) => Some(naive_from_unix(time)),
None => None, None => None,
@ -753,27 +834,10 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id; let community_id = data.community_id;
community_moderators.append( // Verify that only mods or admins can add mod
&mut blocking(pool, move |conn| { is_mod_or_admin(pool, user_id, community_id).await?;
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
if data.added { if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
@ -852,26 +916,9 @@ impl Perform for Oper<TransferCommunity> {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
let community_form = CommunityForm {
name: read_community.name,
title: read_community.title,
description: read_community.description,
category_id: read_community.category_id,
creator_id: data.user_id, // This makes the new user the community creator
removed: None,
deleted: None,
nsfw: read_community.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
};
let community_id = data.community_id; let community_id = data.community_id;
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form); let new_creator = data.user_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(pool, update).await?.is_err() { if blocking(pool, update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into()); return Err(APIError::err("couldnt_update_community").into());
}; };
@ -941,8 +988,27 @@ impl Perform for Oper<TransferCommunity> {
Ok(GetCommunityResponse { Ok(GetCommunityResponse {
community: community_view, community: community_view,
moderators, moderators,
admins,
online: 0, online: 0,
}) })
} }
} }
pub fn send_community_websocket(
res: &CommunityResponse,
websocket_info: Option<WebsocketInfo>,
op: UserOperation,
) {
if let Some(ws) = websocket_info {
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
ws.chatserver.do_send(SendCommunityRoomMessage {
op,
response: res_sent,
community_id: res.community.id,
my_id: ws.id,
});
}
}

View file

@ -1,6 +1,14 @@
use crate::{websocket::WebsocketInfo, DbPool, LemmyError}; use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError};
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*}; use lemmy_db::{
community::*,
community_view::*,
moderator::*,
site::*,
user::*,
user_view::*,
Crud,
};
pub mod claims; pub mod claims;
pub mod comment; pub mod comment;
@ -44,3 +52,25 @@ pub trait Perform {
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<Self::Response, LemmyError>; ) -> Result<Self::Response, LemmyError>;
} }
pub async fn is_mod_or_admin(
pool: &DbPool,
user_id: i32,
community_id: i32,
) -> Result<(), LemmyError> {
let is_mod_or_admin = blocking(pool, move |conn| {
Community::is_mod_or_admin(conn, user_id, community_id)
})
.await?;
if !is_mod_or_admin {
return Err(APIError::err("not_an_admin").into());
}
Ok(())
}
pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
Ok(())
}

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking, blocking,
fetch_iframely_and_pictrs_data, fetch_iframely_and_pictrs_data,
@ -18,10 +18,8 @@ use lemmy_db::{
naive_now, naive_now,
post::*, post::*,
post_view::*, post_view::*,
site::*,
site_view::*, site_view::*,
user::*, user::*,
user_view::*,
Crud, Crud,
Likeable, Likeable,
ListingType, ListingType,
@ -66,7 +64,6 @@ pub struct GetPostResponse {
comments: Vec<CommentView>, comments: Vec<CommentView>,
community: CommunityView, community: CommunityView,
moderators: Vec<CommunityModeratorView>, moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
pub online: usize, pub online: usize,
} }
@ -96,20 +93,42 @@ pub struct CreatePostLike {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditPost { pub struct EditPost {
pub edit_id: i32, pub edit_id: i32,
creator_id: i32,
community_id: i32,
name: String, name: String,
url: Option<String>, url: Option<String>,
body: Option<String>, body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool, nsfw: bool,
locked: Option<bool>, auth: String,
stickied: Option<bool>, }
#[derive(Serialize, Deserialize)]
pub struct DeletePost {
pub edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemovePost {
pub edit_id: i32,
removed: bool,
reason: Option<String>, reason: Option<String>,
auth: String, auth: String,
} }
#[derive(Serialize, Deserialize)]
pub struct LockPost {
pub edit_id: i32,
locked: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct StickyPost {
pub edit_id: i32,
stickied: bool,
auth: String,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SavePost { pub struct SavePost {
post_id: i32, post_id: i32,
@ -311,14 +330,6 @@ impl Perform for Oper<GetPost> {
}) })
.await??; .await??;
let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let online = if let Some(ws) = websocket_info { let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id { if let Some(id) = ws.id {
ws.chatserver.do_send(JoinPostRoom { ws.chatserver.do_send(JoinPostRoom {
@ -343,7 +354,6 @@ impl Perform for Oper<GetPost> {
comments, comments,
community, community,
moderators, moderators,
admins,
online, online,
}) })
} }
@ -549,35 +559,10 @@ impl Perform for Oper<EditPost> {
let user_id = claims.id; let user_id = claims.id;
let edit_id = data.edit_id; let edit_id = data.edit_id;
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Verify its the creator or a mod or admin
let community_id = read_post.community_id;
let mut editors: Vec<i32> = vec![read_post.creator_id];
let mut moderators: Vec<i32> = vec![];
moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
editors.extend(&moderators);
if !editors.contains(&user_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Check for a community ban // Check for a community ban
let community_id = read_post.community_id; let community_id = orig_post.community_id;
let is_banned = let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok(); move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? { if blocking(pool, is_banned).await? {
@ -590,55 +575,34 @@ impl Perform for Oper<EditPost> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Verify that only the creator can edit
if !Post::is_post_creator(user_id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Fetch Iframely and Pictrs cached image // Fetch Iframely and Pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = { let post_form = PostForm {
// only modify some properties if they are a moderator
if moderators.contains(&user_id) {
PostForm {
name: data.name.trim().to_owned(), name: data.name.trim().to_owned(),
url: data.url.to_owned(), url: data.url.to_owned(),
body: data.body.to_owned(), body: data.body.to_owned(),
creator_id: read_post.creator_id.to_owned(),
community_id: read_post.community_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw, nsfw: data.nsfw,
locked: data.locked.to_owned(), creator_id: orig_post.creator_id.to_owned(),
stickied: data.stickied.to_owned(), community_id: orig_post.community_id,
removed: Some(orig_post.removed),
deleted: Some(orig_post.deleted),
locked: Some(orig_post.locked),
stickied: Some(orig_post.stickied),
updated: Some(naive_now()), updated: Some(naive_now()),
embed_title: iframely_title, embed_title: iframely_title,
embed_description: iframely_description, embed_description: iframely_description,
embed_html: iframely_html, embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail, thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id, ap_id: orig_post.ap_id,
local: read_post.local, local: orig_post.local,
published: None, published: None,
}
} else {
PostForm {
name: read_post.name.trim().to_owned(),
url: data.url.to_owned(),
body: data.body.to_owned(),
creator_id: read_post.creator_id.to_owned(),
community_id: read_post.community_id,
removed: Some(read_post.removed),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
locked: Some(read_post.locked),
stickied: Some(read_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id,
local: read_post.local,
published: None,
}
}
}; };
let edit_id = data.edit_id; let edit_id = data.edit_id;
@ -656,58 +620,8 @@ impl Perform for Oper<EditPost> {
} }
}; };
if moderators.contains(&user_id) { // Send apub update
// Mod tables
if let Some(removed) = data.removed.to_owned() {
let form = ModRemovePostForm {
mod_user_id: user_id,
post_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
}
if let Some(locked) = data.locked.to_owned() {
let form = ModLockPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
locked: Some(locked),
};
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
}
if let Some(stickied) = data.stickied.to_owned() {
let form = ModStickyPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
stickied: Some(stickied),
};
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
}
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_post.send_delete(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if moderators.contains(&user_id) {
if removed {
updated_post.send_remove(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
}
}
} else {
updated_post.send_update(&user, &self.client, pool).await?; updated_post.send_update(&user, &self.client, pool).await?;
}
let edit_id = data.edit_id; let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| { let post_view = blocking(pool, move |conn| {
@ -729,6 +643,324 @@ impl Perform for Oper<EditPost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeletePost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &DeletePost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the creator can delete
if !Post::is_post_creator(user_id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Update the post
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_post = blocking(pool, move |conn| {
Post::update_deleted(conn, edit_id, deleted)
})
.await??;
// apub updates
if deleted {
updated_post.send_delete(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::DeletePost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemovePost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &RemovePost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can remove
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let removed = data.removed;
let updated_post = blocking(pool, move |conn| {
Post::update_removed(conn, edit_id, removed)
})
.await??;
// Mod tables
let form = ModRemovePostForm {
mod_user_id: user_id,
post_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
// apub updates
if removed {
updated_post.send_remove(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::RemovePost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<LockPost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &LockPost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can lock
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let locked = data.locked;
let updated_post =
blocking(pool, move |conn| Post::update_locked(conn, edit_id, locked)).await??;
// Mod tables
let form = ModLockPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
locked: Some(locked),
};
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
updated_post.send_update(&user, &self.client, pool).await?;
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::LockPost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<StickyPost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can sticky
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let stickied = data.stickied;
let updated_post = blocking(pool, move |conn| {
Post::update_stickied(conn, edit_id, stickied)
})
.await??;
// Mod tables
let form = ModStickyPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
stickied: Some(stickied),
};
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post.send_update(&user, &self.client, pool).await?;
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::StickyPost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl Perform for Oper<SavePost> { impl Perform for Oper<SavePost> {
type Response = PostResponse; type Response = PostResponse;

View file

@ -1,6 +1,6 @@
use super::user::Register; use super::user::Register;
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking, blocking,
version, version,
@ -257,10 +257,7 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; is_admin(pool, user_id).await?;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
let site_form = SiteForm { let site_form = SiteForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -311,10 +308,7 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; is_admin(pool, user_id).await?;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
@ -693,12 +687,7 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
// Only let admins read this // Only let admins read this
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; is_admin(pool, user_id).await?;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) {
return Err(APIError::err("not_an_admin").into());
}
let config_hjson = Settings::read_config_file()?; let config_hjson = Settings::read_config_file()?;

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::ApubObjectType, apub::ApubObjectType,
blocking, blocking,
websocket::{ websocket::{
@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>, moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>, comments: Vec<CommentView>,
posts: Vec<PostView>, posts: Vec<PostView>,
admins: Vec<UserView>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -174,9 +173,9 @@ pub struct GetUserMentions {
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditUserMention { pub struct MarkUserMentionAsRead {
user_mention_id: i32, user_mention_id: i32,
read: Option<bool>, read: bool,
auth: String, auth: String,
} }
@ -216,9 +215,21 @@ pub struct CreatePrivateMessage {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditPrivateMessage { pub struct EditPrivateMessage {
edit_id: i32, edit_id: i32,
content: Option<String>, content: String,
deleted: Option<bool>, auth: String,
read: Option<bool>, }
#[derive(Serialize, Deserialize)]
pub struct DeletePrivateMessage {
edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkPrivateMessageAsRead {
edit_id: i32,
read: bool,
auth: String, auth: String,
} }
@ -264,7 +275,7 @@ impl Perform for Oper<Login> {
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let user = match blocking(pool, move |conn| { let user = match blocking(pool, move |conn| {
Claims::find_by_email_or_username(conn, &username_or_email) User_::find_by_email_or_username(conn, &username_or_email)
}) })
.await? .await?
{ {
@ -631,14 +642,6 @@ impl Perform for Oper<GetUserDetails> {
}) })
.await??; .await??;
let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
// If its not the same user, remove the email, and settings // If its not the same user, remove the email, and settings
// TODO an if let chain would be better here, but can't figure it out // TODO an if let chain would be better here, but can't figure it out
// TODO separate out settings into its own thing // TODO separate out settings into its own thing
@ -653,7 +656,6 @@ impl Perform for Oper<GetUserDetails> {
moderates, moderates,
comments, comments,
posts, posts,
admins,
}) })
} }
} }
@ -677,10 +679,7 @@ impl Perform for Oper<AddAdmin> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); is_admin(pool, user_id).await?;
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
let added = data.added; let added = data.added;
let added_user_id = data.user_id; let added_user_id = data.user_id;
@ -739,10 +738,7 @@ impl Perform for Oper<BanUser> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); is_admin(pool, user_id).await?;
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
let ban = data.ban; let ban = data.ban;
let banned_user_id = data.user_id; let banned_user_id = data.user_id;
@ -862,7 +858,7 @@ impl Perform for Oper<GetUserMentions> {
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl Perform for Oper<EditUserMention> { impl Perform for Oper<MarkUserMentionAsRead> {
type Response = UserMentionResponse; type Response = UserMentionResponse;
async fn perform( async fn perform(
@ -870,7 +866,7 @@ impl Perform for Oper<EditUserMention> {
pool: &DbPool, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<UserMentionResponse, LemmyError> { ) -> Result<UserMentionResponse, LemmyError> {
let data: &EditUserMention = &self.data; let data: &MarkUserMentionAsRead = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
@ -887,15 +883,9 @@ impl Perform for Oper<EditUserMention> {
return Err(APIError::err("couldnt_update_comment").into()); return Err(APIError::err("couldnt_update_comment").into());
} }
let user_mention_form = UserMentionForm {
recipient_id: read_user_mention.recipient_id,
comment_id: read_user_mention.comment_id,
read: data.read.to_owned(),
};
let user_mention_id = read_user_mention.id; let user_mention_id = read_user_mention.id;
let update_mention = let read = data.read;
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form); let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
if blocking(pool, update_mention).await?.is_err() { if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into()); return Err(APIError::err("couldnt_update_comment").into());
}; };
@ -940,71 +930,27 @@ impl Perform for Oper<MarkAllAsRead> {
.await??; .await??;
// TODO: this should probably be a bulk operation // TODO: this should probably be a bulk operation
// Not easy to do as a bulk operation,
// because recipient_id isn't in the comment table
for reply in &replies { for reply in &replies {
let reply_id = reply.id; let reply_id = reply.id;
let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id); let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
if blocking(pool, mark_as_read).await?.is_err() { if blocking(pool, mark_as_read).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into()); return Err(APIError::err("couldnt_update_comment").into());
} }
} }
// Mentions // Mark all user mentions as read
let mentions = blocking(pool, move |conn| { let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
UserMentionQueryBuilder::create(conn, user_id) if blocking(pool, update_user_mentions).await?.is_err() {
.unread_only(true)
.page(1)
.limit(999)
.list()
})
.await??;
// TODO: this should probably be a bulk operation
for mention in &mentions {
let mention_form = UserMentionForm {
recipient_id: mention.to_owned().recipient_id,
comment_id: mention.to_owned().id,
read: Some(true),
};
let user_mention_id = mention.user_mention_id;
let update_mention =
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form);
if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into()); return Err(APIError::err("couldnt_update_comment").into());
} }
}
// messages // Mark all private_messages as read
let messages = blocking(pool, move |conn| { let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
PrivateMessageQueryBuilder::create(conn, user_id)
.page(1)
.limit(999)
.unread_only(true)
.list()
})
.await??;
// TODO: this should probably be a bulk operation
for message in &messages {
let private_message_form = PrivateMessageForm {
content: message.to_owned().content,
creator_id: message.to_owned().creator_id,
recipient_id: message.to_owned().recipient_id,
deleted: None,
read: Some(true),
updated: None,
ap_id: message.to_owned().ap_id,
local: message.local,
published: None,
};
let message_id = message.id;
let update_pm =
move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
if blocking(pool, update_pm).await?.is_err() { if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into()); return Err(APIError::err("couldnt_update_private_message").into());
} }
}
Ok(GetRepliesResponse { replies: vec![] }) Ok(GetRepliesResponse { replies: vec![] })
} }
@ -1293,59 +1239,25 @@ impl Perform for Oper<EditPrivateMessage> {
let user_id = claims.id; let user_id = claims.id;
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
// Check for a site ban // Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Check to make sure they are the creator (or the recipient marking as read // Checking permissions
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id) let edit_id = data.edit_id;
|| orig_private_message.creator_id.eq(&user_id)) let orig_private_message =
{ blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into()); return Err(APIError::err("no_private_message_edit_allowed").into());
} }
let content_slurs_removed = match &data.content { // Doing the update
Some(content) => remove_slurs(content), let content_slurs_removed = remove_slurs(&data.content);
None => orig_private_message.content.clone(),
};
let private_message_form = {
if data.read.is_some() {
PrivateMessageForm {
content: orig_private_message.content.to_owned(),
creator_id: orig_private_message.creator_id,
recipient_id: orig_private_message.recipient_id,
read: data.read.to_owned(),
updated: orig_private_message.updated,
deleted: Some(orig_private_message.deleted),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
} else {
PrivateMessageForm {
content: content_slurs_removed,
creator_id: orig_private_message.creator_id,
recipient_id: orig_private_message.recipient_id,
deleted: data.deleted.to_owned(),
read: Some(orig_private_message.read),
updated: Some(naive_now()),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
}
};
let edit_id = data.edit_id; let edit_id = data.edit_id;
let updated_private_message = match blocking(pool, move |conn| { let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update(conn, edit_id, &private_message_form) PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
}) })
.await? .await?
{ {
@ -1353,9 +1265,76 @@ impl Perform for Oper<EditPrivateMessage> {
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
}; };
if data.read.is_none() { // Send the apub update
if let Some(deleted) = data.deleted.to_owned() { updated_private_message
if deleted { .send_update(&user, &self.client, pool)
.await?;
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res.clone(),
recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeletePrivateMessage> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &DeletePrivateMessage = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
}
// Doing the update
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// Send the apub update
if data.deleted {
updated_private_message updated_private_message
.send_delete(&user, &self.client, pool) .send_delete(&user, &self.client, pool)
.await?; .await?;
@ -1364,27 +1343,83 @@ impl Perform for Oper<EditPrivateMessage> {
.send_undo_delete(&user, &self.client, pool) .send_undo_delete(&user, &self.client, pool)
.await?; .await?;
} }
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id; let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message }; let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info { if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage { ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::DeletePrivateMessage,
response: res.clone(), response: res.clone(),
recipient_id: orig_private_message.recipient_id, recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<MarkPrivateMessageAsRead> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &MarkPrivateMessageAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.recipient_id {
return Err(APIError::err("couldnt_update_private_message").into());
}
// Doing the update
let edit_id = data.edit_id;
let read = data.read;
match blocking(pool, move |conn| {
PrivateMessage::update_read(conn, edit_id, read)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// No need to send an apub update
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::MarkPrivateMessageAsRead,
response: res.clone(),
recipient_id,
my_id: ws.id, my_id: ws.id,
}); });
} }

View file

@ -393,7 +393,7 @@ async fn receive_create_comment(
// anyway. // anyway.
let mentions = scrape_text_for_mentions(&inserted_comment.content); let mentions = scrape_text_for_mentions(&inserted_comment.content);
let recipient_ids = let recipient_ids =
send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?; send_local_notifs(mentions, inserted_comment.clone(), user, post, pool, true).await?;
// Refetch the view // Refetch the view
let comment_view = blocking(pool, move |conn| { let comment_view = blocking(pool, move |conn| {
@ -404,6 +404,7 @@ async fn receive_create_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -558,7 +559,7 @@ async fn receive_update_comment(
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&updated_comment.content); let mentions = scrape_text_for_mentions(&updated_comment.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?; let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
// Refetch the view // Refetch the view
let comment_view = let comment_view =
@ -567,6 +568,7 @@ async fn receive_update_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -616,6 +618,7 @@ async fn receive_like_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -665,6 +668,7 @@ async fn receive_dislike_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -960,6 +964,7 @@ async fn receive_delete_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -1017,6 +1022,7 @@ async fn receive_remove_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -1108,6 +1114,7 @@ async fn receive_undo_delete_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -1165,6 +1172,7 @@ async fn receive_undo_remove_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {
@ -1464,6 +1472,7 @@ async fn receive_undo_like_comment(
let res = CommentResponse { let res = CommentResponse {
comment: comment_view, comment: comment_view,
recipient_ids, recipient_ids,
form_id: None,
}; };
chat_server.do_send(SendComment { chat_server.do_send(SendComment {

View file

@ -1,5 +1,4 @@
use crate::{ use crate::{
api::claims::Claims,
apub::{ apub::{
activities::send_activity, activities::send_activity,
create_apub_response, create_apub_response,
@ -253,7 +252,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name; let user_name = info.into_inner().user_name;
let user = blocking(&db, move |conn| { let user = blocking(&db, move |conn| {
Claims::find_by_email_or_username(conn, &user_name) User_::find_by_email_or_username(conn, &user_name)
}) })
.await??; .await??;
let u = user.to_apub(&db).await?; let u = user.to_apub(&db).await?;

View file

@ -53,7 +53,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("", web::put().to(route_post::<EditCommunity>)) .route("", web::put().to(route_post::<EditCommunity>))
.route("/list", web::get().to(route_get::<ListCommunities>)) .route("/list", web::get().to(route_get::<ListCommunities>))
.route("/follow", web::post().to(route_post::<FollowCommunity>)) .route("/follow", web::post().to(route_post::<FollowCommunity>))
.route("/delete", web::post().to(route_post::<DeleteCommunity>))
// Mod Actions // Mod Actions
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
.route("/transfer", web::post().to(route_post::<TransferCommunity>)) .route("/transfer", web::post().to(route_post::<TransferCommunity>))
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>)) .route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
.route("/mod", web::post().to(route_post::<AddModToCommunity>)), .route("/mod", web::post().to(route_post::<AddModToCommunity>)),
@ -71,6 +73,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetPost>)) .route("", web::get().to(route_get::<GetPost>))
.route("", web::put().to(route_post::<EditPost>)) .route("", web::put().to(route_post::<EditPost>))
.route("/delete", web::post().to(route_post::<DeletePost>))
.route("/remove", web::post().to(route_post::<RemovePost>))
.route("/lock", web::post().to(route_post::<LockPost>))
.route("/sticky", web::post().to(route_post::<StickyPost>))
.route("/list", web::get().to(route_get::<GetPosts>)) .route("/list", web::get().to(route_get::<GetPosts>))
.route("/like", web::post().to(route_post::<CreatePostLike>)) .route("/like", web::post().to(route_post::<CreatePostLike>))
.route("/save", web::put().to(route_post::<SavePost>)), .route("/save", web::put().to(route_post::<SavePost>)),
@ -81,6 +87,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::post().to(route_post::<CreateComment>)) .route("", web::post().to(route_post::<CreateComment>))
.route("", web::put().to(route_post::<EditComment>)) .route("", web::put().to(route_post::<EditComment>))
.route("/delete", web::post().to(route_post::<DeleteComment>))
.route("/remove", web::post().to(route_post::<RemoveComment>))
.route(
"/mark_as_read",
web::post().to(route_post::<MarkCommentAsRead>),
)
.route("/like", web::post().to(route_post::<CreateCommentLike>)) .route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)), .route("/save", web::put().to(route_post::<SaveComment>)),
) )
@ -90,7 +102,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/list", web::get().to(route_get::<GetPrivateMessages>)) .route("/list", web::get().to(route_get::<GetPrivateMessages>))
.route("", web::post().to(route_post::<CreatePrivateMessage>)) .route("", web::post().to(route_post::<CreatePrivateMessage>))
.route("", web::put().to(route_post::<EditPrivateMessage>)), .route("", web::put().to(route_post::<EditPrivateMessage>))
.route(
"/delete",
web::post().to(route_post::<DeletePrivateMessage>),
)
.route(
"/mark_as_read",
web::post().to(route_post::<MarkPrivateMessageAsRead>),
),
) )
// User // User
.service( .service(
@ -107,7 +127,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetUserDetails>)) .route("", web::get().to(route_get::<GetUserDetails>))
.route("/mention", web::get().to(route_get::<GetUserMentions>)) .route("/mention", web::get().to(route_get::<GetUserMentions>))
.route("/mention", web::put().to(route_post::<EditUserMention>)) .route(
"/mention/mark_as_read",
web::post().to(route_post::<MarkUserMentionAsRead>),
)
.route("/replies", web::get().to(route_get::<GetReplies>)) .route("/replies", web::get().to(route_get::<GetReplies>))
.route( .route(
"/followed_communities", "/followed_communities",

View file

@ -28,19 +28,28 @@ pub enum UserOperation {
GetCommunity, GetCommunity,
CreateComment, CreateComment,
EditComment, EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment, SaveComment,
CreateCommentLike, CreateCommentLike,
GetPosts, GetPosts,
CreatePostLike, CreatePostLike,
EditPost, EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost, SavePost,
EditCommunity, EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity, FollowCommunity,
GetFollowedCommunities, GetFollowedCommunities,
GetUserDetails, GetUserDetails,
GetReplies, GetReplies,
GetUserMentions, GetUserMentions,
EditUserMention, MarkUserMentionAsRead,
GetModlog, GetModlog,
BanFromCommunity, BanFromCommunity,
AddModToCommunity, AddModToCommunity,
@ -59,6 +68,8 @@ pub enum UserOperation {
PasswordChange, PasswordChange,
CreatePrivateMessage, CreatePrivateMessage,
EditPrivateMessage, EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages, GetPrivateMessages,
UserJoin, UserJoin,
GetComments, GetComments,

View file

@ -212,6 +212,9 @@ impl ChatServer {
// Also leave all communities // Also leave all communities
// This avoids double messages // This avoids double messages
// TODO found a bug, whereby community messages like
// delete and remove aren't sent, because
// you left the community room
for sessions in self.community_rooms.values_mut() { for sessions in self.community_rooms.values_mut() {
sessions.remove(&id); sessions.remove(&id);
} }
@ -443,18 +446,28 @@ impl ChatServer {
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await, UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
UserOperation::BanUser => do_user_operation::<BanUser>(args).await, UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await, UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await, UserOperation::MarkUserMentionAsRead => {
do_user_operation::<MarkUserMentionAsRead>(args).await
}
UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await, UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await, UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await, UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await, UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Private Message ops
UserOperation::CreatePrivateMessage => { UserOperation::CreatePrivateMessage => {
do_user_operation::<CreatePrivateMessage>(args).await do_user_operation::<CreatePrivateMessage>(args).await
} }
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await, UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
UserOperation::DeletePrivateMessage => {
do_user_operation::<DeletePrivateMessage>(args).await
}
UserOperation::MarkPrivateMessageAsRead => {
do_user_operation::<MarkPrivateMessageAsRead>(args).await
}
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await, UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops // Site ops
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await, UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
@ -473,6 +486,8 @@ impl ChatServer {
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await, UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await, UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await, UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
UserOperation::DeleteCommunity => do_user_operation::<DeleteCommunity>(args).await,
UserOperation::RemoveCommunity => do_user_operation::<RemoveCommunity>(args).await,
UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await, UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
UserOperation::GetFollowedCommunities => { UserOperation::GetFollowedCommunities => {
do_user_operation::<GetFollowedCommunities>(args).await do_user_operation::<GetFollowedCommunities>(args).await
@ -485,12 +500,19 @@ impl ChatServer {
UserOperation::GetPost => do_user_operation::<GetPost>(args).await, UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await, UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
UserOperation::EditPost => do_user_operation::<EditPost>(args).await, UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
UserOperation::DeletePost => do_user_operation::<DeletePost>(args).await,
UserOperation::RemovePost => do_user_operation::<RemovePost>(args).await,
UserOperation::LockPost => do_user_operation::<LockPost>(args).await,
UserOperation::StickyPost => do_user_operation::<StickyPost>(args).await,
UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await, UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
UserOperation::SavePost => do_user_operation::<SavePost>(args).await, UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
// Comment ops // Comment ops
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await, UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
UserOperation::EditComment => do_user_operation::<EditComment>(args).await, UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
UserOperation::DeleteComment => do_user_operation::<DeleteComment>(args).await,
UserOperation::RemoveComment => do_user_operation::<RemoveComment>(args).await,
UserOperation::MarkCommentAsRead => do_user_operation::<MarkCommentAsRead>(args).await,
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await, UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
UserOperation::GetComments => do_user_operation::<GetComments>(args).await, UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await, UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,

View file

@ -4,22 +4,28 @@ import {
LoginForm, LoginForm,
LoginResponse, LoginResponse,
PostForm, PostForm,
DeletePostForm,
RemovePostForm,
// TODO need to test LockPost and StickyPost federated
PostResponse, PostResponse,
SearchResponse, SearchResponse,
FollowCommunityForm, FollowCommunityForm,
CommunityResponse, CommunityResponse,
GetFollowedCommunitiesResponse, GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse, GetPostResponse,
CommentForm, CommentForm,
DeleteCommentForm,
RemoveCommentForm,
CommentResponse, CommentResponse,
CommunityForm, CommunityForm,
GetCommunityForm, DeleteCommunityForm,
RemoveCommunityForm,
GetCommunityResponse, GetCommunityResponse,
CommentLikeForm, CommentLikeForm,
CreatePostLikeForm, CreatePostLikeForm,
PrivateMessageForm, PrivateMessageForm,
EditPrivateMessageForm, EditPrivateMessageForm,
DeletePrivateMessageForm,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
GetUserMentionsResponse, GetUserMentionsResponse,
@ -97,7 +103,6 @@ describe('main', () => {
name, name,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
community_id: 2, community_id: 2,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -266,7 +271,6 @@ describe('main', () => {
name, name,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
community_id: 3, community_id: 3,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -323,7 +327,6 @@ describe('main', () => {
edit_id: 2, edit_id: 2,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
community_id: 3, community_id: 3,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -382,7 +385,6 @@ describe('main', () => {
let unlikeCommentForm: CommentLikeForm = { let unlikeCommentForm: CommentLikeForm = {
comment_id: createResponse.comment.id, comment_id: createResponse.comment.id,
score: 0, score: 0,
post_id: 2,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
}; };
@ -585,7 +587,6 @@ describe('main', () => {
name: postName, name: postName,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id, community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -620,19 +621,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent); expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta deletes the comment // lemmy_beta deletes the comment
let deleteCommentForm: CommentForm = { let deleteCommentForm: DeleteCommentForm = {
content: commentContent,
edit_id: createCommentRes.comment.id, edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: true, deleted: true,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
}; };
let deleteCommentRes: CommentResponse = await fetch( let deleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`, `${lemmyBetaApiUrl}/comment/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -649,19 +647,16 @@ describe('main', () => {
expect(getPostRes.comments[0].deleted).toBe(true); expect(getPostRes.comments[0].deleted).toBe(true);
// lemmy_beta undeletes the comment // lemmy_beta undeletes the comment
let undeleteCommentForm: CommentForm = { let undeleteCommentForm: DeleteCommentForm = {
content: commentContent,
edit_id: createCommentRes.comment.id, edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: false, deleted: false,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
}; };
let undeleteCommentRes: CommentResponse = await fetch( let undeleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`, `${lemmyBetaApiUrl}/comment/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -677,23 +672,22 @@ describe('main', () => {
expect(getPostUndeleteRes.comments[0].deleted).toBe(false); expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
// lemmy_beta deletes the post // lemmy_beta deletes the post
let deletePostForm: PostForm = { let deletePostForm: DeletePostForm = {
name: postName,
edit_id: createPostRes.post.id, edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: true, deleted: true,
auth: lemmyBetaAuth,
}; };
let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { let deletePostRes: PostResponse = await fetch(
method: 'PUT', `${lemmyBetaApiUrl}/post/delete`,
{
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: wrapper(deletePostForm), body: wrapper(deletePostForm),
}).then(d => d.json()); }
).then(d => d.json());
expect(deletePostRes.post.deleted).toBe(true); expect(deletePostRes.post.deleted).toBe(true);
// Make sure lemmy_alpha sees the post is deleted // Make sure lemmy_alpha sees the post is deleted
@ -703,20 +697,16 @@ describe('main', () => {
expect(getPostResAgain.post.deleted).toBe(true); expect(getPostResAgain.post.deleted).toBe(true);
// lemmy_beta undeletes the post // lemmy_beta undeletes the post
let undeletePostForm: PostForm = { let undeletePostForm: DeletePostForm = {
name: postName,
edit_id: createPostRes.post.id, edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: false, deleted: false,
auth: lemmyBetaAuth,
}; };
let undeletePostRes: PostResponse = await fetch( let undeletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`, `${lemmyBetaApiUrl}/post/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -732,20 +722,16 @@ describe('main', () => {
expect(getPostResAgainTwo.post.deleted).toBe(false); expect(getPostResAgainTwo.post.deleted).toBe(false);
// lemmy_beta deletes the community // lemmy_beta deletes the community
let deleteCommunityForm: CommunityForm = { let deleteCommunityForm: DeleteCommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
edit_id: createCommunityRes.community.id, edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: true, deleted: true,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
}; };
let deleteResponse: CommunityResponse = await fetch( let deleteResponse: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`, `${lemmyBetaApiUrl}/community/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -765,20 +751,16 @@ describe('main', () => {
expect(getCommunityRes.community.deleted).toBe(true); expect(getCommunityRes.community.deleted).toBe(true);
// lemmy_beta undeletes the community // lemmy_beta undeletes the community
let undeleteCommunityForm: CommunityForm = { let undeleteCommunityForm: DeleteCommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
edit_id: createCommunityRes.community.id, edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: false, deleted: false,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
}; };
let undeleteCommunityRes: CommunityResponse = await fetch( let undeleteCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`, `${lemmyBetaApiUrl}/community/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -861,7 +843,6 @@ describe('main', () => {
name: postName, name: postName,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id, community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -896,19 +877,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent); expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta removes the comment // lemmy_beta removes the comment
let removeCommentForm: CommentForm = { let removeCommentForm: RemoveCommentForm = {
content: commentContent,
edit_id: createCommentRes.comment.id, edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: true, removed: true,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
}; };
let removeCommentRes: CommentResponse = await fetch( let removeCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`, `${lemmyBetaApiUrl}/comment/remove`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -925,19 +903,16 @@ describe('main', () => {
expect(getPostRes.comments[0].removed).toBe(true); expect(getPostRes.comments[0].removed).toBe(true);
// lemmy_beta undeletes the comment // lemmy_beta undeletes the comment
let unremoveCommentForm: CommentForm = { let unremoveCommentForm: RemoveCommentForm = {
content: commentContent,
edit_id: createCommentRes.comment.id, edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: false, removed: false,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
}; };
let unremoveCommentRes: CommentResponse = await fetch( let unremoveCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`, `${lemmyBetaApiUrl}/comment/remove`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -953,23 +928,22 @@ describe('main', () => {
expect(getPostUnremoveRes.comments[0].removed).toBe(false); expect(getPostUnremoveRes.comments[0].removed).toBe(false);
// lemmy_beta deletes the post // lemmy_beta deletes the post
let removePostForm: PostForm = { let removePostForm: RemovePostForm = {
name: postName,
edit_id: createPostRes.post.id, edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: true, removed: true,
auth: lemmyBetaAuth,
}; };
let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, { let removePostRes: PostResponse = await fetch(
method: 'PUT', `${lemmyBetaApiUrl}/post/remove`,
{
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: wrapper(removePostForm), body: wrapper(removePostForm),
}).then(d => d.json()); }
).then(d => d.json());
expect(removePostRes.post.removed).toBe(true); expect(removePostRes.post.removed).toBe(true);
// Make sure lemmy_alpha sees the post is deleted // Make sure lemmy_alpha sees the post is deleted
@ -979,20 +953,16 @@ describe('main', () => {
expect(getPostResAgain.post.removed).toBe(true); expect(getPostResAgain.post.removed).toBe(true);
// lemmy_beta unremoves the post // lemmy_beta unremoves the post
let unremovePostForm: PostForm = { let unremovePostForm: RemovePostForm = {
name: postName,
edit_id: createPostRes.post.id, edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: false, removed: false,
auth: lemmyBetaAuth,
}; };
let unremovePostRes: PostResponse = await fetch( let unremovePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`, `${lemmyBetaApiUrl}/post/remove`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1007,21 +977,17 @@ describe('main', () => {
}).then(d => d.json()); }).then(d => d.json());
expect(getPostResAgainTwo.post.removed).toBe(false); expect(getPostResAgainTwo.post.removed).toBe(false);
// lemmy_beta deletes the community // lemmy_beta removes the community
let removeCommunityForm: CommunityForm = { let removeCommunityForm: RemoveCommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
edit_id: createCommunityRes.community.id, edit_id: createCommunityRes.community.id,
nsfw: false,
removed: true, removed: true,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
}; };
let removeCommunityRes: CommunityResponse = await fetch( let removeCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`, `${lemmyBetaApiUrl}/community/remove`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1029,7 +995,7 @@ describe('main', () => {
} }
).then(d => d.json()); ).then(d => d.json());
// Make sure the delete went through // Make sure the remove went through
expect(removeCommunityRes.community.removed).toBe(true); expect(removeCommunityRes.community.removed).toBe(true);
// Re-get it from alpha, make sure its removed there too // Re-get it from alpha, make sure its removed there too
@ -1041,20 +1007,16 @@ describe('main', () => {
expect(getCommunityRes.community.removed).toBe(true); expect(getCommunityRes.community.removed).toBe(true);
// lemmy_beta unremoves the community // lemmy_beta unremoves the community
let unremoveCommunityForm: CommunityForm = { let unremoveCommunityForm: RemoveCommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
edit_id: createCommunityRes.community.id, edit_id: createCommunityRes.community.id,
nsfw: false,
removed: false, removed: false,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
}; };
let unremoveCommunityRes: CommunityResponse = await fetch( let unremoveCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`, `${lemmyBetaApiUrl}/community/remove`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1149,16 +1111,16 @@ describe('main', () => {
); );
// lemmy alpha deletes the private message // lemmy alpha deletes the private message
let deletePrivateMessageForm: EditPrivateMessageForm = { let deletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: true, deleted: true,
edit_id: createRes.message.id, edit_id: createRes.message.id,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
}; };
let deleteRes: PrivateMessageResponse = await fetch( let deleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`, `${lemmyAlphaApiUrl}/private_message/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1182,16 +1144,16 @@ describe('main', () => {
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0); expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
// lemmy alpha undeletes the private message // lemmy alpha undeletes the private message
let undeletePrivateMessageForm: EditPrivateMessageForm = { let undeletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: false, deleted: false,
edit_id: createRes.message.id, edit_id: createRes.message.id,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
}; };
let undeleteRes: PrivateMessageResponse = await fetch( let undeleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`, `${lemmyAlphaApiUrl}/private_message/delete`,
{ {
method: 'PUT', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1252,7 +1214,6 @@ describe('main', () => {
name: postName, name: postName,
auth: lemmyAlphaAuth, auth: lemmyAlphaAuth,
community_id: 2, community_id: 2,
creator_id: 2,
nsfw: false, nsfw: false,
}; };
@ -1363,7 +1324,6 @@ describe('main', () => {
name: betaPostName, name: betaPostName,
auth: lemmyBetaAuth, auth: lemmyBetaAuth,
community_id: 2, community_id: 2,
creator_id: 2,
nsfw: false, nsfw: false,
}; };

View file

@ -115,34 +115,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
); );
} }
handleFinished(op: UserOperation, data: CommentResponse) { handleCommentSubmit(msg: { val: string; formId: string }) {
let isReply = this.state.commentForm.content = msg.val;
this.props.node !== undefined && data.comment.parent_id !== null; this.state.commentForm.form_id = msg.formId;
let xor =
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
if (
(data.comment.creator_id == UserService.Instance.user.id &&
((op == UserOperation.CreateComment &&
// If its a reply, make sure parent child match
isReply &&
data.comment.parent_id == this.props.node.comment.id) ||
// Otherwise, check the XOR of the two
(!isReply && xor))) ||
// If its a comment edit, only check that its from your user, and that its a
// text edit only
(data.comment.creator_id == UserService.Instance.user.id &&
op == UserOperation.EditComment &&
data.comment.content)
) {
this.state.finished = true;
this.setState(this.state);
}
}
handleCommentSubmit(val: string) {
this.state.commentForm.content = val;
if (this.props.edit) { if (this.props.edit) {
WebSocketService.Instance.editComment(this.state.commentForm); WebSocketService.Instance.editComment(this.state.commentForm);
} else { } else {
@ -160,12 +135,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// Only do the showing and hiding if logged in // Only do the showing and hiding if logged in
if (UserService.Instance.user) { if (UserService.Instance.user) {
if (res.op == UserOperation.CreateComment) { if (
res.op == UserOperation.CreateComment ||
res.op == UserOperation.EditComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
} else if (res.op == UserOperation.EditComment) { // This only finishes this form, if the randomly generated form_id matches the one received
let data = res.data as CommentResponse; if (this.state.commentForm.form_id == data.form_id) {
this.handleFinished(res.op, data); this.setState({ finished: true });
}
} }
} }
} }

View file

@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
import { import {
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
CommentLikeForm, CommentLikeForm,
CommentForm as CommentFormI, DeleteCommentForm,
EditUserMentionForm, RemoveCommentForm,
MarkCommentAsReadForm,
MarkUserMentionAsReadForm,
SaveCommentForm, SaveCommentForm,
BanFromCommunityForm, BanFromCommunityForm,
BanUserForm, BanUserForm,
@ -848,16 +850,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
} }
handleDeleteClick(i: CommentNode) { handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = { let deleteForm: DeleteCommentForm = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id, edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
deleted: !i.props.node.comment.deleted, deleted: !i.props.node.comment.deleted,
auth: null, auth: null,
}; };
WebSocketService.Instance.editComment(deleteForm); WebSocketService.Instance.deleteComment(deleteForm);
} }
handleSaveCommentClick(i: CommentNode) { handleSaveCommentClick(i: CommentNode) {
@ -901,7 +899,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote, score: this.state.my_vote,
}; };
@ -929,7 +926,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = { let form: CommentLikeForm = {
comment_id: i.comment.id, comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote, score: this.state.my_vote,
}; };
@ -950,17 +946,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModRemoveSubmit(i: CommentNode) { handleModRemoveSubmit(i: CommentNode) {
event.preventDefault(); event.preventDefault();
let form: CommentFormI = { let form: RemoveCommentForm = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id, 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, removed: !i.props.node.comment.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
auth: null, auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.removeComment(form);
i.state.showRemoveDialog = false; i.state.showRemoveDialog = false;
i.setState(i.state); i.setState(i.state);
@ -969,22 +961,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleMarkRead(i: CommentNode) { handleMarkRead(i: CommentNode) {
// if it has a user_mention_id field, then its a mention // if it has a user_mention_id field, then its a mention
if (i.props.node.comment.user_mention_id) { if (i.props.node.comment.user_mention_id) {
let form: EditUserMentionForm = { let form: MarkUserMentionAsReadForm = {
user_mention_id: i.props.node.comment.user_mention_id, user_mention_id: i.props.node.comment.user_mention_id,
read: !i.props.node.comment.read, read: !i.props.node.comment.read,
}; };
WebSocketService.Instance.editUserMention(form); WebSocketService.Instance.markUserMentionAsRead(form);
} else { } else {
let form: CommentFormI = { let form: MarkCommentAsReadForm = {
content: i.props.node.comment.content,
edit_id: i.props.node.comment.id, edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
read: !i.props.node.comment.read, read: !i.props.node.comment.read,
auth: null, auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.markCommentAsRead(form);
} }
i.state.readLoading = true; i.state.readLoading = true;

View file

@ -355,12 +355,15 @@ export class Community extends Component<any, State> {
let data = res.data as GetCommunityResponse; let data = res.data as GetCommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.online = data.online; this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`; document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state); this.setState(this.state);
this.fetchData(); this.fetchData();
} else if (res.op == UserOperation.EditCommunity) { } else if (
res.op == UserOperation.EditCommunity ||
res.op == UserOperation.DeleteCommunity ||
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse; let data = res.data as CommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.setState(this.state); this.setState(this.state);
@ -376,7 +379,13 @@ export class Community extends Component<any, State> {
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (res.op == UserOperation.EditPost) { } else if (
res.op == UserOperation.EditPost ||
res.op == UserOperation.DeletePost ||
res.op == UserOperation.RemovePost ||
res.op == UserOperation.LockPost ||
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse; let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts); editPostFindRes(data, this.state.posts);
this.setState(this.state); this.setState(this.state);
@ -405,7 +414,11 @@ export class Community extends Component<any, State> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);
@ -428,6 +441,7 @@ export class Community extends Component<any, State> {
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse; let data = res.data as GetSiteResponse;
this.state.site = data.site; this.state.site = data.site;
this.state.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -446,9 +446,30 @@ export class Inbox extends Component<any, InboxState> {
let found: PrivateMessageI = this.state.messages.find( let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id m => m.id === data.message.id
); );
if (found) {
found.content = data.message.content; found.content = data.message.content;
found.updated = data.message.updated; found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.DeletePrivateMessage) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.deleted = data.message.deleted; found.deleted = data.message.deleted;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.updated = data.message.updated;
// If youre in the unread view, just remove it from the list // If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) { if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
this.state.messages = this.state.messages.filter( this.state.messages = this.state.messages.filter(
@ -458,15 +479,21 @@ export class Inbox extends Component<any, InboxState> {
let found = this.state.messages.find(c => c.id == data.message.id); let found = this.state.messages.find(c => c.id == data.message.id);
found.read = data.message.read; found.read = data.message.read;
} }
}
this.sendUnreadCount(); this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) { } else if (res.op == UserOperation.MarkAllAsRead) {
// Moved to be instant // Moved to be instant
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.replies); editCommentRes(data, this.state.replies);
this.setState(this.state);
} else if (res.op == UserOperation.MarkCommentAsRead) {
let data = res.data as CommentResponse;
// If youre in the unread view, just remove it from the list // If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) { if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
@ -480,7 +507,7 @@ export class Inbox extends Component<any, InboxState> {
this.sendUnreadCount(); this.sendUnreadCount();
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (res.op == UserOperation.EditUserMention) { } else if (res.op == UserOperation.MarkUserMentionAsRead) {
let data = res.data as UserMentionResponse; let data = res.data as UserMentionResponse;
let found = this.state.mentions.find(c => c.id == data.mention.id); let found = this.state.mentions.find(c => c.id == data.mention.id);

View file

@ -702,7 +702,11 @@ export class Main extends Component<any, MainState> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);

View file

@ -21,7 +21,7 @@ interface MarkdownTextAreaProps {
replyType?: boolean; replyType?: boolean;
focus?: boolean; focus?: boolean;
disabled?: boolean; disabled?: boolean;
onSubmit?(val: string): any; onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any; onContentChange?(val: string): any;
onReplyCancel?(): any; onReplyCancel?(): any;
} }
@ -391,7 +391,8 @@ export class MarkdownTextArea extends Component<
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.state.loading = true;
i.setState(i.state); i.setState(i.state);
i.props.onSubmit(i.state.content); let msg = { val: i.state.content, formId: i.formId };
i.props.onSubmit(msg);
} }
handleReplyCancel(i: MarkdownTextArea) { handleReplyCancel(i: MarkdownTextArea) {

View file

@ -71,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
nsfw: false, nsfw: false,
auth: null, auth: null,
community_id: null, community_id: null,
creator_id: UserService.Instance.user
? UserService.Instance.user.id
: null,
}, },
communities: [], communities: [],
loading: false, loading: false,
@ -99,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: this.props.post.name, name: this.props.post.name,
community_id: this.props.post.community_id, community_id: this.props.post.community_id,
edit_id: this.props.post.id, edit_id: this.props.post.id,
creator_id: this.props.post.creator_id,
url: this.props.post.url, url: this.props.post.url,
nsfw: this.props.post.nsfw, nsfw: this.props.post.nsfw,
auth: null, auth: null,

View file

@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services';
import { import {
Post, Post,
CreatePostLikeForm, CreatePostLikeForm,
PostForm as PostFormI, DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm, SavePostForm,
CommunityUser, CommunityUser,
UserView, UserView,
@ -33,7 +36,6 @@ import {
setupTippy, setupTippy,
hostname, hostname,
previewLines, previewLines,
toast,
} from '../utils'; } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -1114,18 +1116,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} }
handleDeleteClick(i: PostListing) { handleDeleteClick(i: PostListing) {
let deleteForm: PostFormI = { let deleteForm: DeletePostForm = {
body: i.props.post.body,
community_id: i.props.post.community_id,
name: i.props.post.name,
url: i.props.post.url,
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted, deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
auth: null, auth: null,
}; };
WebSocketService.Instance.editPost(deleteForm); WebSocketService.Instance.deletePost(deleteForm);
} }
handleSavePostClick(i: PostListing) { handleSavePostClick(i: PostListing) {
@ -1163,46 +1159,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
handleModRemoveSubmit(i: PostListing) { handleModRemoveSubmit(i: PostListing) {
event.preventDefault(); event.preventDefault();
let form: PostFormI = { let form: RemovePostForm = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed, removed: !i.props.post.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
auth: null, auth: null,
}; };
WebSocketService.Instance.editPost(form); WebSocketService.Instance.removePost(form);
i.state.showRemoveDialog = false; i.state.showRemoveDialog = false;
i.setState(i.state); i.setState(i.state);
} }
handleModLock(i: PostListing) { handleModLock(i: PostListing) {
let form: PostFormI = { let form: LockPostForm = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
locked: !i.props.post.locked, locked: !i.props.post.locked,
auth: null, auth: null,
}; };
WebSocketService.Instance.editPost(form); WebSocketService.Instance.lockPost(form);
} }
handleModSticky(i: PostListing) { handleModSticky(i: PostListing) {
let form: PostFormI = { let form: StickyPostForm = {
name: i.props.post.name,
community_id: i.props.post.community_id,
edit_id: i.props.post.id, edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
stickied: !i.props.post.stickied, stickied: !i.props.post.stickied,
auth: null, auth: null,
}; };
WebSocketService.Instance.editPost(form); WebSocketService.Instance.stickyPost(form);
} }
handleModBanFromCommunityShow(i: PostListing) { handleModBanFromCommunityShow(i: PostListing) {

View file

@ -8,7 +8,7 @@ import {
GetPostResponse, GetPostResponse,
PostResponse, PostResponse,
Comment, Comment,
CommentForm as CommentFormI, MarkCommentAsReadForm,
CommentResponse, CommentResponse,
CommentSortType, CommentSortType,
CommentViewType, CommentViewType,
@ -168,16 +168,12 @@ export class Post extends Component<any, PostState> {
UserService.Instance.user && UserService.Instance.user &&
UserService.Instance.user.id == parent_user_id UserService.Instance.user.id == parent_user_id
) { ) {
let form: CommentFormI = { let form: MarkCommentAsReadForm = {
content: found.content,
edit_id: found.id, edit_id: found.id,
creator_id: found.creator_id,
post_id: found.post_id,
parent_id: found.parent_id,
read: true, read: true,
auth: null, auth: null,
}; };
WebSocketService.Instance.editComment(form); WebSocketService.Instance.markCommentAsRead(form);
UserService.Instance.user.unreadCount--; UserService.Instance.user.unreadCount--;
UserService.Instance.sub.next({ UserService.Instance.sub.next({
user: UserService.Instance.user, user: UserService.Instance.user,
@ -409,7 +405,6 @@ export class Post extends Component<any, PostState> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.state.online = data.online; this.state.online = data.online;
this.state.loading = false; this.state.loading = false;
document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`; document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
@ -436,7 +431,11 @@ export class Post extends Component<any, PostState> {
this.state.comments.unshift(data.comment); this.state.comments.unshift(data.comment);
this.setState(this.state); this.setState(this.state);
} }
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);
@ -453,7 +452,13 @@ export class Post extends Component<any, PostState> {
let data = res.data as PostResponse; let data = res.data as PostResponse;
createPostLikeRes(data, this.state.post); createPostLikeRes(data, this.state.post);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.EditPost) { } else if (
res.op == UserOperation.EditPost ||
res.op == UserOperation.DeletePost ||
res.op == UserOperation.RemovePost ||
res.op == UserOperation.LockPost ||
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse; let data = res.data as PostResponse;
this.state.post = data.post; this.state.post = data.post;
this.setState(this.state); this.setState(this.state);
@ -463,7 +468,11 @@ export class Post extends Component<any, PostState> {
this.state.post = data.post; this.state.post = data.post;
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (res.op == UserOperation.EditCommunity) { } else if (
res.op == UserOperation.EditCommunity ||
res.op == UserOperation.DeleteCommunity ||
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse; let data = res.data as CommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.state.post.community_id = data.community.id; this.state.post.community_id = data.community.id;
@ -521,7 +530,6 @@ export class Post extends Component<any, PostState> {
let data = res.data as GetCommunityResponse; let data = res.data as GetCommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component<
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
return; return;
} else if (res.op == UserOperation.EditPrivateMessage) { } else if (
res.op == UserOperation.EditPrivateMessage ||
res.op == UserOperation.DeletePrivateMessage ||
res.op == UserOperation.MarkPrivateMessageAsRead
) {
let data = res.data as PrivateMessageResponse; let data = res.data as PrivateMessageResponse;
this.state.loading = false; this.state.loading = false;
this.props.onEdit(data.message); this.props.onEdit(data.message);

View file

@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { import {
PrivateMessage as PrivateMessageI, PrivateMessage as PrivateMessageI,
EditPrivateMessageForm, DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils'; import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
} }
handleDeleteClick(i: PrivateMessage) { handleDeleteClick(i: PrivateMessage) {
let form: EditPrivateMessageForm = { let form: DeletePrivateMessageForm = {
edit_id: i.props.privateMessage.id, edit_id: i.props.privateMessage.id,
deleted: !i.props.privateMessage.deleted, deleted: !i.props.privateMessage.deleted,
}; };
WebSocketService.Instance.editPrivateMessage(form); WebSocketService.Instance.deletePrivateMessage(form);
} }
handleReplyCancel() { handleReplyCancel() {
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
} }
handleMarkRead(i: PrivateMessage) { handleMarkRead(i: PrivateMessage) {
let form: EditPrivateMessageForm = { let form: MarkPrivateMessageAsReadForm = {
edit_id: i.props.privateMessage.id, edit_id: i.props.privateMessage.id,
read: !i.props.privateMessage.read, read: !i.props.privateMessage.read,
}; };
WebSocketService.Instance.editPrivateMessage(form); WebSocketService.Instance.markPrivateMessageAsRead(form);
} }
handleMessageCollapse(i: PrivateMessage) { handleMessageCollapse(i: PrivateMessage) {

View file

@ -4,7 +4,8 @@ import {
Community, Community,
CommunityUser, CommunityUser,
FollowCommunityForm, FollowCommunityForm,
CommunityForm as CommunityFormI, DeleteCommunityForm,
RemoveCommunityForm,
UserView, UserView,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
@ -284,16 +285,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleDeleteClick(i: Sidebar) { handleDeleteClick(i: Sidebar) {
event.preventDefault(); event.preventDefault();
let deleteForm: CommunityFormI = { let deleteForm: DeleteCommunityForm = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
edit_id: i.props.community.id, edit_id: i.props.community.id,
deleted: !i.props.community.deleted, deleted: !i.props.community.deleted,
nsfw: i.props.community.nsfw,
auth: null,
}; };
WebSocketService.Instance.editCommunity(deleteForm); WebSocketService.Instance.deleteCommunity(deleteForm);
} }
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {
@ -350,18 +346,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleModRemoveSubmit(i: Sidebar) { handleModRemoveSubmit(i: Sidebar) {
event.preventDefault(); event.preventDefault();
let deleteForm: CommunityFormI = { let removeForm: RemoveCommunityForm = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
edit_id: i.props.community.id, edit_id: i.props.community.id,
removed: !i.props.community.removed, removed: !i.props.community.removed,
reason: i.state.removeReason, reason: i.state.removeReason,
expires: getUnixTime(i.state.removeExpires), expires: getUnixTime(i.state.removeExpires),
nsfw: i.props.community.nsfw,
auth: null,
}; };
WebSocketService.Instance.editCommunity(deleteForm); WebSocketService.Instance.removeCommunity(removeForm);
i.state.showRemoveDialog = false; i.state.showRemoveDialog = false;
i.setState(i.state); i.setState(i.state);

View file

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take, last } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { import {
UserOperation, UserOperation,
@ -16,7 +16,6 @@ import {
CommentResponse, CommentResponse,
BanUserResponse, BanUserResponse,
PostResponse, PostResponse,
AddAdminResponse,
} from '../interfaces'; } from '../interfaces';
import { import {
wsJsonToRes, wsJsonToRes,
@ -41,6 +40,7 @@ interface UserDetailsProps {
enableNsfw: boolean; enableNsfw: boolean;
view: UserDetailsView; view: UserDetailsView;
onPageChange(page: number): number | any; onPageChange(page: number): number | any;
admins: Array<UserView>;
} }
interface UserDetailsState { interface UserDetailsState {
@ -49,7 +49,6 @@ interface UserDetailsState {
comments: Array<Comment>; comments: Array<Comment>;
posts: Array<Post>; posts: Array<Post>;
saved?: Array<Post>; saved?: Array<Post>;
admins: Array<UserView>;
} }
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> { export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
@ -63,7 +62,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
comments: [], comments: [],
posts: [], posts: [],
saved: [], saved: [],
admins: [],
}; };
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
@ -152,7 +150,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
{i.type === 'posts' ? ( {i.type === 'posts' ? (
<PostListing <PostListing
post={i.data as Post} post={i.data as Post}
admins={this.state.admins} admins={this.props.admins}
showCommunity showCommunity
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -160,7 +158,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
) : ( ) : (
<CommentNodes <CommentNodes
nodes={[{ comment: i.data as Comment }]} nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins} admins={this.props.admins}
noIndent noIndent
showContext showContext
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
@ -177,7 +175,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div> <div>
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins} admins={this.props.admins}
noIndent noIndent
showContext showContext
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
@ -192,7 +190,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
{this.state.posts.map(post => ( {this.state.posts.map(post => (
<PostListing <PostListing
post={post} post={post}
admins={this.state.admins} admins={this.props.admins}
showCommunity showCommunity
enableDownvotes={this.props.enableDownvotes} enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw} enableNsfw={this.props.enableNsfw}
@ -252,7 +250,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
follows: data.follows, follows: data.follows,
moderates: data.moderates, moderates: data.moderates,
posts: data.posts, posts: data.posts,
admins: data.admins,
}); });
} else if (res.op == UserOperation.CreateCommentLike) { } else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse; const data = res.data as CommentResponse;
@ -260,7 +257,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
this.setState({ this.setState({
comments: this.state.comments, comments: this.state.comments,
}); });
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
const data = res.data as CommentResponse; const data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState({ this.setState({
@ -298,11 +299,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
posts: this.state.posts, posts: this.state.posts,
comments: this.state.comments, comments: this.state.comments,
}); });
} else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.setState({
admins: data.admins,
});
} }
} }
} }

View file

@ -13,9 +13,9 @@ import {
DeleteAccountForm, DeleteAccountForm,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse, GetSiteResponse,
Site,
UserDetailsView, UserDetailsView,
UserDetailsResponse, UserDetailsResponse,
AddAdminResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -54,7 +54,7 @@ interface UserState {
deleteAccountLoading: boolean; deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean; deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm; deleteAccountForm: DeleteAccountForm;
site: Site; siteRes: GetSiteResponse;
} }
interface UserProps { interface UserProps {
@ -114,6 +114,10 @@ export class User extends Component<any, UserState> {
deleteAccountForm: { deleteAccountForm: {
password: null, password: null,
}, },
siteRes: {
admins: [],
banned: [],
online: undefined,
site: { site: {
id: undefined, id: undefined,
name: undefined, name: undefined,
@ -128,6 +132,7 @@ export class User extends Component<any, UserState> {
open_registration: undefined, open_registration: undefined,
enable_nsfw: undefined, enable_nsfw: undefined,
}, },
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -201,7 +206,7 @@ export class User extends Component<any, UserState> {
// Couldnt get a refresh working. This does for now. // Couldnt get a refresh working. This does for now.
location.reload(); location.reload();
} }
document.title = `/u/${this.state.username} - ${this.state.site.name}`; document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
setupTippy(); setupTippy();
} }
@ -236,8 +241,9 @@ export class User extends Component<any, UserState> {
sort={SortType[this.state.sort]} sort={SortType[this.state.sort]}
page={this.state.page} page={this.state.page}
limit={fetchLimit} limit={fetchLimit}
enableDownvotes={this.state.site.enable_downvotes} enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw} enableNsfw={this.state.siteRes.site.enable_nsfw}
admins={this.state.siteRes.admins}
view={this.state.view} view={this.state.view}
onPageChange={this.handlePageChange} onPageChange={this.handlePageChange}
/> />
@ -637,7 +643,7 @@ export class User extends Component<any, UserState> {
/> />
</div> </div>
</div> </div>
{this.state.site.enable_nsfw && ( {this.state.siteRes.site.enable_nsfw && (
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input
@ -1063,9 +1069,12 @@ export class User extends Component<any, UserState> {
this.context.router.history.push('/'); this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
const data = res.data as GetSiteResponse; const data = res.data as GetSiteResponse;
this.setState({ this.state.siteRes = data;
site: data.site, this.setState(this.state);
}); } else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.state.siteRes.admins = data.admins;
this.setState(this.state);
} }
} }
} }

123
ui/src/interfaces.ts vendored
View file

@ -9,19 +9,28 @@ export enum UserOperation {
GetCommunity, GetCommunity,
CreateComment, CreateComment,
EditComment, EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment, SaveComment,
CreateCommentLike, CreateCommentLike,
GetPosts, GetPosts,
CreatePostLike, CreatePostLike,
EditPost, EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost, SavePost,
EditCommunity, EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity, FollowCommunity,
GetFollowedCommunities, GetFollowedCommunities,
GetUserDetails, GetUserDetails,
GetReplies, GetReplies,
GetUserMentions, GetUserMentions,
EditUserMention, MarkUserMentionAsRead,
GetModlog, GetModlog,
BanFromCommunity, BanFromCommunity,
AddModToCommunity, AddModToCommunity,
@ -40,6 +49,8 @@ export enum UserOperation {
PasswordChange, PasswordChange,
CreatePrivateMessage, CreatePrivateMessage,
EditPrivateMessage, EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages, GetPrivateMessages,
UserJoin, UserJoin,
GetComments, GetComments,
@ -355,9 +366,9 @@ export interface GetUserMentionsResponse {
mentions: Array<Comment>; mentions: Array<Comment>;
} }
export interface EditUserMentionForm { export interface MarkUserMentionAsReadForm {
user_mention_id: number; user_mention_id: number;
read?: boolean; read: boolean;
auth?: string; auth?: string;
} }
@ -571,13 +582,23 @@ export interface UserSettingsForm {
export interface CommunityForm { export interface CommunityForm {
name: string; name: string;
edit_id?: number;
title: string; title: string;
description?: string; description?: string;
category_id: number; category_id: number;
edit_id?: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean; nsfw: boolean;
auth?: string;
}
export interface DeleteCommunityForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface RemoveCommunityForm {
edit_id: number;
removed: boolean;
reason?: string; reason?: string;
expires?: number; expires?: number;
auth?: string; auth?: string;
@ -592,7 +613,6 @@ export interface GetCommunityForm {
export interface GetCommunityResponse { export interface GetCommunityResponse {
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number; online: number;
} }
@ -619,19 +639,37 @@ export interface PostForm {
name: string; name: string;
url?: string; url?: string;
body?: string; body?: string;
community_id: number; community_id?: number;
updated?: number;
edit_id?: number; edit_id?: number;
creator_id: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean; nsfw: boolean;
locked?: boolean; auth: string;
stickied?: boolean; }
export interface DeletePostForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemovePostForm {
edit_id: number;
removed: boolean;
reason?: string; reason?: string;
auth: string; auth: string;
} }
export interface LockPostForm {
edit_id: number;
locked: boolean;
auth: string;
}
export interface StickyPostForm {
edit_id: number;
stickied: boolean;
auth: string;
}
export interface PostFormParams { export interface PostFormParams {
name: string; name: string;
url?: string; url?: string;
@ -649,7 +687,6 @@ export interface GetPostResponse {
comments: Array<Comment>; comments: Array<Comment>;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number; online: number;
} }
@ -665,14 +702,30 @@ export interface PostResponse {
export interface CommentForm { export interface CommentForm {
content: string; content: string;
post_id: number; post_id?: number;
parent_id?: number; parent_id?: number;
edit_id?: number; edit_id?: number;
creator_id?: number; creator_id?: number;
removed?: boolean; form_id?: string;
deleted?: boolean; auth: string;
}
export interface DeleteCommentForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemoveCommentForm {
edit_id: number;
removed: boolean;
reason?: string; reason?: string;
read?: boolean; auth: string;
}
export interface MarkCommentAsReadForm {
edit_id: number;
read: boolean;
auth: string; auth: string;
} }
@ -685,11 +738,11 @@ export interface SaveCommentForm {
export interface CommentResponse { export interface CommentResponse {
comment: Comment; comment: Comment;
recipient_ids: Array<number>; recipient_ids: Array<number>;
form_id?: string;
} }
export interface CommentLikeForm { export interface CommentLikeForm {
comment_id: number; comment_id: number;
post_id: number;
score: number; score: number;
auth?: string; auth?: string;
} }
@ -835,9 +888,19 @@ export interface PrivateMessageFormParams {
export interface EditPrivateMessageForm { export interface EditPrivateMessageForm {
edit_id: number; edit_id: number;
content?: string; content: string;
deleted?: boolean; auth?: string;
read?: boolean; }
export interface DeletePrivateMessageForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface MarkPrivateMessageAsReadForm {
edit_id: number;
read: boolean;
auth?: string; auth?: string;
} }
@ -865,18 +928,26 @@ export interface UserJoinResponse {
} }
export type MessageType = export type MessageType =
| EditPrivateMessageForm
| LoginForm | LoginForm
| RegisterForm | RegisterForm
| CommunityForm | CommunityForm
| DeleteCommunityForm
| RemoveCommunityForm
| FollowCommunityForm | FollowCommunityForm
| ListCommunitiesForm | ListCommunitiesForm
| GetFollowedCommunitiesForm | GetFollowedCommunitiesForm
| PostForm | PostForm
| DeletePostForm
| RemovePostForm
| LockPostForm
| StickyPostForm
| GetPostForm | GetPostForm
| GetPostsForm | GetPostsForm
| GetCommunityForm | GetCommunityForm
| CommentForm | CommentForm
| DeleteCommentForm
| RemoveCommentForm
| MarkCommentAsReadForm
| CommentLikeForm | CommentLikeForm
| SaveCommentForm | SaveCommentForm
| CreatePostLikeForm | CreatePostLikeForm
@ -891,7 +962,7 @@ export type MessageType =
| GetUserDetailsForm | GetUserDetailsForm
| GetRepliesForm | GetRepliesForm
| GetUserMentionsForm | GetUserMentionsForm
| EditUserMentionForm | MarkUserMentionAsReadForm
| GetModlogForm | GetModlogForm
| SiteForm | SiteForm
| SearchForm | SearchForm
@ -901,6 +972,8 @@ export type MessageType =
| PasswordChangeForm | PasswordChangeForm
| PrivateMessageForm | PrivateMessageForm
| EditPrivateMessageForm | EditPrivateMessageForm
| DeletePrivateMessageForm
| MarkPrivateMessageAsReadForm
| GetPrivateMessagesForm | GetPrivateMessagesForm
| SiteConfigForm; | SiteConfigForm;

View file

@ -4,9 +4,18 @@ import {
RegisterForm, RegisterForm,
UserOperation, UserOperation,
CommunityForm, CommunityForm,
DeleteCommunityForm,
RemoveCommunityForm,
PostForm, PostForm,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm, SavePostForm,
CommentForm, CommentForm,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentAsReadForm,
SaveCommentForm, SaveCommentForm,
CommentLikeForm, CommentLikeForm,
GetPostForm, GetPostForm,
@ -28,7 +37,7 @@ import {
UserView, UserView,
GetRepliesForm, GetRepliesForm,
GetUserMentionsForm, GetUserMentionsForm,
EditUserMentionForm, MarkUserMentionAsReadForm,
SearchForm, SearchForm,
UserSettingsForm, UserSettingsForm,
DeleteAccountForm, DeleteAccountForm,
@ -36,6 +45,8 @@ import {
PasswordChangeForm, PasswordChangeForm,
PrivateMessageForm, PrivateMessageForm,
EditPrivateMessageForm, EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
GetPrivateMessagesForm, GetPrivateMessagesForm,
GetCommentsForm, GetCommentsForm,
UserJoinForm, UserJoinForm,
@ -103,18 +114,24 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm)); this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
} }
public createCommunity(communityForm: CommunityForm) { public createCommunity(form: CommunityForm) {
this.setAuth(communityForm); this.setAuth(form);
this.ws.send( this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
);
} }
public editCommunity(communityForm: CommunityForm) { public editCommunity(form: CommunityForm) {
this.setAuth(communityForm); this.setAuth(form);
this.ws.send( this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form));
this.wsSendWrapper(UserOperation.EditCommunity, communityForm) }
);
public deleteCommunity(form: DeleteCommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeleteCommunity, form));
}
public removeCommunity(form: RemoveCommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemoveCommunity, form));
} }
public followCommunity(followCommunityForm: FollowCommunityForm) { public followCommunity(followCommunityForm: FollowCommunityForm) {
@ -140,9 +157,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {})); this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
} }
public createPost(postForm: PostForm) { public createPost(form: PostForm) {
this.setAuth(postForm); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm)); this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form));
} }
public getPost(form: GetPostForm) { public getPost(form: GetPostForm) {
@ -155,14 +172,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form)); this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
} }
public createComment(commentForm: CommentForm) { public createComment(form: CommentForm) {
this.setAuth(commentForm); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm)); this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form));
} }
public editComment(commentForm: CommentForm) { public editComment(form: CommentForm) {
this.setAuth(commentForm); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm)); this.ws.send(this.wsSendWrapper(UserOperation.EditComment, form));
}
public deleteComment(form: DeleteCommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeleteComment, form));
}
public removeComment(form: RemoveCommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemoveComment, form));
}
public markCommentAsRead(form: MarkCommentAsReadForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.MarkCommentAsRead, form));
} }
public likeComment(form: CommentLikeForm) { public likeComment(form: CommentLikeForm) {
@ -190,9 +222,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form)); this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
} }
public editPost(postForm: PostForm) { public editPost(form: PostForm) {
this.setAuth(postForm); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm)); this.ws.send(this.wsSendWrapper(UserOperation.EditPost, form));
}
public deletePost(form: DeletePostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeletePost, form));
}
public removePost(form: RemovePostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemovePost, form));
}
public lockPost(form: LockPostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.LockPost, form));
}
public stickyPost(form: StickyPostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.StickyPost, form));
} }
public savePost(form: SavePostForm) { public savePost(form: SavePostForm) {
@ -245,9 +297,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form)); this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
} }
public editUserMention(form: EditUserMentionForm) { public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
this.setAuth(form); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form)); this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form));
} }
public getModlog(form: GetModlogForm) { public getModlog(form: GetModlogForm) {
@ -315,6 +367,18 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form)); this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
} }
public deletePrivateMessage(form: DeletePrivateMessageForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form));
}
public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
this.setAuth(form);
this.ws.send(
this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form)
);
}
public getPrivateMessages(form: GetPrivateMessagesForm) { public getPrivateMessages(form: GetPrivateMessagesForm) {
this.setAuth(form); this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form)); this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));

View file

@ -256,6 +256,7 @@
"couldnt_save_post": "Couldn't save post.", "couldnt_save_post": "Couldn't save post.",
"no_slurs": "No slurs.", "no_slurs": "No slurs.",
"not_an_admin": "Not an admin.", "not_an_admin": "Not an admin.",
"not_a_moderator": "Not a moderator.",
"site_already_exists": "Site already exists.", "site_already_exists": "Site already exists.",
"couldnt_update_site": "Couldn't update site.", "couldnt_update_site": "Couldn't update site.",
"couldnt_find_that_username_or_email": "couldnt_find_that_username_or_email":