Browse Source

Merge pull request #997 from LemmyNet/api_edit_separation

Api edit separation.
sticky_locked_federation_tests
Felix Ableitner 1 year ago
committed by GitHub
parent
commit
227f397d5e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 576
      docs/src/contributing_websocket_http_api.md
  2. 2
      server/Cargo.lock
  3. 4
      server/lemmy_db/Cargo.toml
  4. 48
      server/lemmy_db/src/comment.rs
  5. 56
      server/lemmy_db/src/community.rs
  6. 8
      server/lemmy_db/src/community_view.rs
  7. 21
      server/lemmy_db/src/lib.rs
  8. 44
      server/lemmy_db/src/post.rs
  9. 52
      server/lemmy_db/src/private_message.rs
  10. 14
      server/lemmy_db/src/user.rs
  11. 24
      server/lemmy_db/src/user_mention.rs
  12. 2
      server/lemmy_utils/Cargo.toml
  13. 11
      server/lemmy_utils/src/lib.rs
  14. 14
      server/src/api/claims.rs
  15. 528
      server/src/api/comment.rs
  16. 336
      server/src/api/community.rs
  17. 34
      server/src/api/mod.rs
  18. 508
      server/src/api/post.rs
  19. 19
      server/src/api/site.rs
  20. 329
      server/src/api/user.rs
  21. 13
      server/src/apub/shared_inbox.rs
  22. 3
      server/src/apub/user.rs
  23. 27
      server/src/routes/api.rs
  24. 13
      server/src/websocket/mod.rs
  25. 28
      server/src/websocket/server.rs
  26. 184
      ui/src/api_tests/api.spec.ts
  27. 45
      ui/src/components/comment-form.tsx
  28. 36
      ui/src/components/comment-node.tsx
  29. 22
      ui/src/components/community.tsx
  30. 57
      ui/src/components/inbox.tsx
  31. 6
      ui/src/components/main.tsx
  32. 5
      ui/src/components/markdown-textarea.tsx
  33. 4
      ui/src/components/post-form.tsx
  34. 40
      ui/src/components/post-listing.tsx
  35. 32
      ui/src/components/post.tsx
  36. 6
      ui/src/components/private-message-form.tsx
  37. 11
      ui/src/components/private-message.tsx
  38. 21
      ui/src/components/sidebar.tsx
  39. 26
      ui/src/components/user-details.tsx
  40. 53
      ui/src/components/user.tsx
  41. 123
      ui/src/interfaces.ts
  42. 114
      ui/src/services/WebSocketService.ts
  43. 1
      ui/translations/en.json

576
docs/src/contributing_websocket_http_api.md

