diff --git a/README.md b/README.md index 3668d147e..d4ccab2e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg) -[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy) +[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=main)](https://travis-ci.org/LemmyNet/lemmy) [![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues) [![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/) [![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/) @@ -26,7 +26,7 @@ · Request Feature · - Releases + Releases

@@ -34,7 +34,7 @@ Front Page|Post ---|--- -![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png) +![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png) [Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). diff --git a/ansible/VERSION b/ansible/VERSION index 51ae6cf92..ced68c150 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.7.20 +v0.7.26 diff --git a/ansible/templates/nginx.conf b/ansible/templates/nginx.conf index 5847bad01..eaaa6b79e 100644 --- a/ansible/templates/nginx.conf +++ b/ansible/templates/nginx.conf @@ -1,4 +1,3 @@ -proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off; limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s; server { @@ -65,13 +64,6 @@ server { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - - # Proxy Cache - proxy_cache lemmy_frontend_cache; - proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; - proxy_cache_revalidate on; - proxy_cache_lock on; - proxy_cache_min_uses 5; } # Redirect pictshare images to pictrs diff --git a/docker/prod/deploy.sh b/docker/prod/deploy.sh index 2c6e3d312..2e8728180 100755 --- a/docker/prod/deploy.sh +++ b/docker/prod/deploy.sh @@ -1,10 +1,10 @@ #!/bin/sh set -e -git checkout master +git checkout main # Import translations git fetch weblate -git merge weblate/master +git merge weblate/main # Creating the new tag new_tag="$1" @@ -12,8 +12,6 @@ third_semver=$(echo $new_tag | cut -d "." -f 3) # Setting the version on the front end cd ../../ -echo "export const version: string = '$new_tag';" > "ui/src/version.ts" -git add "ui/src/version.ts" # Setting the version on the backend echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs" git add "server/src/version.rs" diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 7feb65d33..3affe5f06 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -12,7 +12,7 @@ services: restart: always lemmy: - image: dessalines/lemmy:v0.7.20 + image: dessalines/lemmy:v0.7.26 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/docs/src/about.md b/docs/src/about.md index 2c0e418b3..8db35b4f6 100644 --- a/docs/src/about.md +++ b/docs/src/about.md @@ -2,7 +2,7 @@ Front Page|Post ---|--- -![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png) +![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png) [Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). diff --git a/docs/src/about_guide.md b/docs/src/about_guide.md index 6f709b580..3c5e34b4b 100644 --- a/docs/src/about_guide.md +++ b/docs/src/about_guide.md @@ -35,6 +35,8 @@ Horizontal Rule
\--- | Horizontal Rule
\*\*\* | Horizontal Rule

\`Inline code\` with backticks | |`Inline code` with backticks \`\`\`
\# code block
print '3 backticks or'
print 'indent 4 spaces'
\`\`\` | ····\# code block
····print '3 backticks or'
····print 'indent 4 spaces' | \# code block
print '3 backticks or'
print 'indent 4 spaces' ::: spoiler hidden or nsfw stuff
*a bunch of spoilers here*
::: | |
hidden or nsfw stuff

a bunch of spoilers here

+Some ~subscript~ text | | Some subscript text +Some ^superscript^ text | | Some superscript text [CommonMark Tutorial](https://commonmark.org/help/tutorial/) diff --git a/docs/src/about_ranking.md b/docs/src/about_ranking.md index fe9e82bbb..f1ed9b386 100644 --- a/docs/src/about_ranking.md +++ b/docs/src/about_ranking.md @@ -26,4 +26,4 @@ Gravity = Decay gravity, 1.8 is default A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k. -![](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/rank_algorithm.png) +![](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/rank_algorithm.png) diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md index cc4c56898..c2aef3f7f 100644 --- a/docs/src/administration_configuration.md +++ b/docs/src/administration_configuration.md @@ -1,7 +1,7 @@ # Configuration The configuration is based on the file -[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/master/server/config/defaults.hjson). +[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/main/server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file. diff --git a/docs/src/administration_install_ansible.md b/docs/src/administration_install_ansible.md index 4676f47d4..849957ad1 100644 --- a/docs/src/administration_install_ansible.md +++ b/docs/src/administration_install_ansible.md @@ -19,7 +19,7 @@ ansible-playbook lemmy.yml --become To update to a new version, just run the following in your local Lemmy repo: ```bash -git pull origin master +git pull origin main cd ansible ansible-playbook lemmy.yml --become ``` diff --git a/docs/src/administration_install_docker.md b/docs/src/administration_install_docker.md index a2bed794f..ae0375a81 100644 --- a/docs/src/administration_install_docker.md +++ b/docs/src/administration_install_docker.md @@ -8,9 +8,9 @@ mkdir /lemmy cd /lemmy # download default config files -wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml -wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson -wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/iframely.config.local.js +wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml +wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/lemmy.hjson +wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/iframely.config.local.js # Set correct permissions for pictrs folder mkdir -p volumes/pictrs @@ -21,10 +21,10 @@ After this, have a look at the [config file](administration_configuration.md) na `docker-compose up -d` -To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf), could be setup with: +To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf), could be setup with: ```bash -wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf +wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf # Replace the {{ vars }} sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf ``` @@ -36,6 +36,6 @@ You will also need to setup TLS, for example with [Let's Encrypt](https://letsen To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo: ```bash -wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml +wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml docker-compose up -d ``` diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md index 066386f50..9bdeff9ad 100644 --- a/docs/src/contributing_local_development.md +++ b/docs/src/contributing_local_development.md @@ -1,16 +1,26 @@ -### Ubuntu - - -#### Build requirements: +### Install build requirements +#### Ubuntu ``` -sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 git +sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 # install yarn curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt update && sudo apt install yarn ``` -#### Get the source code +#### macOS + +Install Rust using [the recommended option on rust-lang.org](https://www.rust-lang.org/tools/install) (rustup). + +Then, install [Homebrew](https://brew.sh/) if you don't already have it installed. + +Finally, install Node and Yarn. + +``` +brew install node yarn +``` + +### Get the source code ``` git clone https://github.com/LemmyNet/lemmy.git # or alternatively from gitea @@ -20,36 +30,49 @@ git clone https://github.com/LemmyNet/lemmy.git All the following commands need to be run either in `lemmy/server` or `lemmy/ui`, as indicated by the `cd` command. -#### Build the backend (Rust) +### Build the backend (Rust) ``` cd server cargo build # for development, use `cargo check` instead) ``` -#### Build the frontend (Typescript) +### Build the frontend (Typescript) ``` cd ui yarn yarn build ``` -#### Setup postgresql +### Setup postgresql +#### Ubuntu ``` sudo apt install postgresql sudo systemctl start postgresql -# initialize postgres database + +# Either execute server/db-init.sh, or manually initialize the postgres database: sudo -u postgres psql -c "create user lemmy with password 'password' superuser;" -U postgres sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -U postgres export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy -# or execute server/db-init.sh ``` -#### Run a local development instance +#### macOS +``` +brew install postgresql +brew services start postgresql +/usr/local/opt/postgres/bin/createuser -s postgres + +# Either execute server/db-init.sh, or manually initialize the postgres database: +psql -c "create user lemmy with password 'password' superuser;" -U postgres +psql -c 'create database lemmy with owner lemmy;' -U postgres +export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy +``` + +### Run a local development instance ``` # run each of these in a seperate terminal cd server && cargo run -ui & yarn start +cd ui && yarn start ``` Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit diff --git a/docs/src/contributing_websocket_http_api.md b/docs/src/contributing_websocket_http_api.md index 6ed25b98e..62cb1fc44 100644 --- a/docs/src/contributing_websocket_http_api.md +++ b/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, + 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, + limit: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "GetPrivateMessages", + data: { + messages: Vec, + } +} +``` + +##### 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 @@ -754,6 +952,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url` site: Option, admins: Vec, banned: Vec, + online: usize, // This is currently broken + version: String, } } ``` @@ -854,7 +1054,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url` data: { community: CommunityView, moderators: Vec, - admins: Vec, } } ``` @@ -971,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 @@ -979,14 +1178,9 @@ Mods and admins can remove and lock a community, creators can delete it. op: "EditCommunity", data: { edit_id: i32, - name: String, title: String, description: Option, category_id: i32, - removed: Option, - deleted: Option, - reason: Option, - expires: Option, auth: String } } @@ -1004,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, + expires: Option, + auth: String, + } +} +``` +##### Response +```rust +{ + op: "RemoveCommunity", + data: { + community: CommunityView + } +} +``` +##### HTTP + +`POST /community/remove` + #### Follow Community ##### Request ```rust @@ -1089,8 +1339,9 @@ Mods and admins can remove and lock a community, creators can delete it. name: String, url: Option, body: Option, + nsfw: bool, community_id: i32, - auth: String + auth: String, } } ``` @@ -1127,7 +1378,6 @@ Mods and admins can remove and lock a community, creators can delete it. comments: Vec, community: CommunityView, moderators: Vec, - admins: Vec, } } ``` @@ -1196,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, body: Option, - removed: Option, - deleted: Option, - locked: Option, - reason: Option, - auth: String + nsfw: bool, + auth: String, } } ``` @@ -1232,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, + 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 @@ -1266,8 +1622,8 @@ Mods and admins can remove and lock a post, creators can delete it. data: { content: String, parent_id: Option, - edit_id: Option, post_id: i32, + form_id: Option, // An optional form id, so you know which message came back auth: String } } @@ -1288,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 @@ -1296,15 +1652,9 @@ Mods and admins can remove a comment, creators can delete it. op: "EditComment", data: { content: String, - parent_id: Option, edit_id: i32, - creator_id: i32, - post_id: i32, - removed: Option, - deleted: Option, - reason: Option, - read: Option, - auth: String + form_id: Option, + auth: String, } } ``` @@ -1321,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, + 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 @@ -1356,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 } diff --git a/server/Cargo.lock b/server/Cargo.lock index d90b96799..a6171cf50 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -1,35 +1,9 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "activitystreams" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464cb473bfb402b857cc15b1153974c203a43f1485da4dda15cd17a738548958" -dependencies = [ - "activitystreams-derive", - "chrono", - "mime", - "serde 1.0.114", - "serde_json", - "thiserror", - "url", -] - -[[package]] -name = "activitystreams-derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "activitystreams-ext" version = "0.1.0" -source = "git+https://git.asonix.dog/asonix/activitystreams-ext#e5c97f4ea9f60e49bc7ff27fb0fb515d3190fd25" +source = "git+https://yerbamate.dev/asonix/activitystreams-ext?branch=main#2799a4c606467a2f577e1f45f93c6828ec83cfdf" dependencies = [ "activitystreams-new", "serde 1.0.114", @@ -39,12 +13,14 @@ dependencies = [ [[package]] name = "activitystreams-new" version = "0.1.0" -source = "git+https://git.asonix.dog/asonix/activitystreams-sketch#99c7e9aa5596eda846a1ebd5978ca72d11d4c08a" +source = "git+https://yerbamate.dev/asonix/activitystreams-new?branch=main#857d5167dfa13054dd0d21d3d54f8147eea0d546" dependencies = [ - "activitystreams", + "chrono", + "mime", "serde 1.0.114", "serde_json", - "typed-builder", + "thiserror", + "url", ] [[package]] @@ -111,9 +87,9 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.3.0-alpha.1" +version = "0.3.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b32e0fdd5998c2712549cbc39dff46c8754d55e3dd9f4d017d9e28de30cac6" +checksum = "627f597ad98061816766201db8afc7444752992f2919b2e60f53a7fa27f01aed" dependencies = [ "actix-http", "actix-service", @@ -132,9 +108,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "2.0.0-alpha.4" +version = "2.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7ea0568480d199952a51de70271946da57c33cc0e8b83f54383e70958dff21" +checksum = "33f501768e82e8548763b7f55309e2f8bcc7f9f4273c75b47af99ac2b2581f37" dependencies = [ "actix-codec", "actix-connect", @@ -147,6 +123,7 @@ dependencies = [ "bitflags", "brotli2", "bytes", + "cookie", "copyless", "derive_more", "either", @@ -160,6 +137,7 @@ dependencies = [ "http", "httparse", "indexmap", + "itoa", "language-tags", "lazy_static", "log", @@ -171,7 +149,7 @@ dependencies = [ "serde 1.0.114", "serde_json", "serde_urlencoded", - "sha-1", + "sha-1 0.9.1", "slab", "time 0.2.16", ] @@ -260,16 +238,16 @@ dependencies = [ [[package]] name = "actix-threadpool" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" dependencies = [ "derive_more", "futures-channel", "lazy_static", "log", "num_cpus", - "parking_lot 0.10.2", + "parking_lot 0.11.0", "threadpool", ] @@ -313,9 +291,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "3.0.0-alpha.3" +version = "3.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd6df56ec5f9a1a0d8335f156f36e1e8f76dbd736fa0cc0f6bc3a69be1e6124" +checksum = "9125c29b7d9911bfdb4d0d4d8f1cf4fee4f21515cf2a405a423c30c245364297" dependencies = [ "actix-codec", "actix-http", @@ -353,24 +331,25 @@ dependencies = [ [[package]] name = "actix-web-actors" -version = "3.0.0-alpha.1" +version = "3.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5efeb3907582f9c724ce27be093ab8aafabd97be828bc6750c0d467f5e1aa3" +checksum = "55ef22b33c49a28dda61866d5573c5b8ceb080a099cd59e7371b78b48bbf1bc0" dependencies = [ "actix", "actix-codec", "actix-http", "actix-web", "bytes", - "futures", + "futures-channel", + "futures-core", "pin-project", ] [[package]] name = "actix-web-codegen" -version = "0.2.2" +version = "0.3.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766" +checksum = "df9679f5b1f4c819de08b63b0a61a131b2fdc30b367c2c208984fda8eaa07fa0" dependencies = [ "proc-macro2", "quote", @@ -390,24 +369,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" dependencies = [ "gimli", ] [[package]] name = "adler" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10" - -[[package]] -name = "adler32" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" [[package]] name = "aho-corasick" @@ -481,9 +454,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "awc" -version = "2.0.0-alpha.2" +version = "2.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7038a9747cd5159b9f0550895eaf865c0143baa7e4eee834e9294d0a7e0e4be" +checksum = "374057b508d4083208996be82141891c2e14c8885f45991b21c1621200ab6df3" dependencies = [ "actix-codec", "actix-http", @@ -505,14 +478,14 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c" +checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.3.7", + "miniz_oxide", "object", "rustc-demangle", ] @@ -590,7 +563,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.2", + "generic-array 0.14.3", ] [[package]] @@ -599,7 +572,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" dependencies = [ - "generic-array 0.14.2", + "generic-array 0.14.3", ] [[package]] @@ -642,6 +615,15 @@ dependencies = [ "libc", ] +[[package]] +name = "buf-min" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4" +dependencies = [ + "bytes", +] + [[package]] name = "bufstream" version = "0.1.4" @@ -668,12 +650,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "bytes" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b" -dependencies = [ - "loom", -] +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytestring" @@ -686,9 +665,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe" +checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" [[package]] name = "cfg-if" @@ -698,9 +677,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4" +checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" dependencies = [ "num-integer", "num-traits 0.2.12", @@ -770,6 +749,16 @@ dependencies = [ "serde-hjson", ] +[[package]] +name = "cookie" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca761767cf3fa9068cc893ec8c247a22d0fd0535848e65640c0548bd1f8bbb36" +dependencies = [ + "percent-encoding", + "time 0.2.16", +] + [[package]] name = "copyless" version = "0.1.5" @@ -794,9 +783,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "cpuid-bool" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" [[package]] name = "crc32fast" @@ -809,12 +798,12 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6" dependencies = [ + "cfg-if", "crossbeam-utils", - "maybe-uninit", ] [[package]] @@ -950,7 +939,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.2", + "generic-array 0.14.3", ] [[package]] @@ -1142,7 +1131,7 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide 0.4.0", + "miniz_oxide", ] [[package]] @@ -1292,19 +1281,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generator" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68" -dependencies = [ - "cc", - "libc", - "log", - "rustc_version", - "winapi 0.3.9", -] - [[package]] name = "generic-array" version = "0.12.3" @@ -1316,9 +1292,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" +checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63" dependencies = [ "typenum", "version_check 0.9.2", @@ -1337,15 +1313,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" [[package]] name = "h2" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" +checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" dependencies = [ "bytes", "fnv", @@ -1354,10 +1330,19 @@ dependencies = [ "futures-util", "http", "indexmap", - "log", "slab", "tokio", "tokio-util 0.3.1", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb" +dependencies = [ + "autocfg 1.0.0", ] [[package]] @@ -1371,9 +1356,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" dependencies = [ "libc", ] @@ -1472,18 +1457,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" +checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7" dependencies = [ "autocfg 1.0.0", + "hashbrown", ] [[package]] name = "instant" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69da7ce1490173c2bf4d26bc8be429aaeeaf4cce6c4b970b7949651fa17655fe" +checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" [[package]] name = "iovec" @@ -1523,9 +1509,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "js-sys" -version = "0.3.41" +version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916" +checksum = "52732a3d3ad72c58ad2dc70624f9c17b46ecd0943b9a4f1ee37c4c18c5d983e2" dependencies = [ "wasm-bindgen", ] @@ -1573,19 +1559,21 @@ dependencies = [ "bcrypt", "chrono", "diesel", + "lazy_static", "log", + "regex", "serde 1.0.114", "serde_json", "sha2", "strum", "strum_macros", + "url", ] [[package]] name = "lemmy_server" version = "0.0.1" dependencies = [ - "activitystreams", "activitystreams-ext", "activitystreams-new", "actix", @@ -1693,9 +1681,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.71" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" +checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" [[package]] name = "linked-hash-map" @@ -1724,33 +1712,22 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de302ce1fe7482db13738fbaf2e21cfb06a986b89c0bf38d88abf16681aada4e" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" dependencies = [ "scopeguard", ] [[package]] name = "log" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ "cfg-if", ] -[[package]] -name = "loom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", -] - [[package]] name = "lru-cache" version = "0.1.2" @@ -1778,12 +1755,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.3.3" @@ -1827,15 +1798,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - [[package]] name = "miniz_oxide" version = "0.4.0" @@ -2060,7 +2022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" dependencies = [ "instant", - "lock_api 0.4.0", + "lock_api 0.4.1", "parking_lot_core 0.8.0", ] @@ -2150,7 +2112,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" dependencies = [ "maplit", "pest", - "sha-1", + "sha-1 0.8.2", ] [[package]] @@ -2187,9 +2149,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" [[package]] name = "ppv-lite86" @@ -2220,9 +2182,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" dependencies = [ "unicode-xid", ] @@ -2434,9 +2396,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "regex" @@ -2559,12 +2521,6 @@ dependencies = [ "parking_lot 0.11.0", ] -[[package]] -name = "scoped-tls" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" - [[package]] name = "scopeguard" version = "1.1.0" @@ -2703,6 +2659,19 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha-1" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "sha1" version = "0.6.0" @@ -2734,9 +2703,9 @@ dependencies = [ [[package]] name = "simple_asn1" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" +checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" dependencies = [ "chrono", "num-bigint", @@ -2751,9 +2720,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" +checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" [[package]] name = "socket2" @@ -2869,9 +2838,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0" dependencies = [ "proc-macro2", "quote", @@ -3076,6 +3045,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "tracing" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e2a2de6b0d5cbb13fc21193a2296888eaab62b6044479aafb3c54c01c29fcd" +dependencies = [ + "cfg-if", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ae75f0d28ae10786f3b1895c55fe72e79928fd5ccdebb5438c75e93fec178f" +dependencies = [ + "lazy_static", +] + [[package]] name = "trust-dns-proto" version = "0.19.5" @@ -3132,17 +3121,6 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" -[[package]] -name = "typed-builder" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fc4459191c621a53ef6c6ca5642e6e0e5ccc61f3e5b8ad6b6ab5317f0200fb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "typenum" version = "1.12.0" @@ -3251,18 +3229,19 @@ dependencies = [ [[package]] name = "v_escape" -version = "0.7.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6" +checksum = "b66158ce426982197fd44266d68125fd4000f1d42f5ee33ef02b500b4b6b0024" dependencies = [ + "buf-min", "v_escape_derive", ] [[package]] name = "v_escape_derive" -version = "0.5.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae" +checksum = "cae7cffca0b1f9af9b20610f6fdeee9ffcce61417b5ad186a5d482dc904e24cd" dependencies = [ "nom 4.2.3", "proc-macro2", @@ -3272,9 +3251,9 @@ dependencies = [ [[package]] name = "v_htmlescape" -version = "0.4.5" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41" +checksum = "f5fd25529cb2f78527b5ee507bcfb357b26d057b5e480853c26d49a4ead5c629" dependencies = [ "cfg-if", "v_escape", @@ -3312,9 +3291,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasm-bindgen" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2" +checksum = "f3edbcc9536ab7eababcc6d2374a0b7bfe13a2b6d562c5e07f370456b1a8f33d" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3322,9 +3301,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df" +checksum = "89ed2fb8c84bfad20ea66b26a3743f3e7ba8735a69fe7d95118c33ec8fc1244d" dependencies = [ "bumpalo", "lazy_static", @@ -3337,9 +3316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8" +checksum = "eb071268b031a64d92fc6cf691715ca5a40950694d8f683c5bb43db7c730929e" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3347,9 +3326,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75" +checksum = "cf592c807080719d1ff2f245a687cbadb3ed28b2077ed7084b47aba8b691f2c6" dependencies = [ "proc-macro2", "quote", @@ -3360,15 +3339,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.64" +version = "0.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae" +checksum = "72b6c0220ded549d63860c78c38f3bcc558d1ca3f4efa74942c536ddbbb55e87" [[package]] name = "web-sys" -version = "0.3.41" +version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d" +checksum = "8be2398f326b7ba09815d0b403095f34dd708579220d099caae89be0b32137b2" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/server/Cargo.toml b/server/Cargo.toml index 2aa3c139b..356cbced5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,9 +18,8 @@ lemmy_db = { path = "./lemmy_db" } diesel = "1.4.4" diesel_migrations = "1.4.0" dotenv = "0.15.0" -activitystreams = "0.6.2" -activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" } -activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" } +activitystreams-new = { git = "https://yerbamate.dev/asonix/activitystreams-new", branch = "main" } +activitystreams-ext = { git = "https://yerbamate.dev/asonix/activitystreams-ext", branch = "main" } bcrypt = "0.8.0" chrono = { version = "0.4.7", features = ["serde"] } serde_json = { version = "1.0.52", features = ["preserve_order"]} diff --git a/server/lemmy_db/Cargo.toml b/server/lemmy_db/Cargo.toml index d94cf5fc6..6d342c1e7 100644 --- a/server/lemmy_db/Cargo.toml +++ b/server/lemmy_db/Cargo.toml @@ -12,4 +12,7 @@ strum = "0.18.0" strum_macros = "0.18.0" log = "0.4.0" sha2 = "0.9" -bcrypt = "0.8.0" \ No newline at end of file +bcrypt = "0.8.0" +url = { version = "2.1.1", features = ["serde"] } +lazy_static = "1.3.0" +regex = "1.3.5" diff --git a/server/lemmy_db/src/activity.rs b/server/lemmy_db/src/activity.rs index 83f85ca1e..557eb9e9e 100644 --- a/server/lemmy_db/src/activity.rs +++ b/server/lemmy_db/src/activity.rs @@ -117,7 +117,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_862362".into(), bio: None, local: true, private_key: None, diff --git a/server/lemmy_db/src/comment.rs b/server/lemmy_db/src/comment.rs index 602070d51..de6904133 100644 --- a/server/lemmy_db/src/comment.rs +++ b/server/lemmy_db/src/comment.rs @@ -1,5 +1,6 @@ use super::{post::Post, *}; use crate::schema::{comment, comment_like, comment_saved}; +use url::{ParseError, Url}; // WITH RECURSIVE MyTree AS ( // SELECT * FROM comment WHERE parent_id IS NULL @@ -42,6 +43,12 @@ pub struct CommentForm { pub local: bool, } +impl CommentForm { + pub fn get_ap_id(&self) -> Result { + Url::parse(&self.ap_id) + } +} + impl Crud for Comment { fn read(conn: &PgConnection, comment_id: i32) -> Result { use crate::schema::comment::dsl::*; @@ -90,14 +97,6 @@ impl Comment { comment.filter(ap_id.eq(object_id)).first::(conn) } - pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result { - use crate::schema::comment::dsl::*; - - diesel::update(comment.find(comment_id)) - .set(read.eq(true)) - .get_result::(conn) - } - pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result { use crate::schema::comment::dsl::*; @@ -109,6 +108,46 @@ impl Comment { )) .get_result::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + comment_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + comment_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn update_content( + conn: &PgConnection, + comment_id: i32, + new_content: &str, + ) -> Result { + use crate::schema::comment::dsl::*; + diesel::update(comment.find(comment_id)) + .set((content.eq(new_content), updated.eq(naive_now()))) + .get_result::(conn) + } } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] @@ -226,7 +265,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_283687".into(), bio: None, local: true, private_key: None, @@ -246,7 +285,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_928738972".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/comment_view.rs b/server/lemmy_db/src/comment_view.rs index 4af13c2d9..7f99ba4ad 100644 --- a/server/lemmy_db/src/comment_view.rs +++ b/server/lemmy_db/src/comment_view.rs @@ -498,7 +498,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_92873982".into(), bio: None, local: true, private_key: None, @@ -518,7 +518,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_7625376".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/community.rs b/server/lemmy_db/src/community.rs index 607520803..3a78d769d 100644 --- a/server/lemmy_db/src/community.rs +++ b/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 { @@ -88,10 +88,10 @@ impl Community { .first::(conn) } - pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result { + pub fn read_from_actor_id(conn: &PgConnection, for_actor_id: &str) -> Result { use crate::schema::community::dsl::*; community - .filter(actor_id.eq(community_id)) + .filter(actor_id.eq(for_actor_id)) .first::(conn) } @@ -99,6 +99,60 @@ impl Community { use crate::schema::community::dsl::*; community.filter(local.eq(true)).load::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + community_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + community_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_creator( + conn: &PgConnection, + community_id: i32, + new_creator_id: i32, + ) -> Result { + use crate::schema::community::dsl::*; + diesel::update(community.find(community_id)) + .set((creator_id.eq(new_creator_id), updated.eq(naive_now()))) + .get_result::(conn) + } + + fn community_mods_and_admins( + conn: &PgConnection, + community_id: i32, + ) -> Result, Error> { + use crate::{community_view::CommunityModeratorView, user_view::UserView}; + let mut mods_and_admins: Vec = 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)] @@ -258,7 +312,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_8266238".into(), bio: None, local: true, private_key: None, @@ -278,7 +332,7 @@ mod tests { removed: None, deleted: None, updated: None, - actor_id: "http://fake.com".into(), + actor_id: "changeme_7625376".into(), local: true, private_key: None, public_key: None, @@ -300,7 +354,7 @@ mod tests { deleted: false, published: inserted_community.published, updated: None, - actor_id: "http://fake.com".into(), + actor_id: inserted_community.actor_id.to_owned(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/community_view.rs b/server/lemmy_db/src/community_view.rs index 5c6bd81a1..880c94559 100644 --- a/server/lemmy_db/src/community_view.rs +++ b/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, Error> { + pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result, 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::(conn) } - pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result, Error> { + pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result, 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::(conn) } diff --git a/server/lemmy_db/src/lib.rs b/server/lemmy_db/src/lib.rs index 2eead841d..cca2994b8 100644 --- a/server/lemmy_db/src/lib.rs +++ b/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")); + } } diff --git a/server/lemmy_db/src/moderator.rs b/server/lemmy_db/src/moderator.rs index f5d33d967..0992197b1 100644 --- a/server/lemmy_db/src/moderator.rs +++ b/server/lemmy_db/src/moderator.rs @@ -470,7 +470,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_829398".into(), bio: None, local: true, private_key: None, @@ -497,7 +497,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_82982738".into(), bio: None, local: true, private_key: None, @@ -517,7 +517,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_283687".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/password_reset_request.rs b/server/lemmy_db/src/password_reset_request.rs index a2692add8..2529ba67a 100644 --- a/server/lemmy_db/src/password_reset_request.rs +++ b/server/lemmy_db/src/password_reset_request.rs @@ -105,7 +105,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_8292378".into(), bio: None, local: true, private_key: None, diff --git a/server/lemmy_db/src/post.rs b/server/lemmy_db/src/post.rs index 1525a675f..d46677897 100644 --- a/server/lemmy_db/src/post.rs +++ b/server/lemmy_db/src/post.rs @@ -8,6 +8,7 @@ use crate::{ }; use diesel::{dsl::*, result::Error, *}; use serde::{Deserialize, Serialize}; +use url::{ParseError, Url}; #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name = "post"] @@ -56,6 +57,12 @@ pub struct PostForm { pub local: bool, } +impl PostForm { + pub fn get_ap_id(&self) -> Result { + Url::parse(&self.ap_id) + } +} + impl Post { pub fn read(conn: &PgConnection, post_id: i32) -> Result { use crate::schema::post::dsl::*; @@ -101,6 +108,50 @@ impl Post { )) .get_result::(conn) } + + pub fn update_deleted( + conn: &PgConnection, + post_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_removed( + conn: &PgConnection, + post_id: i32, + new_removed: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(removed.eq(new_removed)) + .get_result::(conn) + } + + pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(locked.eq(new_locked)) + .get_result::(conn) + } + + pub fn update_stickied( + conn: &PgConnection, + post_id: i32, + new_stickied: bool, + ) -> Result { + use crate::schema::post::dsl::*; + diesel::update(post.find(post_id)) + .set(stickied.eq(new_stickied)) + .get_result::(conn) + } + + pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool { + user_id == post_creator_id + } } impl Crud for Post { @@ -272,7 +323,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_8292683678".into(), bio: None, local: true, private_key: None, @@ -292,7 +343,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_8223262378".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/post_view.rs b/server/lemmy_db/src/post_view.rs index 3e9f87376..ffc8afebd 100644 --- a/server/lemmy_db/src/post_view.rs +++ b/server/lemmy_db/src/post_view.rs @@ -415,7 +415,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_8282738268".into(), bio: None, local: true, private_key: None, @@ -435,7 +435,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_2763".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/private_message.rs b/server/lemmy_db/src/private_message.rs index 1c0b455f3..3486cf545 100644 --- a/server/lemmy_db/src/private_message.rs +++ b/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::(conn) } + + pub fn update_content( + conn: &PgConnection, + private_message_id: i32, + new_content: &str, + ) -> Result { + 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::(conn) + } + + pub fn update_deleted( + conn: &PgConnection, + private_message_id: i32, + new_deleted: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(deleted.eq(new_deleted)) + .get_result::(conn) + } + + pub fn update_read( + conn: &PgConnection, + private_message_id: i32, + new_read: bool, + ) -> Result { + use crate::schema::private_message::dsl::*; + diesel::update(private_message.find(private_message_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result, 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::(conn) + } } #[cfg(test)] @@ -113,7 +157,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_6723878".into(), bio: None, local: true, private_key: None, @@ -140,7 +184,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_287263876".into(), bio: None, local: true, private_key: None, @@ -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); } } diff --git a/server/lemmy_db/src/user.rs b/server/lemmy_db/src/user.rs index 556fc1a75..e53890770 100644 --- a/server/lemmy_db/src/user.rs +++ b/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::(conn) } -} -impl User_ { + pub fn find_by_email_or_username( + conn: &PgConnection, + username_or_email: &str, + ) -> Result { + 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_.filter(name.eq(username)).first::(conn) } @@ -166,7 +176,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_9826382637".into(), bio: None, local: true, private_key: None, @@ -195,7 +205,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: inserted_user.actor_id.to_owned(), bio: None, local: true, private_key: None, diff --git a/server/lemmy_db/src/user_mention.rs b/server/lemmy_db/src/user_mention.rs index 9f23f4410..f32318e0a 100644 --- a/server/lemmy_db/src/user_mention.rs +++ b/server/lemmy_db/src/user_mention.rs @@ -52,6 +52,30 @@ impl Crud for UserMention { } } +impl UserMention { + pub fn update_read( + conn: &PgConnection, + user_mention_id: i32, + new_read: bool, + ) -> Result { + use crate::schema::user_mention::dsl::*; + diesel::update(user_mention.find(user_mention_id)) + .set(read.eq(new_read)) + .get_result::(conn) + } + + pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result, 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::(conn) + } +} + #[cfg(test)] mod tests { use crate::{ @@ -86,7 +110,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_628763".into(), bio: None, local: true, private_key: None, @@ -113,7 +137,7 @@ mod tests { lang: "browser".into(), show_avatars: true, send_notifications_to_email: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_927389278".into(), bio: None, local: true, private_key: None, @@ -133,7 +157,7 @@ mod tests { deleted: None, updated: None, nsfw: false, - actor_id: "http://fake.com".into(), + actor_id: "changeme_876238".into(), local: true, private_key: None, public_key: None, diff --git a/server/lemmy_db/src/user_view.rs b/server/lemmy_db/src/user_view.rs index f2ac47422..d61fe9c52 100644 --- a/server/lemmy_db/src/user_view.rs +++ b/server/lemmy_db/src/user_view.rs @@ -157,7 +157,28 @@ impl UserView { pub fn admins(conn: &PgConnection) -> Result, Error> { use super::user_view::user_fast::dsl::*; + use diesel::sql_types::{Nullable, Text}; user_fast + // The select is necessary here to not get back emails + .select(( + id, + actor_id, + name, + avatar, + "".into_sql::>(), + matrix_user_id, + bio, + local, + admin, + banned, + show_avatars, + send_notifications_to_email, + published, + number_of_posts, + post_score, + number_of_comments, + comment_score, + )) .filter(admin.eq(true)) .order_by(published) .load::(conn) @@ -165,6 +186,28 @@ impl UserView { pub fn banned(conn: &PgConnection) -> Result, Error> { use super::user_view::user_fast::dsl::*; - user_fast.filter(banned.eq(true)).load::(conn) + use diesel::sql_types::{Nullable, Text}; + user_fast + .select(( + id, + actor_id, + name, + avatar, + "".into_sql::>(), + matrix_user_id, + bio, + local, + admin, + banned, + show_avatars, + send_notifications_to_email, + published, + number_of_posts, + post_score, + number_of_comments, + comment_score, + )) + .filter(banned.eq(true)) + .load::(conn) } } diff --git a/server/lemmy_utils/Cargo.toml b/server/lemmy_utils/Cargo.toml index fed22f585..9685c0eda 100644 --- a/server/lemmy_utils/Cargo.toml +++ b/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"] } \ No newline at end of file +url = { version = "2.1.1", features = ["serde"] } diff --git a/server/lemmy_utils/src/lib.rs b/server/lemmy_utils/src/lib.rs index d88335e2d..6d851b033 100644 --- a/server/lemmy_utils/src/lib.rs +++ b/server/lemmy_utils/src/lib.rs @@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime { DateTime::::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")); diff --git a/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql new file mode 100644 index 000000000..7a4f2e2f1 --- /dev/null +++ b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql @@ -0,0 +1,20 @@ + +alter table community alter column actor_id set not null; +alter table community alter column actor_id set default 'http://fake.com'; +alter table user_ alter column actor_id set not null; +alter table user_ alter column actor_id set default 'http://fake.com'; + +drop function generate_unique_changeme; + +update community +set actor_id = 'http://fake.com' +where actor_id like 'changeme_%'; + +update user_ +set actor_id = 'http://fake.com' +where actor_id like 'changeme_%'; + +drop index idx_user_lower_actor_id; +create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id)); + +drop index idx_community_lower_actor_id; diff --git a/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql new file mode 100644 index 000000000..e32ed5e08 --- /dev/null +++ b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql @@ -0,0 +1,50 @@ +-- Following this issue : https://github.com/LemmyNet/lemmy/issues/957 + +-- Creating a unique changeme actor_id +create or replace function generate_unique_changeme() +returns text language sql +as $$ + select 'changeme_' || string_agg (substr('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789', ceil (random() * 62)::integer, 1), '') + from generate_series(1, 20) +$$; + +-- Need to delete the possible community and user dupes for ones that don't start with the fake one +-- A few test inserts, to make sure this removes later dupes +-- insert into community (name, title, category_id, creator_id) values ('testcom', 'another testcom', 1, 2); +delete from community a using ( + select min(id) as id, actor_id + from community + group by actor_id having count(*) > 1 +) b +where a.actor_id = b.actor_id +and a.id <> b.id; + +delete from user_ a using ( + select min(id) as id, actor_id + from user_ + group by actor_id having count(*) > 1 +) b +where a.actor_id = b.actor_id +and a.id <> b.id; + +-- Replacing the current default on the columns, to the unique one +update community +set actor_id = generate_unique_changeme() +where actor_id = 'http://fake.com'; + +update user_ +set actor_id = generate_unique_changeme() +where actor_id = 'http://fake.com'; + +-- Add the unique indexes +alter table community alter column actor_id set not null; +alter table community alter column actor_id set default generate_unique_changeme(); + +alter table user_ alter column actor_id set not null; +alter table user_ alter column actor_id set default generate_unique_changeme(); + +-- Add lowercase uniqueness too +drop index idx_user_name_lower_actor_id; +create unique index idx_user_lower_actor_id on user_ (lower(actor_id)); + +create unique index idx_community_lower_actor_id on community (lower(actor_id)); diff --git a/server/src/api/claims.rs b/server/src/api/claims.rs index eec9d1a71..9118714ba 100644 --- a/server/src/api/claims.rs +++ b/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 { - 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 { let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims; User_::read(&conn, claims.id) diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index f8bdf5d5b..df772f535 100644 --- a/server/src/api/comment.rs +++ b/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, - edit_id: Option, // TODO this isn't used pub post_id: i32, + form_id: Option, auth: String, } #[derive(Serialize, Deserialize)] pub struct EditComment { content: String, - parent_id: Option, // 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, - deleted: Option, + form_id: Option, + 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, - read: Option, + 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, + pub form_id: Option, } #[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 { 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 { 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 { // 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 { 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,34 @@ impl Perform for Oper { 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 = vec![orig_comment.creator_id]; - let mut moderators: Vec = vec![]; - - 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()); - } - - // 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 = - blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; - if user_id != parent_comment.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - None => { - let parent_post_id = orig_comment.post_id; - let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; - if user_id != parent_post.creator_id { - return Err(APIError::err("no_comment_edit_allowed").into()); - } - } - } + // 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 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 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) + Comment::update_content(conn, edit_id, &content_slurs_removed) }) .await? { @@ -360,54 +301,19 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; - if data.read.is_none() { - 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?; - } + // Send the apub update + 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; + // Do the mentions / recipients + let post_id = orig_comment.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 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| { @@ -418,6 +324,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: comment_view, recipient_ids, + form_id: data.form_id.to_owned(), }; if let Some(ws) = websocket_info { @@ -436,6 +343,291 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 + 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 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 { + 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 { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 { + type Response = CommentResponse; + + async fn perform( + &self, + pool: &DbPool, + _websocket_info: Option, + ) -> Result { + 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; + match parent_id { + Some(pid) => { + let parent_comment = + blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??; + if user_id != parent_comment.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + None => { + let parent_post_id = orig_comment.post_id; + let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??; + if user_id != parent_post.creator_id { + return Err(APIError::err("no_comment_edit_allowed").into()); + } + } + } + + // Do the mark as read + let read = data.read; + match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? { + Ok(comment) => comment, + Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), + }; + + // Refetch it + let edit_id = data.edit_id; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, edit_id, Some(user_id)) + }) + .await??; + + let res = CommentResponse { + comment: comment_view, + recipient_ids: Vec::new(), + form_id: None, + }; + + Ok(res) + } +} + #[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; @@ -480,6 +672,7 @@ impl Perform for Oper { Ok(CommentResponse { comment: comment_view, recipient_ids: Vec::new(), + form_id: None, }) } } @@ -512,8 +705,12 @@ impl Perform for Oper { } } + 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 - 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 community_id = post.community_id; let is_banned = @@ -550,7 +747,7 @@ impl Perform for Oper { let like_form = CommentLikeForm { comment_id: data.comment_id, - post_id: data.post_id, + post_id, user_id, score: data.score, }; @@ -587,6 +784,7 @@ impl Perform for Oper { let mut res = CommentResponse { comment: liked_comment, recipient_ids, + form_id: None, }; if let Some(ws) = websocket_info { @@ -675,9 +873,10 @@ pub async fn send_local_notifs( user: User_, post: Post, pool: &DbPool, + do_send_email: bool, ) -> Result, LemmyError> { 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?; @@ -690,6 +889,7 @@ fn do_send_local_notifs( comment: &Comment, user: &User_, post: &Post, + do_send_email: bool, ) -> Vec { let mut recipient_ids = Vec::new(); 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 - 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 { let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,); let html = &format!( @@ -744,7 +944,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_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 { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( @@ -767,7 +967,7 @@ fn do_send_local_notifs( if let Ok(parent_user) = User_::read(&conn, post.creator_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 { let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,); let html = &format!( diff --git a/server/src/api/community.rs b/server/src/api/community.rs index e5063e0ff..c5ae152aa 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -1,6 +1,6 @@ use super::*; use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform}, apub::ActorType, blocking, websocket::{ @@ -34,7 +34,6 @@ pub struct GetCommunity { pub struct GetCommunityResponse { pub community: CommunityView, pub moderators: Vec, - pub admins: Vec, pub online: usize, } @@ -98,13 +97,24 @@ pub struct AddModToCommunityResponse { #[derive(Serialize, Deserialize)] pub struct EditCommunity { pub edit_id: i32, - name: String, title: String, description: Option, category_id: i32, - removed: Option, - deleted: Option, 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, expires: Option, auth: String, @@ -185,13 +195,6 @@ impl Perform for Oper { 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 { if let Some(id) = ws.id { ws.chatserver.do_send(JoinCommunityRoom { @@ -213,7 +216,6 @@ impl Perform for Oper { let res = GetCommunityResponse { community: community_view, moderators, - admins, online, }; @@ -264,6 +266,17 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + // Double check for duplicate community actor_ids + let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string(); + let actor_id_cloned = actor_id.to_owned(); + let community_dupe = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &actor_id_cloned) + }) + .await?; + if community_dupe.is_ok() { + return Err(APIError::err("community_already_exists").into()); + } + // When you create a community, make sure the user becomes a moderator and a follower let keypair = generate_actor_keypair()?; @@ -277,7 +290,7 @@ impl Perform for Oper { deleted: None, nsfw: data.nsfw, updated: None, - actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(), + actor_id, local: true, private_key: Some(keypair.private_key), public_key: Some(keypair.public_key), @@ -333,10 +346,6 @@ impl Perform for Oper { ) -> Result { let data: &EditCommunity = &self.data; - if let Err(slurs) = slur_check(&data.name) { - return Err(APIError::err(&slurs_vec_to_str(slurs)).into()); - } - if let Err(slurs) = slur_check(&data.title) { return Err(APIError::err(&slurs_vec_to_str(slurs)).into()); } @@ -352,10 +361,6 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("not_logged_in").into()), }; - if !is_valid_community_name(&data.name) { - return Err(APIError::err("invalid_community_name").into()); - } - let user_id = claims.id; // Check for a site ban @@ -364,37 +369,28 @@ impl Perform for Oper { 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 mut editors: Vec = Vec::new(); - editors.append( - &mut blocking(pool, move |conn| { - CommunityModeratorView::for_community(conn, edit_id) - .map(|v| v.into_iter().map(|m| m.user_id).collect()) - }) - .await??, - ); - editors.append( - &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 mods: Vec = blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, edit_id) + .map(|v| v.into_iter().map(|m| m.user_id).collect()) + }) + .await??; + if !mods.contains(&user_id) { + return Err(APIError::err("not_a_moderator").into()); } let edit_id = data.edit_id; let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??; let community_form = CommunityForm { - name: data.name.to_owned(), + name: read_community.name, title: data.title.to_owned(), description: data.description.to_owned(), category_id: data.category_id.to_owned(), creator_id: read_community.creator_id, - removed: data.removed.to_owned(), - deleted: data.deleted.to_owned(), + removed: Some(read_community.removed), + deleted: Some(read_community.deleted), nsfw: data.nsfw, updated: Some(naive_now()), actor_id: read_community.actor_id, @@ -406,7 +402,7 @@ impl Perform for Oper { }; 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) }) .await? @@ -415,42 +411,77 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; - // Mod tables - if let Some(removed) = data.removed.to_owned() { - 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??; + // TODO there needs to be some kind of an apub update + // process for communities and users + + 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::EditCommunity); + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommunityResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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()); } - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_community - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_community - .send_undo_delete(&user, &self.client, pool) - .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?; - } + // 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 { + updated_community + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_community + .send_undo_delete(&user, &self.client, pool) + .await?; } let edit_id = data.edit_id; @@ -463,20 +494,88 @@ impl Perform for Oper { community: community_view, }; - 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; + send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity); - ws.chatserver.do_send(SendCommunityRoomMessage { - op: UserOperation::EditCommunity, - response: res_sent, - community_id: data.edit_id, - my_id: ws.id, - }); + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = CommunityResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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) } } @@ -652,27 +751,10 @@ impl Perform for Oper { let user_id = claims.id; - let mut community_moderators: Vec = vec![]; - let community_id = data.community_id; - community_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??, - ); - 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()); - } + // Verify that only mods or admins can ban + is_mod_or_admin(pool, user_id, community_id).await?; let community_user_ban_form = CommunityUserBanForm { community_id: data.community_id, @@ -692,6 +774,7 @@ impl Perform for Oper { } // Mod tables + // TODO eventually do correct expires let expires = match data.expires { Some(time) => Some(naive_from_unix(time)), None => None, @@ -751,27 +834,10 @@ impl Perform for Oper { user_id: data.user_id, }; - let mut community_moderators: Vec = vec![]; - let community_id = data.community_id; - community_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??, - ); - 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()); - } + // Verify that only mods or admins can add mod + is_mod_or_admin(pool, user_id, community_id).await?; if data.added { let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); @@ -850,26 +916,9 @@ impl Perform for Oper { 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 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() { return Err(APIError::err("couldnt_update_community").into()); }; @@ -939,8 +988,27 @@ impl Perform for Oper { Ok(GetCommunityResponse { community: community_view, moderators, - admins, online: 0, }) } } + +pub fn send_community_websocket( + res: &CommunityResponse, + websocket_info: Option, + 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, + }); + } +} diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index bb65815ad..901172601 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -1,6 +1,14 @@ -use crate::{websocket::WebsocketInfo, DbPool, LemmyError}; +use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError}; 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 comment; @@ -44,3 +52,25 @@ pub trait Perform { websocket_info: Option, ) -> Result; } + +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(()) +} diff --git a/server/src/api/post.rs b/server/src/api/post.rs index b9518f0e9..79881c4b5 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.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, fetch_iframely_and_pictrs_data, @@ -18,10 +18,8 @@ use lemmy_db::{ naive_now, post::*, post_view::*, - site::*, site_view::*, user::*, - user_view::*, Crud, Likeable, ListingType, @@ -37,6 +35,7 @@ use lemmy_utils::{ }; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use url::Url; #[derive(Serialize, Deserialize, Debug)] pub struct CreatePost { @@ -65,7 +64,6 @@ pub struct GetPostResponse { comments: Vec, community: CommunityView, moderators: Vec, - admins: Vec, pub online: usize, } @@ -95,20 +93,42 @@ pub struct CreatePostLike { #[derive(Serialize, Deserialize)] pub struct EditPost { pub edit_id: i32, - creator_id: i32, - community_id: i32, name: String, url: Option, body: Option, - removed: Option, - deleted: Option, nsfw: bool, - locked: Option, - stickied: Option, + auth: String, +} + +#[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, 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)] pub struct SavePost { post_id: i32, @@ -162,6 +182,13 @@ impl Perform for Oper { return Err(APIError::err("site_ban").into()); } + if let Some(url) = data.url.as_ref() { + match Url::parse(url) { + Ok(_t) => (), + Err(_e) => return Err(APIError::err("invalid_url").into()), + } + } + // Fetch Iframely and pictrs cached image let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; @@ -303,14 +330,6 @@ impl Perform for Oper { }) .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 { if let Some(id) = ws.id { ws.chatserver.do_send(JoinPostRoom { @@ -335,7 +354,6 @@ impl Perform for Oper { comments, community, moderators, - admins, online, }) } @@ -541,35 +559,10 @@ impl Perform for Oper { let user_id = claims.id; let edit_id = data.edit_id; - let read_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 = vec![read_post.creator_id]; - let mut moderators: Vec = 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()); - } + let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??; // Check for a community ban - let community_id = read_post.community_id; + 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? { @@ -582,55 +575,34 @@ impl Perform for Oper { 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 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await; - let post_form = { - // only modify some properties if they are a moderator - if moderators.contains(&user_id) { - PostForm { - name: data.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: data.removed.to_owned(), - deleted: data.deleted.to_owned(), - nsfw: data.nsfw, - locked: data.locked.to_owned(), - stickied: data.stickied.to_owned(), - 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, - } - } 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 post_form = PostForm { + name: data.name.trim().to_owned(), + url: data.url.to_owned(), + body: data.body.to_owned(), + nsfw: data.nsfw, + creator_id: orig_post.creator_id.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()), + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail, + ap_id: orig_post.ap_id, + local: orig_post.local, + published: None, }; let edit_id = data.edit_id; @@ -648,58 +620,8 @@ impl Perform for Oper { } }; - if moderators.contains(&user_id) { - // 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?; - } + // Send apub update + updated_post.send_update(&user, &self.client, pool).await?; let edit_id = data.edit_id; let post_view = blocking(pool, move |conn| { @@ -721,6 +643,324 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 { + type Response = PostResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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)] impl Perform for Oper { type Response = PostResponse; diff --git a/server/src/api/site.rs b/server/src/api/site.rs index 241a80e31..85511e6c9 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -1,8 +1,9 @@ use super::user::Register; use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, APIError, Oper, Perform}, apub::fetcher::search_by_apub_id, blocking, + version, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, DbPool, LemmyError, @@ -110,6 +111,7 @@ pub struct GetSiteResponse { admins: Vec, banned: Vec, pub online: usize, + version: String, } #[derive(Serialize, Deserialize)] @@ -255,10 +257,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; - if !user.admin { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let site_form = SiteForm { name: data.name.to_owned(), @@ -309,10 +308,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; - if !user.admin { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; @@ -424,6 +420,7 @@ impl Perform for Oper { admins, banned, online, + version: version::VERSION.to_string(), }) } } @@ -666,6 +663,7 @@ impl Perform for Oper { admins, banned, online: 0, + version: version::VERSION.to_string(), }) } } @@ -689,12 +687,7 @@ impl Perform for Oper { let user_id = claims.id; // Only let admins read this - let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; - let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); - - if !admin_ids.contains(&user_id) { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let config_hjson = Settings::read_config_file()?; diff --git a/server/src/api/user.rs b/server/src/api/user.rs index ddcf2ef2e..32a16b00e 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -1,5 +1,5 @@ use crate::{ - api::{claims::Claims, APIError, Oper, Perform}, + api::{claims::Claims, is_admin, APIError, Oper, Perform}, apub::ApubObjectType, blocking, websocket::{ @@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse { moderates: Vec, comments: Vec, posts: Vec, - admins: Vec, } #[derive(Serialize, Deserialize)] @@ -174,9 +173,9 @@ pub struct GetUserMentions { } #[derive(Serialize, Deserialize)] -pub struct EditUserMention { +pub struct MarkUserMentionAsRead { user_mention_id: i32, - read: Option, + read: bool, auth: String, } @@ -216,9 +215,21 @@ pub struct CreatePrivateMessage { #[derive(Serialize, Deserialize)] pub struct EditPrivateMessage { edit_id: i32, - content: Option, - deleted: Option, - read: Option, + content: String, + auth: String, +} + +#[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, } @@ -264,7 +275,7 @@ impl Perform for Oper { // Fetch that username / email let username_or_email = data.username_or_email.clone(); 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? { @@ -631,20 +642,10 @@ impl Perform for Oper { }) .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 - if let Some(user_id) = user_id { - if user_details_id != user_id { - user_view.email = None; - } - } else { + // 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 separate out settings into its own thing + if user_id.is_none() || user_details_id != user_id.unwrap_or(0) { user_view.email = None; } @@ -655,7 +656,6 @@ impl Perform for Oper { moderates, comments, posts, - admins, }) } } @@ -679,10 +679,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); - if !blocking(pool, is_admin).await?? { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let added = data.added; let added_user_id = data.user_id; @@ -741,10 +738,7 @@ impl Perform for Oper { let user_id = claims.id; // Make sure user is an admin - let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); - if !blocking(pool, is_admin).await?? { - return Err(APIError::err("not_an_admin").into()); - } + is_admin(pool, user_id).await?; let ban = data.ban; let banned_user_id = data.user_id; @@ -864,7 +858,7 @@ impl Perform for Oper { } #[async_trait::async_trait(?Send)] -impl Perform for Oper { +impl Perform for Oper { type Response = UserMentionResponse; async fn perform( @@ -872,7 +866,7 @@ impl Perform for Oper { pool: &DbPool, _websocket_info: Option, ) -> Result { - let data: &EditUserMention = &self.data; + let data: &MarkUserMentionAsRead = &self.data; let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, @@ -880,28 +874,23 @@ impl Perform for Oper { }; let user_id = claims.id; - if user_id != data.user_mention_id { + + let user_mention_id = data.user_mention_id; + let read_user_mention = + blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??; + + if user_id != read_user_mention.recipient_id { return Err(APIError::err("couldnt_update_comment").into()); } - let user_mention_id = data.user_mention_id; - let user_mention = - blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??; - - let user_mention_form = UserMentionForm { - recipient_id: user_id, - comment_id: user_mention.comment_id, - read: data.read.to_owned(), - }; - - let user_mention_id = user_mention.id; - let update_mention = - move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form); + let user_mention_id = read_user_mention.id; + let read = data.read; + let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read); if blocking(pool, update_mention).await?.is_err() { return Err(APIError::err("couldnt_update_comment").into()); }; - let user_mention_id = user_mention.id; + let user_mention_id = read_user_mention.id; let user_mention_view = blocking(pool, move |conn| { UserMentionView::read(conn, user_mention_id, user_id) }) @@ -941,70 +930,26 @@ impl Perform for Oper { .await??; // 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 { 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() { return Err(APIError::err("couldnt_update_comment").into()); } } - // Mentions - let mentions = blocking(pool, move |conn| { - UserMentionQueryBuilder::create(conn, user_id) - .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()); - } + // Mark all user mentions as read + let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id); + if blocking(pool, update_user_mentions).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); } - // messages - let messages = blocking(pool, move |conn| { - 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() { - return Err(APIError::err("couldnt_update_private_message").into()); - } + // Mark all private_messages as read + let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id); + if blocking(pool, update_pm).await?.is_err() { + return Err(APIError::err("couldnt_update_private_message").into()); } Ok(GetRepliesResponse { replies: vec![] }) @@ -1294,59 +1239,25 @@ impl Perform for Oper { 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 let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } - // Check to make sure they are the creator (or the recipient marking as read - if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id) - || orig_private_message.creator_id.eq(&user_id)) - { + // 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()); } - let content_slurs_removed = match &data.content { - Some(content) => remove_slurs(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, - } - } - }; - + // Doing the update + let content_slurs_removed = remove_slurs(&data.content); let edit_id = data.edit_id; 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? { @@ -1354,30 +1265,14 @@ impl Perform for Oper { Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), }; - if data.read.is_none() { - if let Some(deleted) = data.deleted.to_owned() { - if deleted { - updated_private_message - .send_delete(&user, &self.client, pool) - .await?; - } else { - updated_private_message - .send_undo_delete(&user, &self.client, pool) - .await?; - } - } else { - updated_private_message - .send_update(&user, &self.client, pool) - .await?; - } - } else { - updated_private_message - .send_update(&user, &self.client, pool) - .await?; - } + // Send the apub update + updated_private_message + .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 }; @@ -1385,7 +1280,146 @@ impl Perform for Oper { ws.chatserver.do_send(SendUserRoomMessage { op: UserOperation::EditPrivateMessage, 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 { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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 + .send_delete(&user, &self.client, pool) + .await?; + } else { + updated_private_message + .send_undo_delete(&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::DeletePrivateMessage, + response: res.clone(), + recipient_id, + my_id: ws.id, + }); + } + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl Perform for Oper { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + pool: &DbPool, + websocket_info: Option, + ) -> Result { + 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, }); } diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index 204a380d3..a14e6ec37 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -10,41 +10,20 @@ use crate::{ DbPool, LemmyError, }; -use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; +use activitystreams_new::base::AnyBase; use actix_web::client::Client; use lemmy_db::{community::Community, user::User_}; use log::debug; -use serde::Serialize; -use std::fmt::Debug; use url::Url; -pub fn populate_object_props( - props: &mut ObjectProperties, - addressed_ccs: Vec, - object_id: &str, -) -> Result<(), LemmyError> { - props - .set_context_xsd_any_uri(context())? - // TODO: the activity needs a seperate id from the object - .set_id(object_id)? - // TODO: should to/cc go on the Create, or on the Post? or on both? - // TODO: handle privacy on the receiving side (at least ignore anything thats not public) - .set_to_xsd_any_uri(public())? - .set_many_cc_xsd_any_uris(addressed_ccs)?; - Ok(()) -} - -pub async fn send_activity_to_community( +pub async fn send_activity_to_community( creator: &User_, community: &Community, to: Vec, - activity: A, + activity: AnyBase, client: &Client, pool: &DbPool, -) -> Result<(), LemmyError> -where - A: Activity + Base + Serialize + Debug + Clone + Send + 'static, -{ +) -> Result<(), LemmyError> { insert_activity(creator.id, activity.clone(), true, pool).await?; // if this is a local community, we need to do an announce from the community instead @@ -58,15 +37,12 @@ where } /// Send an activity to a list of recipients, using the correct headers etc. -pub async fn send_activity( +pub async fn send_activity( client: &Client, - activity: &A, + activity: &AnyBase, actor: &dyn ActorType, to: Vec, -) -> Result<(), LemmyError> -where - A: Serialize, -{ +) -> Result<(), LemmyError> { let activity = serde_json::to_string(&activity)?; debug!("Sending activitypub activity {} to {:?}", activity, to); diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 9e5e53a7b..774abba6b 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -1,6 +1,6 @@ use crate::{ apub::{ - activities::{populate_object_props, send_activity_to_community}, + activities::send_activity_to_community, create_apub_response, create_apub_tombstone_response, create_tombstone, @@ -21,13 +21,15 @@ use crate::{ DbPool, LemmyError, }; -use activitystreams::{ +use activitystreams_new::{ activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, + base::AnyBase, context, link::Mention, - object::{kind::NoteType, properties::ObjectProperties, Note}, + object::{kind::NoteType, Note, Tombstone}, + prelude::*, + public, }; -use activitystreams_new::object::Tombstone; use actix_web::{body::Body, client::Client, web::Path, HttpResponse}; use itertools::Itertools; use lemmy_db::{ @@ -40,6 +42,8 @@ use lemmy_db::{ use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData}; use log::debug; use serde::Deserialize; +use serde_json::Error; +use url::Url; #[derive(Deserialize)] pub struct CommentQuery { @@ -66,8 +70,7 @@ impl ToApub for Comment { type Response = Note; async fn to_apub(&self, pool: &DbPool) -> Result { - let mut comment = Note::default(); - let oprops: &mut ObjectProperties = comment.as_mut(); + let mut comment = Note::new(); let creator_id = self.creator_id; let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; @@ -88,18 +91,18 @@ impl ToApub for Comment { in_reply_to_vec.push(parent_comment.ap_id); } - oprops + comment // Not needed when the Post is embedded in a collection (like for community outbox) - .set_context_xsd_any_uri(context())? - .set_id(self.ap_id.to_owned())? - .set_published(convert_datetime(self.published))? - .set_to_xsd_any_uri(community.actor_id)? - .set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)? - .set_content_xsd_string(self.content.to_owned())? - .set_attributed_to_xsd_any_uri(creator.actor_id)?; + .set_context(context()) + .set_id(Url::parse(&self.ap_id)?) + .set_published(convert_datetime(self.published)) + .set_to(community.actor_id) + .set_many_in_reply_tos(in_reply_to_vec) + .set_content(self.content.to_owned()) + .set_attributed_to(creator.actor_id); if let Some(u) = self.updated { - oprops.set_updated(convert_datetime(u))?; + comment.set_updated(convert_datetime(u)); } Ok(comment) @@ -110,7 +113,7 @@ impl ToApub for Comment { self.deleted, &self.ap_id, self.updated, - NoteType.to_string(), + NoteType::Note.to_string(), ) } } @@ -124,14 +127,25 @@ impl FromApub for CommentForm { note: &Note, client: &Client, pool: &DbPool, + actor_id: &Url, ) -> Result { - let oprops = ¬e.object_props; - let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); + let creator_actor_id = ¬e + .attributed_to() + .unwrap() + .as_single_xsd_any_uri() + .unwrap(); - let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?; + let creator = get_or_fetch_and_upsert_remote_user(creator_actor_id, client, pool).await?; - let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap(); - let post_ap_id = in_reply_tos.next().unwrap().to_string(); + let mut in_reply_tos = note + .in_reply_to() + .as_ref() + .unwrap() + .as_many() + .unwrap() + .iter() + .map(|i| i.as_xsd_any_uri().unwrap()); + let post_ap_id = in_reply_tos.next().unwrap(); // This post, or the parent comment might not yet exist on this server yet, fetch them. let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; @@ -140,7 +154,7 @@ impl FromApub for CommentForm { // For deeply nested comments, FromApub automatically gets called recursively let parent_id: Option = match in_reply_tos.next() { Some(parent_comment_uri) => { - let parent_comment_ap_id = &parent_comment_uri.to_string(); + let parent_comment_ap_id = &parent_comment_uri; let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?; @@ -153,20 +167,18 @@ impl FromApub for CommentForm { creator_id: creator.id, post_id: post.id, parent_id, - content: oprops - .get_content_xsd_string() - .map(|c| c.to_string()) - .unwrap(), + content: note + .content() + .unwrap() + .as_single_xsd_string() + .unwrap() + .to_string(), removed: None, read: None, - published: oprops - .get_published() - .map(|u| u.as_ref().to_owned().naive_local()), - updated: oprops - .get_updated() - .map(|u| u.as_ref().to_owned().naive_local()), + published: note.published().map(|u| u.to_owned().naive_local()), + updated: note.updated().map(|u| u.to_owned().naive_local()), deleted: None, - ap_id: oprops.get_id().unwrap().to_string(), + ap_id: note.id(actor_id.domain().unwrap())?.unwrap().to_string(), local: false, }) } @@ -193,18 +205,24 @@ impl ApubObjectType for Comment { collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?; let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut create = Create::new(); - populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?; - - // Set the mention tags - create.object_props.set_many_tag_base_boxes(maa.tags)?; - + let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?); create - .create_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(maa.addressed_ccs.to_owned()) + // Set the mention tags + .set_many_tags(maa.get_tags()?); - send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?; + send_activity_to_community( + &creator, + &community, + maa.inboxes, + create.into_any_base()?, + client, + pool, + ) + .await?; Ok(()) } @@ -227,18 +245,24 @@ impl ApubObjectType for Comment { collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?; let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut update = Update::new(); - populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?; - - // Set the mention tags - update.object_props.set_many_tag_base_boxes(maa.tags)?; - + let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?); update - .update_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(maa.addressed_ccs.to_owned()) + // Set the mention tags + .set_many_tags(maa.get_tags()?); - send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?; + send_activity_to_community( + &creator, + &community, + maa.inboxes, + update.into_any_base()?, + client, + pool, + ) + .await?; Ok(()) } @@ -257,24 +281,18 @@ impl ApubObjectType for Comment { let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut delete = Delete::default(); - - populate_object_props( - &mut delete.object_props, - vec![community.get_followers_url()], - &id, - )?; - + let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?); delete - .delete_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &creator, &community, vec![community.get_shared_inbox_url()], - delete, + delete.into_any_base()?, client, pool, ) @@ -298,40 +316,28 @@ impl ApubObjectType for Comment { // Generate a fake delete activity, with the correct object let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut delete = Delete::default(); - - populate_object_props( - &mut delete.object_props, - vec![community.get_followers_url()], - &id, - )?; - + let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?); delete - .delete_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); // TODO // Undo that fake activity let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut undo = Undo::default(); - - populate_object_props( - &mut undo.object_props, - vec![community.get_followers_url()], - &undo_id, - )?; - + let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?); undo - .undo_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(delete)?; + .set_context(context()) + .set_id(Url::parse(&undo_id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &creator, &community, vec![community.get_shared_inbox_url()], - undo, + undo.into_any_base()?, client, pool, ) @@ -354,24 +360,18 @@ impl ApubObjectType for Comment { let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut remove = Remove::default(); - - populate_object_props( - &mut remove.object_props, - vec![community.get_followers_url()], - &id, - )?; - + let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?); remove - .remove_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &mod_, &community, vec![community.get_shared_inbox_url()], - remove, + remove.into_any_base()?, client, pool, ) @@ -395,39 +395,27 @@ impl ApubObjectType for Comment { // Generate a fake delete activity, with the correct object let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut remove = Remove::default(); - - populate_object_props( - &mut remove.object_props, - vec![community.get_followers_url()], - &id, - )?; - + let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?); remove - .remove_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); // Undo that fake activity let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut undo = Undo::default(); - - populate_object_props( - &mut undo.object_props, - vec![community.get_followers_url()], - &undo_id, - )?; - + let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?); undo - .undo_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(remove)?; + .set_context(context()) + .set_id(Url::parse(&undo_id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &mod_, &community, vec![community.get_shared_inbox_url()], - undo, + undo.into_any_base()?, client, pool, ) @@ -454,22 +442,18 @@ impl ApubLikeableType for Comment { let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut like = Like::new(); - populate_object_props( - &mut like.object_props, - vec![community.get_followers_url()], - &id, - )?; + let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?); like - .like_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &creator, &community, vec![community.get_shared_inbox_url()], - like, + like.into_any_base()?, client, pool, ) @@ -493,22 +477,18 @@ impl ApubLikeableType for Comment { let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut dislike = Dislike::new(); - populate_object_props( - &mut dislike.object_props, - vec![community.get_followers_url()], - &id, - )?; + let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?); dislike - .dislike_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &creator, &community, vec![community.get_shared_inbox_url()], - dislike, + dislike.into_any_base()?, client, pool, ) @@ -532,38 +512,28 @@ impl ApubLikeableType for Comment { let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut like = Like::new(); - populate_object_props( - &mut like.object_props, - vec![community.get_followers_url()], - &id, - )?; + let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?); like - .like_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(note)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); // TODO // Undo that fake activity let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4()); - let mut undo = Undo::default(); - - populate_object_props( - &mut undo.object_props, - vec![community.get_followers_url()], - &undo_id, - )?; - + let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?); undo - .undo_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(like)?; + .set_context(context()) + .set_id(Url::parse(&undo_id)?) + .set_to(public()) + .set_many_ccs(vec![community.get_followers_url()]); send_activity_to_community( &creator, &community, vec![community.get_shared_inbox_url()], - undo, + undo.into_any_base()?, client, pool, ) @@ -578,6 +548,16 @@ struct MentionsAndAddresses { tags: Vec, } +impl MentionsAndAddresses { + fn get_tags(&self) -> Result, Error> { + self + .tags + .iter() + .map(|t| t.to_owned().into_any_base()) + .collect::, Error>>() + } +} + /// This takes a comment, and builds a list of to_addresses, inboxes, /// and mention tags, so they know where to be sent to. /// Addresses are the users / addresses that go in the cc field. @@ -604,17 +584,14 @@ async fn collect_non_local_mentions_and_addresses( // TODO should it be fetching it every time? if let Ok(actor_id) = fetch_webfinger_url(mention, client).await { debug!("mention actor_id: {}", actor_id); - addressed_ccs.push(actor_id.to_owned()); + addressed_ccs.push(actor_id.to_owned().to_string()); let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?; let shared_inbox = mention_user.get_shared_inbox_url(); mention_inboxes.push(shared_inbox); let mut mention_tag = Mention::new(); - mention_tag - .link_props - .set_href(actor_id)? - .set_name_xsd_string(mention.full_name())?; + mention_tag.set_href(actor_id).set_name(mention.full_name()); tags.push(mention_tag); } } diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 529039fc0..f84e6508d 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -1,6 +1,6 @@ use crate::{ apub::{ - activities::{populate_object_props, send_activity}, + activities::send_activity, create_apub_response, create_apub_tombstone_response, create_tombstone, @@ -18,22 +18,16 @@ use crate::{ DbPool, LemmyError, }; -use activitystreams::{ - activity::{Accept, Announce, Delete, Remove, Undo}, - Activity, - Base, - BaseBox, -}; use activitystreams_ext::Ext2; use activitystreams_new::{ - activity::Follow, + activity::{Accept, Announce, Delete, Follow, Remove, Undo}, actor::{kind::GroupType, ApActor, Endpoints, Group}, - base::BaseExt, + base::{AnyBase, BaseExt}, collection::UnorderedCollection, context, object::Tombstone, prelude::*, - primitives::{XsdAnyUri, XsdDateTime}, + public, }; use actix_web::{body::Body, client::Client, web, HttpResponse}; use itertools::Itertools; @@ -44,8 +38,8 @@ use lemmy_db::{ user::User_, }; use lemmy_utils::convert_datetime; -use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, str::FromStr}; +use serde::Deserialize; +use url::Url; #[derive(Deserialize)] pub struct CommunityQuery { @@ -72,13 +66,13 @@ impl ToApub for Community { let mut group = Group::new(); group .set_context(context()) - .set_id(XsdAnyUri::from_str(&self.actor_id)?) + .set_id(Url::parse(&self.actor_id)?) .set_name(self.name.to_owned()) - .set_published(XsdDateTime::from(convert_datetime(self.published))) + .set_published(convert_datetime(self.published)) .set_many_attributed_tos(moderators); if let Some(u) = self.updated.to_owned() { - group.set_updated(XsdDateTime::from(convert_datetime(u))); + group.set_updated(convert_datetime(u)); } if let Some(d) = self.description.to_owned() { // TODO: this should be html, also add source field with raw markdown @@ -117,14 +111,14 @@ impl ToApub for Community { self.deleted, &self.actor_id, self.updated, - GroupType.to_string(), + GroupType::Group.to_string(), ) } } #[async_trait::async_trait(?Send)] impl ActorType for Community { - fn actor_id(&self) -> String { + fn actor_id_str(&self) -> String { self.actor_id.to_owned() } @@ -138,27 +132,23 @@ impl ActorType for Community { /// As a local community, accept the follow request from a remote user. async fn send_accept_follow( &self, - follow: &Follow, + follow: Follow, client: &Client, pool: &DbPool, ) -> Result<(), LemmyError> { - let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); + let actor_uri = follow.actor()?.as_single_xsd_any_uri().unwrap().to_string(); let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut accept = Accept::new(); - accept - .object_props - .set_context_xsd_any_uri(context())? - .set_id(id)?; - accept - .accept_props - .set_actor_xsd_any_uri(self.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; + let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?); let to = format!("{}/inbox", actor_uri); + accept + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(to.clone()); insert_activity(self.creator_id, accept.clone(), true, pool).await?; - send_activity(client, &accept, self, vec![to]).await?; + send_activity(client, &accept.into_any_base()?, self, vec![to]).await?; Ok(()) } @@ -172,17 +162,12 @@ impl ActorType for Community { let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut delete = Delete::default(); - populate_object_props( - &mut delete.object_props, - vec![self.get_followers_url()], - &id, - )?; - + let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?); delete - .delete_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(group)?)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); insert_activity(self.creator_id, delete.clone(), true, pool).await?; @@ -191,7 +176,7 @@ impl ActorType for Community { // Note: For an accept, since it was automatic, no one pushed a button, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(client, &delete, creator, inboxes).await?; + send_activity(client, &delete.into_any_base()?, creator, inboxes).await?; Ok(()) } @@ -205,33 +190,22 @@ impl ActorType for Community { let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut delete = Delete::default(); - populate_object_props( - &mut delete.object_props, - vec![self.get_followers_url()], - &id, - )?; - + let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?); delete - .delete_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(group)?)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); // TODO // Undo that fake activity let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut undo = Undo::default(); - - populate_object_props( - &mut undo.object_props, - vec![self.get_followers_url()], - &undo_id, - )?; - + let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?); undo - .undo_props - .set_actor_xsd_any_uri(creator.actor_id.to_owned())? - .set_object_base_box(delete)?; + .set_context(context()) + .set_id(Url::parse(&undo_id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); insert_activity(self.creator_id, undo.clone(), true, pool).await?; @@ -240,7 +214,7 @@ impl ActorType for Community { // Note: For an accept, since it was automatic, no one pushed a button, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(client, &undo, creator, inboxes).await?; + send_activity(client, &undo.into_any_base()?, creator, inboxes).await?; Ok(()) } @@ -254,17 +228,12 @@ impl ActorType for Community { let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut remove = Remove::default(); - populate_object_props( - &mut remove.object_props, - vec![self.get_followers_url()], - &id, - )?; - + let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?); remove - .remove_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(group)?)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); insert_activity(mod_.id, remove.clone(), true, pool).await?; @@ -273,7 +242,7 @@ impl ActorType for Community { // Note: For an accept, since it was automatic, no one pushed a button, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(client, &remove, mod_, inboxes).await?; + send_activity(client, &remove.into_any_base()?, mod_, inboxes).await?; Ok(()) } @@ -287,32 +256,21 @@ impl ActorType for Community { let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut remove = Remove::default(); - populate_object_props( - &mut remove.object_props, - vec![self.get_followers_url()], - &id, - )?; - + let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?); remove - .remove_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(BaseBox::from_concrete(group)?)?; + .set_context(context()) + .set_id(Url::parse(&id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); // Undo that fake activity let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4()); - let mut undo = Undo::default(); - - populate_object_props( - &mut undo.object_props, - vec![self.get_followers_url()], - &undo_id, - )?; - + let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?); undo - .undo_props - .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? - .set_object_base_box(remove)?; + .set_context(context()) + .set_id(Url::parse(&undo_id)?) + .set_to(public()) + .set_many_ccs(vec![self.get_followers_url()]); insert_activity(mod_.id, undo.clone(), true, pool).await?; @@ -321,7 +279,7 @@ impl ActorType for Community { // Note: For an accept, since it was automatic, no one pushed a button, // the community was the actor. // But for remove , the creator is the actor, and does the signing - send_activity(client, &undo, mod_, inboxes).await?; + send_activity(client, &undo.into_any_base()?, mod_, inboxes).await?; Ok(()) } @@ -335,7 +293,7 @@ impl ActorType for Community { .await??; let inboxes = inboxes .into_iter() - .map(|c| get_shared_inbox(&c.user_actor_id)) + .map(|c| get_shared_inbox(&Url::parse(&c.user_actor_id).unwrap())) .filter(|s| !s.is_empty()) .unique() .collect(); @@ -367,8 +325,13 @@ impl FromApub for CommunityForm { type ApubType = GroupExt; /// Parse an ActivityPub group received from another instance into a Lemmy community. - async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result { - let creator_and_moderator_uris = group.attributed_to().unwrap(); + async fn from_apub( + group: &GroupExt, + client: &Client, + pool: &DbPool, + actor_id: &Url, + ) -> Result { + let creator_and_moderator_uris = group.inner.attributed_to().unwrap(); let creator_uri = creator_and_moderator_uris .as_many() .unwrap() @@ -378,26 +341,37 @@ impl FromApub for CommunityForm { .as_xsd_any_uri() .unwrap(); - let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?; + let creator = get_or_fetch_and_upsert_remote_user(creator_uri, client, pool).await?; Ok(CommunityForm { - name: group.name().unwrap().as_single_xsd_string().unwrap().into(), + name: group + .inner + .name() + .unwrap() + .as_one() + .unwrap() + .as_xsd_string() + .unwrap() + .into(), title: group.inner.preferred_username().unwrap().to_string(), // TODO: should be parsed as html and tags like