@ -17,6 +17,7 @@
- [Errors](#errors)
- [API documentation](#api-documentation)
* [Sort Types](#sort-types)
* [Undoing actions](#undoing-actions)
* [Websocket vs HTTP](#websocket-vs-http)
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
+ [Login](#login)
@ -43,142 +44,198 @@
- [Request](#request-5)
- [Response](#response-5)
- [HTTP](#http-6)
+ [Edit User Mention](#edit-user-mention)
+ [Mark User Mention as read](#mark-user-mention-as-read)
- [Request](#request-6)
- [Response](#response-6)
- [HTTP](#http-7)
+ [Mark All As Read](#mark-all-as-read)
+ [Get Private Messages](#get-private-messages)
- [Request](#request-7)
- [Response](#response-7)
- [HTTP](#http-8)
+ [Delete Account](#delete-account)
+ [Create Private Message](#create-private-message)
- [Request](#request-8)
- [Response](#response-8)
- [HTTP](#http-9)
+ [Add admin](#add-admin)
+ [Edit Private Message](#edit-private-message)
- [Request](#request-9)
- [Response](#response-9)
- [HTTP](#http-10)
+ [Ban user](#ban-user)
+ [Delete Private Message](#delete-private-message)
- [Request](#request-10)
- [Response](#response-10)
- [HTTP](#http-11)
* [Site](#site)
+ [List Categories](#list-categories)
+ [Mark Private Message as Read](#mark-private-message-as-read)
- [Request](#request-11)
- [Response](#response-11)
- [HTTP](#http-12)
+ [Search](#search)
+ [Mark All As Read](#mark-all-as-read)
- [Request](#request-12)
- [Response](#response-12)
- [HTTP](#http-13)
+ [Get Modlog](#get-modlog)
+ [Delete Account](#delete-account)
- [Request](#request-13)
- [Response](#response-13)
- [HTTP](#http-14)
+ [Create Site](#create-site)
+ [Add admin](#add-admin)
- [Request](#request-14)
- [Response](#response-14)
- [HTTP](#http-15)
+ [Edit Site](#edit-site)
+ [Ban user](#ban-user)
- [Request](#request-15)
- [Response](#response-15)
- [HTTP](#http-16)
+ [Get Site](#get-site)
* [Site](#site)
+ [List Categories](#list-categories)
- [Request](#request-16)
- [Response](#response-16)
- [HTTP](#http-17)
+ [Transfer Site](#transfer-site)
+ [Search](#search)
- [Request](#request-17)
- [Response](#response-17)
- [HTTP](#http-18)
+ [Get Site Config](#get-site-config)
+ [Get Modlog](#get-modlog)
- [Request](#request-18)
- [Response](#response-18)
- [HTTP](#http-19)
+ [Save Site Config](#save-site-config)
+ [Create Site](#create-site)
- [Request](#request-19)
- [Response](#response-19)
- [HTTP](#http-20)
* [Community](#community)
+ [Get Community](#get-community)
+ [Edit Site](#edit-site)
- [Request](#request-20)
- [Response](#response-20)
- [HTTP](#http-21)
+ [Create Community](#create-community)
+ [Get Site](#get-site)
- [Request](#request-21)
- [Response](#response-21)
- [HTTP](#http-22)
+ [List Communities](#list-communities)
+ [Transfer Site](#transfer-site)
- [Request](#request-22)
- [Response](#response-22)
- [HTTP](#http-23)
+ [Ban from Community](#ban-from-community)
+ [Get Site Config](#get-site-config)
- [Request](#request-23)
- [Response](#response-23)
- [HTTP](#http-24)
+ [Add Mod to Community](#add-mod-to-community)
+ [Save Site Config](#save-site-config)
- [Request](#request-24)
- [Response](#response-24)
- [HTTP](#http-25)
+ [Edit Community](#edit-community)
* [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-25)
- [Response](#response-25)
- [HTTP](#http-26)
+ [Follow Community](#follow-community)
+ [Create Community](#create-community)
- [Request](#request-26)
- [Response](#response-26)
- [HTTP](#http-27)
+ [Get Followed Communities](#get-followed-communities)
+ [List Communities](#list-communities)
- [Request](#request-27)
- [Response](#response-27)
- [HTTP](#http-28)
+ [Transfer Community](#transfer-community)
+ [Ban from Community](#ban-from-community)
- [Request](#request-28)
- [Response](#response-28)
- [HTTP](#http-29)
* [Post](#post)
+ [Create Post](#create-post)
+ [Add Mod to Community](#add-mod-to-community)
- [Request](#request-29)
- [Response](#response-29)
- [HTTP](#http-30)
+ [Get Post](#get-post)
+ [Edit Community](#edit-community)
- [Request](#request-30)
- [Response](#response-30)
- [HTTP](#http-31)
+ [Get Posts](#get-posts)
+ [Delete Community](#delete-community)
- [Request](#request-31)
- [Response](#response-31)
- [HTTP](#http-32)
+ [Create Post Like](#create-post-like)
+ [Remove Community](#remove-community)
- [Request](#request-32)
- [Response](#response-32)
- [HTTP](#http-33)
+ [Edit Post](#edit-post)
+ [Follow Community](#follow-community)
- [Request](#request-33)
- [Response](#response-33)
- [HTTP](#http-34)
+ [Save Post](#save-post)
+ [Get Followed Communities](#get-followed-communities)
- [Request](#request-34)
- [Response](#response-34)
- [HTTP](#http-35)
* [Comment](#comment)
+ [Create Comment](#create-comment)
+ [Transfer Community](#transfer-community)
- [Request](#request-35)
- [Response](#response-35)
- [HTTP](#http-36)
+ [Edit Comment](#edit-comment)
* [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-36)
- [Response](#response-36)
- [HTTP](#http-37)
+ [Save Comment](#save-comment)
+ [Get Post](#get-post)
- [Request](#request-37)
- [Response](#response-37)
- [HTTP](#http-38)
+ [Create Comment Like](#create-comment-like)
+ [Get Posts](#get-posts)
- [Request](#request-38)
- [Response](#response-38)
- [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)
+ [All](#all)
+ [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.
- `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
- 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`
#### Edit User Mention
#### Mark User Mention as read
Only the recipient can do this.
##### Request
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
user_mention_id: i32,
read: Option<bool>,
read: bool,
auth: String,
}
}
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
##### Response
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
mention: UserMentionView,
}
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
```
##### 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
@ -856,7 +1054,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
data: {
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -973,7 +1170,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /community/mod`
#### Edit Community
Mods and admins can remove and lock a community, creators can delete it.
Only mods can edit a community.
##### Request
```rust
@ -984,10 +1181,6 @@ Mods and admins can remove and lock a community, creators can delete it.
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
expires: Option<i64>,
auth: String
}
}
@ -1005,6 +1198,62 @@ Mods and admins can remove and lock a community, creators can delete it.
`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
##### Request
```rust
@ -1090,8 +1339,9 @@ Mods and admins can remove and lock a community, creators can delete it.
name: String,
url: Option<String>,
body: Option<String>,
nsfw: bool,
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>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -1197,25 +1446,17 @@ Post listing types are `All, Subscribed, Community`
`POST /post/like`
#### Edit Post
Mods and admins can remove and lock a post, creators can delete it.
##### Request
```rust
{
op: "EditPost",
data: {
edit_id: i32,
creator_id: i32,
community_id: i32,
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
locked: Option<bool>,
reason: Option<String>,
auth: String
nsfw: bool,
auth: String,
}
}
```
@ -1233,6 +1474,120 @@ Mods and admins can remove and lock a post, creators can delete it.
`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
##### Request
```rust
@ -1267,8 +1622,8 @@ Mods and admins can remove and lock a post, creators can delete it.
data: {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>,
post_id: i32,
form_id: Option<String>, // An optional form id, so you know which message came back
auth: String
}
}
@ -1289,7 +1644,7 @@ Mods and admins can remove and lock a post, creators can delete it.
#### Edit Comment
Mods and admins can remove a comment, creators can delete it.
Only the creator can edit the comment.
##### Request
```rust
@ -1297,15 +1652,9 @@ Mods and admins can remove a comment, creators can delete it.
op: "EditComment",
data: {
content: String,
parent_id: Option<i32>,
edit_id: i32,
creator_id: i32,
post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
read: Option<bool>,
auth: String
form_id: Option<String>,
auth: String,
}
}
```
@ -1322,6 +1671,92 @@ Mods and admins can remove a comment, creators can delete it.
`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
##### Request
```rust
@ -1357,7 +1792,6 @@ Mods and admins can remove a comment, creators can delete it.
op: "CreateCommentLike",
data: {
comment_id: i32,
post_id: i32,
score: i16,
auth: String
}

2
server/Cargo.lock

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

4
server/lemmy_db/Cargo.toml

@ -13,4 +13,6 @@ strum_macros = "0.18.0"
log = "0.4.0"
sha2 = "0.9"
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"

48
server/lemmy_db/src/comment.rs

@ -97,14 +97,6 @@ impl Comment {
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> {
use crate::schema::comment::dsl::*;
@ -116,6 +108,46 @@ impl Comment {
))
.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)]

56
server/lemmy_db/src/community.rs

@ -1,4 +1,5 @@
use crate::{
naive_now,
schema::{community, community_follower, community_moderator, community_user_ban},
Bannable,
Crud,
@ -29,7 +30,6 @@ pub struct Community {
pub last_refreshed_at: chrono::NaiveDateTime,
}
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
@ -99,6 +99,60 @@ impl Community {
use crate::schema::community::dsl::*;
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)]

8
server/lemmy_db/src/community_view.rs

@ -295,18 +295,18 @@ pub struct 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::*;
community_moderator_view
.filter(community_id.eq(from_community_id))
.filter(community_id.eq(for_community_id))
.order_by(published)
.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::*;
community_moderator_view
.filter(user_id.eq(from_user_id))
.filter(user_id.eq(for_user_id))
.order_by(published)
.load::<Self>(conn)
}

21
server/lemmy_db/src/lib.rs

@ -2,9 +2,12 @@
pub extern crate diesel;
#[macro_use]
pub extern crate strum_macros;
#[macro_use]
pub extern crate lazy_static;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate log;
pub extern crate regex;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
@ -12,6 +15,7 @@ pub extern crate strum;
use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
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)]
mod tests {
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};
pub fn establish_unpooled_connection() -> PgConnection {
@ -194,4 +207,10 @@ mod tests {
let test = "This is a fuzzy search";
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"));
}
}

44
server/lemmy_db/src/post.rs

@ -108,6 +108,50 @@ impl Post {
))
.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 {

52
server/lemmy_db/src/private_message.rs

@ -1,4 +1,4 @@
use crate::{schema::private_message, Crud};
use crate::{naive_now, schema::private_message, Crud};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
@ -80,6 +80,50 @@ impl PrivateMessage {
.filter(ap_id.eq(object_id))
.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)]
@ -180,6 +224,10 @@ mod tests {
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
let updated_private_message =
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
let 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();
User_::delete(&conn, inserted_creator.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, updated_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);
}
}

14
server/lemmy_db/src/user.rs

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

24
server/lemmy_db/src/user_mention.rs

@ -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)]
mod tests {
use crate::{

2
server/lemmy_utils/Cargo.toml

@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]}
comrak = "0.7"
lazy_static = "1.3.0"
openssl = "0.10"
url = { version = "2.1.1", features = ["serde"] }
url = { version = "2.1.1", features = ["serde"] }

11
server/lemmy_utils/src/lib.rs

@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
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 {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
#[cfg(test)]
mod tests {
use crate::{
is_email_regex,
is_valid_community_name,
is_valid_post_title,
is_valid_username,
@ -185,12 +180,6 @@ mod tests {
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]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));

14
server/src/api/claims.rs

@ -1,7 +1,7 @@
use diesel::{result::Error, PgConnection};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings};
use lemmy_utils::settings::Settings;
use serde::{Deserialize, Serialize};
type Jwt = String;
@ -54,18 +54,6 @@ impl Claims {
.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> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id)

528
server/src/api/comment.rs

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
blocking,
websocket::{
@ -15,12 +15,10 @@ use lemmy_db::{
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
@ -44,22 +42,38 @@ use std::str::FromStr;
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>, // TODO this isn't used
pub post_id: i32,
form_id: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct EditComment {
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,
creator_id: i32,
pub post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
form_id: Option<String>,
auth: String,
}
#[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>,
read: Option<bool>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkCommentAsRead {
edit_id: i32,
read: bool,
auth: String,
}
@ -74,12 +88,12 @@ pub struct SaveComment {
pub struct CommentResponse {
pub comment: CommentView,
pub recipient_ids: Vec<i32>,
pub form_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentLike {
comment_id: i32,
pub post_id: i32,
score: i16,
auth: String,
}
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
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 inserted_comment =
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()),
};
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| {
let apub_id =
@ -175,8 +196,15 @@ impl Perform for Oper<CreateComment> {
// Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids =
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
let recipient_ids = send_local_notifs(
mentions,
updated_comment.clone(),
user.clone(),
post,
pool,
true,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
@ -201,6 +229,7 @@ impl Perform for Oper<CreateComment> {
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
if let Some(ws) = websocket_info {
@ -237,122 +266,128 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
let mut editors: Vec<i32> = vec![orig_comment.creator_id];
let mut moderators: Vec<i32> = vec![];
// 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;
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
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());
}
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());
}
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
}
} else {
// check that user can mark as read
let parent_id = orig_comment.parent_id;
match parent_id {
Some(pid) => {
let parent_comment =