Merge branch 'main' into fix_config_location

This commit is contained in:
Dessalines 2020-07-22 16:40:02 -04:00
commit cc84316600
103 changed files with 5218 additions and 3639 deletions

6
README.md vendored
View file

@ -1,7 +1,7 @@
<div align="center"> <div align="center">
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg) ![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) [![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/) [![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/) [![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
@ -26,7 +26,7 @@
· ·
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a> <a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
· ·
<a href="https://github.com/LemmyNet/lemmy/blob/master/RELEASES.md">Releases</a> <a href="https://github.com/LemmyNet/lemmy/blob/main/RELEASES.md">Releases</a>
</p> </p>
</p> </p>
@ -34,7 +34,7 @@
Front Page|Post 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). [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).

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.7.20 v0.7.26

View file

@ -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; limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s;
server { server {
@ -65,13 +64,6 @@ server {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "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 # Redirect pictshare images to pictrs

View file

@ -1,10 +1,10 @@
#!/bin/sh #!/bin/sh
set -e set -e
git checkout master git checkout main
# Import translations # Import translations
git fetch weblate git fetch weblate
git merge weblate/master git merge weblate/main
# Creating the new tag # Creating the new tag
new_tag="$1" new_tag="$1"
@ -12,8 +12,6 @@ third_semver=$(echo $new_tag | cut -d "." -f 3)
# Setting the version on the front end # Setting the version on the front end
cd ../../ cd ../../
echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
git add "ui/src/version.ts"
# Setting the version on the backend # Setting the version on the backend
echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs" echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
git add "server/src/version.rs" git add "server/src/version.rs"

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.7.20 image: dessalines/lemmy:v0.7.26
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always

2
docs/src/about.md vendored
View file

@ -2,7 +2,7 @@
Front Page|Post 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). [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).

View file

@ -35,6 +35,8 @@ Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule <br><hr>
\`Inline code\` with backticks | |`Inline code` with backticks \`Inline code\` with backticks | |`Inline code` with backticks
\`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' \`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces'
::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details> ::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details>
Some ~subscript~ text | | Some <sub>subscript</sub> text
Some ^superscript^ text | | Some <sup>superscript</sup> text
[CommonMark Tutorial](https://commonmark.org/help/tutorial/) [CommonMark Tutorial](https://commonmark.org/help/tutorial/)

View file

@ -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. 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)

View file

@ -1,7 +1,7 @@
# Configuration # Configuration
The configuration is based on the file 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 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. can copy the options you want to change into your local `config.hjson` file.

View file

@ -19,7 +19,7 @@ ansible-playbook lemmy.yml --become
To update to a new version, just run the following in your local Lemmy repo: To update to a new version, just run the following in your local Lemmy repo:
```bash ```bash
git pull origin master git pull origin main
cd ansible cd ansible
ansible-playbook lemmy.yml --become ansible-playbook lemmy.yml --become
``` ```

View file

@ -8,9 +8,9 @@ mkdir /lemmy
cd /lemmy cd /lemmy
# download default config files # download default config files
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
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/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/iframely.config.local.js
# Set correct permissions for pictrs folder # Set correct permissions for pictrs folder
mkdir -p volumes/pictrs 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` `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 ```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 }} # Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf 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: 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 ```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 docker-compose up -d
``` ```

View file

@ -1,16 +1,26 @@
### Ubuntu ### Install build requirements
#### Ubuntu
#### Build requirements:
``` ```
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 # install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 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 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 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 git clone https://github.com/LemmyNet/lemmy.git
# or alternatively from gitea # 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 All the following commands need to be run either in `lemmy/server` or `lemmy/ui`, as indicated
by the `cd` command. by the `cd` command.
#### Build the backend (Rust) ### Build the backend (Rust)
``` ```
cd server cd server
cargo build cargo build
# for development, use `cargo check` instead) # for development, use `cargo check` instead)
``` ```
#### Build the frontend (Typescript) ### Build the frontend (Typescript)
``` ```
cd ui cd ui
yarn yarn
yarn build yarn build
``` ```
#### Setup postgresql ### Setup postgresql
#### Ubuntu
``` ```
sudo apt install postgresql sudo apt install postgresql
sudo systemctl start 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 user lemmy with password 'password' superuser;" -U postgres
sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -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 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 # run each of these in a seperate terminal
cd server && cargo run 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 Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit

View file

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

355
server/Cargo.lock generated vendored
View file

@ -1,35 +1,9 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # 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]] [[package]]
name = "activitystreams-ext" name = "activitystreams-ext"
version = "0.1.0" 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 = [ dependencies = [
"activitystreams-new", "activitystreams-new",
"serde 1.0.114", "serde 1.0.114",
@ -39,12 +13,14 @@ dependencies = [
[[package]] [[package]]
name = "activitystreams-new" name = "activitystreams-new"
version = "0.1.0" 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 = [ dependencies = [
"activitystreams", "chrono",
"mime",
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
"typed-builder", "thiserror",
"url",
] ]
[[package]] [[package]]
@ -111,9 +87,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-files" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b32e0fdd5998c2712549cbc39dff46c8754d55e3dd9f4d017d9e28de30cac6" checksum = "627f597ad98061816766201db8afc7444752992f2919b2e60f53a7fa27f01aed"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-service", "actix-service",
@ -132,9 +108,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd7ea0568480d199952a51de70271946da57c33cc0e8b83f54383e70958dff21" checksum = "33f501768e82e8548763b7f55309e2f8bcc7f9f4273c75b47af99ac2b2581f37"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-connect", "actix-connect",
@ -147,6 +123,7 @@ dependencies = [
"bitflags", "bitflags",
"brotli2", "brotli2",
"bytes", "bytes",
"cookie",
"copyless", "copyless",
"derive_more", "derive_more",
"either", "either",
@ -160,6 +137,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"indexmap", "indexmap",
"itoa",
"language-tags", "language-tags",
"lazy_static", "lazy_static",
"log", "log",
@ -171,7 +149,7 @@ dependencies = [
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sha-1", "sha-1 0.9.1",
"slab", "slab",
"time 0.2.16", "time 0.2.16",
] ]
@ -260,16 +238,16 @@ dependencies = [
[[package]] [[package]]
name = "actix-threadpool" name = "actix-threadpool"
version = "0.3.2" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff" checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30"
dependencies = [ dependencies = [
"derive_more", "derive_more",
"futures-channel", "futures-channel",
"lazy_static", "lazy_static",
"log", "log",
"num_cpus", "num_cpus",
"parking_lot 0.10.2", "parking_lot 0.11.0",
"threadpool", "threadpool",
] ]
@ -313,9 +291,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bd6df56ec5f9a1a0d8335f156f36e1e8f76dbd736fa0cc0f6bc3a69be1e6124" checksum = "9125c29b7d9911bfdb4d0d4d8f1cf4fee4f21515cf2a405a423c30c245364297"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@ -353,24 +331,25 @@ dependencies = [
[[package]] [[package]]
name = "actix-web-actors" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b5efeb3907582f9c724ce27be093ab8aafabd97be828bc6750c0d467f5e1aa3" checksum = "55ef22b33c49a28dda61866d5573c5b8ceb080a099cd59e7371b78b48bbf1bc0"
dependencies = [ dependencies = [
"actix", "actix",
"actix-codec", "actix-codec",
"actix-http", "actix-http",
"actix-web", "actix-web",
"bytes", "bytes",
"futures", "futures-channel",
"futures-core",
"pin-project", "pin-project",
] ]
[[package]] [[package]]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.2.2" version = "0.3.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766" checksum = "df9679f5b1f4c819de08b63b0a61a131b2fdc30b367c2c208984fda8eaa07fa0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -390,24 +369,18 @@ dependencies = [
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.12.2" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c" checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
[[package]] [[package]]
name = "adler" name = "adler"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@ -481,9 +454,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]] [[package]]
name = "awc" name = "awc"
version = "2.0.0-alpha.2" version = "2.0.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7038a9747cd5159b9f0550895eaf865c0143baa7e4eee834e9294d0a7e0e4be" checksum = "374057b508d4083208996be82141891c2e14c8885f45991b21c1621200ab6df3"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
@ -505,14 +478,14 @@ dependencies = [
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.49" version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c" checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide 0.3.7", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -590,7 +563,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [ dependencies = [
"generic-array 0.14.2", "generic-array 0.14.3",
] ]
[[package]] [[package]]
@ -599,7 +572,7 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
dependencies = [ dependencies = [
"generic-array 0.14.2", "generic-array 0.14.3",
] ]
[[package]] [[package]]
@ -642,6 +615,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "buf-min"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4"
dependencies = [
"bytes",
]
[[package]] [[package]]
name = "bufstream" name = "bufstream"
version = "0.1.4" version = "0.1.4"
@ -668,12 +650,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "0.5.5" version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
dependencies = [
"loom",
]
[[package]] [[package]]
name = "bytestring" name = "bytestring"
@ -686,9 +665,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.57" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe" checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -698,9 +677,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.12" version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4" checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits 0.2.12", "num-traits 0.2.12",
@ -770,6 +749,16 @@ dependencies = [
"serde-hjson", "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]] [[package]]
name = "copyless" name = "copyless"
version = "0.1.5" version = "0.1.5"
@ -794,9 +783,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]] [[package]]
name = "cpuid-bool" name = "cpuid-bool"
version = "0.1.0" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
@ -809,12 +798,12 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.4.2" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6"
dependencies = [ dependencies = [
"cfg-if",
"crossbeam-utils", "crossbeam-utils",
"maybe-uninit",
] ]
[[package]] [[package]]
@ -950,7 +939,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [ dependencies = [
"generic-array 0.14.2", "generic-array 0.14.3",
] ]
[[package]] [[package]]
@ -1142,7 +1131,7 @@ dependencies = [
"cfg-if", "cfg-if",
"crc32fast", "crc32fast",
"libc", "libc",
"miniz_oxide 0.4.0", "miniz_oxide",
] ]
[[package]] [[package]]
@ -1292,19 +1281,6 @@ dependencies = [
"byteorder", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.12.3" version = "0.12.3"
@ -1316,9 +1292,9 @@ dependencies = [
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.2" version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check 0.9.2", "version_check 0.9.2",
@ -1337,15 +1313,15 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.21.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@ -1354,10 +1330,19 @@ dependencies = [
"futures-util", "futures-util",
"http", "http",
"indexmap", "indexmap",
"log",
"slab", "slab",
"tokio", "tokio",
"tokio-util 0.3.1", "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]] [[package]]
@ -1371,9 +1356,9 @@ dependencies = [
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.14" version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -1472,18 +1457,19 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
dependencies = [ dependencies = [
"autocfg 1.0.0", "autocfg 1.0.0",
"hashbrown",
] ]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69da7ce1490173c2bf4d26bc8be429aaeeaf4cce6c4b970b7949651fa17655fe" checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485"
[[package]] [[package]]
name = "iovec" name = "iovec"
@ -1523,9 +1509,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.41" version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916" checksum = "52732a3d3ad72c58ad2dc70624f9c17b46ecd0943b9a4f1ee37c4c18c5d983e2"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@ -1573,19 +1559,21 @@ dependencies = [
"bcrypt", "bcrypt",
"chrono", "chrono",
"diesel", "diesel",
"lazy_static",
"log", "log",
"regex",
"serde 1.0.114", "serde 1.0.114",
"serde_json", "serde_json",
"sha2", "sha2",
"strum", "strum",
"strum_macros", "strum_macros",
"url",
] ]
[[package]] [[package]]
name = "lemmy_server" name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"activitystreams",
"activitystreams-ext", "activitystreams-ext",
"activitystreams-new", "activitystreams-new",
"actix", "actix",
@ -1693,9 +1681,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.71" version = "0.2.73"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9"
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
@ -1724,33 +1712,22 @@ dependencies = [
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de302ce1fe7482db13738fbaf2e21cfb06a986b89c0bf38d88abf16681aada4e" checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
] ]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.8" version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [ dependencies = [
"cfg-if", "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]] [[package]]
name = "lru-cache" name = "lru-cache"
version = "0.1.2" version = "0.1.2"
@ -1778,12 +1755,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.3" version = "2.3.3"
@ -1827,15 +1798,6 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.0" version = "0.4.0"
@ -2060,7 +2022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api 0.4.0", "lock_api 0.4.1",
"parking_lot_core 0.8.0", "parking_lot_core 0.8.0",
] ]
@ -2150,7 +2112,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [ dependencies = [
"maplit", "maplit",
"pest", "pest",
"sha-1", "sha-1 0.8.2",
] ]
[[package]] [[package]]
@ -2187,9 +2149,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
@ -2220,9 +2182,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.18" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@ -2434,9 +2396,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.56" version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]] [[package]]
name = "regex" name = "regex"
@ -2559,12 +2521,6 @@ dependencies = [
"parking_lot 0.11.0", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -2703,6 +2659,19 @@ dependencies = [
"opaque-debug 0.2.3", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.6.0" version = "0.6.0"
@ -2734,9 +2703,9 @@ dependencies = [
[[package]] [[package]]
name = "simple_asn1" name = "simple_asn1"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618" checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
dependencies = [ dependencies = [
"chrono", "chrono",
"num-bigint", "num-bigint",
@ -2751,9 +2720,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.4.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -2869,9 +2838,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.33" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3076,6 +3045,26 @@ dependencies = [
"tokio", "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]] [[package]]
name = "trust-dns-proto" name = "trust-dns-proto"
version = "0.19.5" version = "0.19.5"
@ -3132,17 +3121,6 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" 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]] [[package]]
name = "typenum" name = "typenum"
version = "1.12.0" version = "1.12.0"
@ -3251,18 +3229,19 @@ dependencies = [
[[package]] [[package]]
name = "v_escape" name = "v_escape"
version = "0.7.4" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6" checksum = "b66158ce426982197fd44266d68125fd4000f1d42f5ee33ef02b500b4b6b0024"
dependencies = [ dependencies = [
"buf-min",
"v_escape_derive", "v_escape_derive",
] ]
[[package]] [[package]]
name = "v_escape_derive" name = "v_escape_derive"
version = "0.5.6" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae" checksum = "cae7cffca0b1f9af9b20610f6fdeee9ffcce61417b5ad186a5d482dc904e24cd"
dependencies = [ dependencies = [
"nom 4.2.3", "nom 4.2.3",
"proc-macro2", "proc-macro2",
@ -3272,9 +3251,9 @@ dependencies = [
[[package]] [[package]]
name = "v_htmlescape" name = "v_htmlescape"
version = "0.4.5" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41" checksum = "f5fd25529cb2f78527b5ee507bcfb357b26d057b5e480853c26d49a4ead5c629"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"v_escape", "v_escape",
@ -3312,9 +3291,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.64" version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2" checksum = "f3edbcc9536ab7eababcc6d2374a0b7bfe13a2b6d562c5e07f370456b1a8f33d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@ -3322,9 +3301,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.64" version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df" checksum = "89ed2fb8c84bfad20ea66b26a3743f3e7ba8735a69fe7d95118c33ec8fc1244d"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"lazy_static", "lazy_static",
@ -3337,9 +3316,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.64" version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8" checksum = "eb071268b031a64d92fc6cf691715ca5a40950694d8f683c5bb43db7c730929e"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -3347,9 +3326,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.64" version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75" checksum = "cf592c807080719d1ff2f245a687cbadb3ed28b2077ed7084b47aba8b691f2c6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3360,15 +3339,15 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.64" version = "0.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae" checksum = "72b6c0220ded549d63860c78c38f3bcc558d1ca3f4efa74942c536ddbbb55e87"
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.41" version = "0.3.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d" checksum = "8be2398f326b7ba09815d0b403095f34dd708579220d099caae89be0b32137b2"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",

5
server/Cargo.toml vendored
View file

@ -18,9 +18,8 @@ lemmy_db = { path = "./lemmy_db" }
diesel = "1.4.4" diesel = "1.4.4"
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
dotenv = "0.15.0" dotenv = "0.15.0"
activitystreams = "0.6.2" activitystreams-new = { git = "https://yerbamate.dev/asonix/activitystreams-new", branch = "main" }
activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" } activitystreams-ext = { git = "https://yerbamate.dev/asonix/activitystreams-ext", branch = "main" }
activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
bcrypt = "0.8.0" bcrypt = "0.8.0"
chrono = { version = "0.4.7", features = ["serde"] } chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]} serde_json = { version = "1.0.52", features = ["preserve_order"]}

View file

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

View file

@ -117,7 +117,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_862362".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,

View file

@ -1,5 +1,6 @@
use super::{post::Post, *}; use super::{post::Post, *};
use crate::schema::{comment, comment_like, comment_saved}; use crate::schema::{comment, comment_like, comment_saved};
use url::{ParseError, Url};
// WITH RECURSIVE MyTree AS ( // WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL // SELECT * FROM comment WHERE parent_id IS NULL
@ -42,6 +43,12 @@ pub struct CommentForm {
pub local: bool, pub local: bool,
} }
impl CommentForm {
pub fn get_ap_id(&self) -> Result<Url, ParseError> {
Url::parse(&self.ap_id)
}
}
impl Crud<CommentForm> for Comment { impl Crud<CommentForm> for Comment {
fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
@ -90,14 +97,6 @@ impl Comment {
comment.filter(ap_id.eq(object_id)).first::<Self>(conn) comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
} }
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(true))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> { pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
@ -109,6 +108,46 @@ impl Comment {
)) ))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
comment_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
comment_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
comment_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
@ -226,7 +265,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_283687".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -246,7 +285,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_928738972".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

@ -498,7 +498,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_92873982".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -518,7 +518,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_7625376".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
naive_now,
schema::{community, community_follower, community_moderator, community_user_ban}, schema::{community, community_follower, community_moderator, community_user_ban},
Bannable, Bannable,
Crud, Crud,
@ -29,7 +30,6 @@ pub struct Community {
pub last_refreshed_at: chrono::NaiveDateTime, pub last_refreshed_at: chrono::NaiveDateTime,
} }
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)] #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"] #[table_name = "community"]
pub struct CommunityForm { pub struct CommunityForm {
@ -88,10 +88,10 @@ impl Community {
.first::<Self>(conn) .first::<Self>(conn)
} }
pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result<Self, Error> { pub fn read_from_actor_id(conn: &PgConnection, for_actor_id: &str) -> Result<Self, Error> {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
community community
.filter(actor_id.eq(community_id)) .filter(actor_id.eq(for_actor_id))
.first::<Self>(conn) .first::<Self>(conn)
} }
@ -99,6 +99,60 @@ impl Community {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
community.filter(local.eq(true)).load::<Community>(conn) community.filter(local.eq(true)).load::<Community>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
community_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
community_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_creator(
conn: &PgConnection,
community_id: i32,
new_creator_id: i32,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, Error> {
use crate::{community_view::CommunityModeratorView, user_view::UserView};
let mut mods_and_admins: Vec<i32> = Vec::new();
mods_and_admins.append(
&mut CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())?,
);
mods_and_admins
.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
Ok(mods_and_admins)
}
pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
Self::community_mods_and_admins(conn, community_id)
.unwrap_or_default()
.contains(&user_id)
}
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
@ -258,7 +312,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_8266238".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -278,7 +332,7 @@ mod tests {
removed: None, removed: None,
deleted: None, deleted: None,
updated: None, updated: None,
actor_id: "http://fake.com".into(), actor_id: "changeme_7625376".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,
@ -300,7 +354,7 @@ mod tests {
deleted: false, deleted: false,
published: inserted_community.published, published: inserted_community.published,
updated: None, updated: None,
actor_id: "http://fake.com".into(), actor_id: inserted_community.actor_id.to_owned(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

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

View file

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

View file

@ -470,7 +470,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_829398".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -497,7 +497,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_82982738".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -517,7 +517,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_283687".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

@ -105,7 +105,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_8292378".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,

View file

@ -8,6 +8,7 @@ use crate::{
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::{ParseError, Url};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "post"] #[table_name = "post"]
@ -56,6 +57,12 @@ pub struct PostForm {
pub local: bool, pub local: bool,
} }
impl PostForm {
pub fn get_ap_id(&self) -> Result<Url, ParseError> {
Url::parse(&self.ap_id)
}
}
impl Post { impl Post {
pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> { pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
@ -101,6 +108,50 @@ impl Post {
)) ))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn update_deleted(
conn: &PgConnection,
post_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
post_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(locked.eq(new_locked))
.get_result::<Self>(conn)
}
pub fn update_stickied(
conn: &PgConnection,
post_id: i32,
new_stickied: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(stickied.eq(new_stickied))
.get_result::<Self>(conn)
}
pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
user_id == post_creator_id
}
} }
impl Crud<PostForm> for Post { impl Crud<PostForm> for Post {
@ -272,7 +323,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_8292683678".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -292,7 +343,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_8223262378".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

@ -415,7 +415,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_8282738268".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -435,7 +435,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_2763".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

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

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
is_email_regex,
naive_now, naive_now,
schema::{user_, user_::dsl::*}, schema::{user_, user_::dsl::*},
Crud, Crud,
@ -125,9 +126,18 @@ impl User_ {
use crate::schema::user_::dsl::*; use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn) user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
} }
}
impl User_ { pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<Self, Error> {
if is_email_regex(username_or_email) {
Self::find_by_email(conn, username_or_email)
} else {
Self::find_by_username(conn, username_or_email)
}
}
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> { pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
user_.filter(name.eq(username)).first::<User_>(conn) user_.filter(name.eq(username)).first::<User_>(conn)
} }
@ -166,7 +176,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_9826382637".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -195,7 +205,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: inserted_user.actor_id.to_owned(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,

View file

@ -52,6 +52,30 @@ impl Crud<UserMentionForm> for UserMention {
} }
} }
impl UserMention {
pub fn update_read(
conn: &PgConnection,
user_mention_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(user_mention.find(user_mention_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(
user_mention
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
@ -86,7 +110,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_628763".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -113,7 +137,7 @@ mod tests {
lang: "browser".into(), lang: "browser".into(),
show_avatars: true, show_avatars: true,
send_notifications_to_email: false, send_notifications_to_email: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_927389278".into(),
bio: None, bio: None,
local: true, local: true,
private_key: None, private_key: None,
@ -133,7 +157,7 @@ mod tests {
deleted: None, deleted: None,
updated: None, updated: None,
nsfw: false, nsfw: false,
actor_id: "http://fake.com".into(), actor_id: "changeme_876238".into(),
local: true, local: true,
private_key: None, private_key: None,
public_key: None, public_key: None,

View file

@ -157,7 +157,28 @@ impl UserView {
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_fast::dsl::*; use super::user_view::user_fast::dsl::*;
use diesel::sql_types::{Nullable, Text};
user_fast user_fast
// The select is necessary here to not get back emails
.select((
id,
actor_id,
name,
avatar,
"".into_sql::<Nullable<Text>>(),
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)) .filter(admin.eq(true))
.order_by(published) .order_by(published)
.load::<Self>(conn) .load::<Self>(conn)
@ -165,6 +186,28 @@ impl UserView {
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_fast::dsl::*; use super::user_view::user_fast::dsl::*;
user_fast.filter(banned.eq(true)).load::<Self>(conn) use diesel::sql_types::{Nullable, Text};
user_fast
.select((
id,
actor_id,
name,
avatar,
"".into_sql::<Nullable<Text>>(),
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::<Self>(conn)
} }
} }

View file

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

View file

@ -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;

View file

@ -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));

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
apub::ActorType, apub::ActorType,
blocking, blocking,
websocket::{ websocket::{
@ -34,7 +34,6 @@ pub struct GetCommunity {
pub struct GetCommunityResponse { pub struct GetCommunityResponse {
pub community: CommunityView, pub community: CommunityView,
pub moderators: Vec<CommunityModeratorView>, pub moderators: Vec<CommunityModeratorView>,
pub admins: Vec<UserView>,
pub online: usize, pub online: usize,
} }
@ -98,13 +97,24 @@ pub struct AddModToCommunityResponse {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct EditCommunity { pub struct EditCommunity {
pub edit_id: i32, pub edit_id: i32,
name: String,
title: String, title: String,
description: Option<String>, description: Option<String>,
category_id: i32, category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool, nsfw: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeleteCommunity {
pub edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemoveCommunity {
pub edit_id: i32,
removed: bool,
reason: Option<String>, reason: Option<String>,
expires: Option<i64>, expires: Option<i64>,
auth: String, auth: String,
@ -185,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let site_creator_id = site.creator_id;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let online = if let Some(ws) = websocket_info { let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id { if let Some(id) = ws.id {
ws.chatserver.do_send(JoinCommunityRoom { ws.chatserver.do_send(JoinCommunityRoom {
@ -213,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
let res = GetCommunityResponse { let res = GetCommunityResponse {
community: community_view, community: community_view,
moderators, moderators,
admins,
online, online,
}; };
@ -264,6 +266,17 @@ impl Perform for Oper<CreateCommunity> {
return Err(APIError::err("site_ban").into()); 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 // When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?; let keypair = generate_actor_keypair()?;
@ -277,7 +290,7 @@ impl Perform for Oper<CreateCommunity> {
deleted: None, deleted: None,
nsfw: data.nsfw, nsfw: data.nsfw,
updated: None, updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(), actor_id,
local: true, local: true,
private_key: Some(keypair.private_key), private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key), public_key: Some(keypair.public_key),
@ -333,10 +346,6 @@ impl Perform for Oper<EditCommunity> {
) -> Result<CommunityResponse, LemmyError> { ) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self.data; 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) { if let Err(slurs) = slur_check(&data.title) {
return Err(APIError::err(&slurs_vec_to_str(slurs)).into()); return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
} }
@ -352,10 +361,6 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("not_logged_in").into()), 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; let user_id = claims.id;
// Check for a site ban // Check for a site ban
@ -364,37 +369,28 @@ impl Perform for Oper<EditCommunity> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Verify its a mod // Verify its a mod (only mods can edit it)
let edit_id = data.edit_id; let edit_id = data.edit_id;
let mut editors: Vec<i32> = Vec::new(); let mods: Vec<i32> = blocking(pool, move |conn| {
editors.append( CommunityModeratorView::for_community(conn, edit_id)
&mut blocking(pool, move |conn| { .map(|v| v.into_iter().map(|m| m.user_id).collect())
CommunityModeratorView::for_community(conn, edit_id) })
.map(|v| v.into_iter().map(|m| m.user_id).collect()) .await??;
}) if !mods.contains(&user_id) {
.await??, return Err(APIError::err("not_a_moderator").into());
);
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 edit_id = data.edit_id; let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??; let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
let community_form = CommunityForm { let community_form = CommunityForm {
name: data.name.to_owned(), name: read_community.name,
title: data.title.to_owned(), title: data.title.to_owned(),
description: data.description.to_owned(), description: data.description.to_owned(),
category_id: data.category_id.to_owned(), category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id, creator_id: read_community.creator_id,
removed: data.removed.to_owned(), removed: Some(read_community.removed),
deleted: data.deleted.to_owned(), deleted: Some(read_community.deleted),
nsfw: data.nsfw, nsfw: data.nsfw,
updated: Some(naive_now()), updated: Some(naive_now()),
actor_id: read_community.actor_id, actor_id: read_community.actor_id,
@ -406,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
}; };
let edit_id = data.edit_id; let edit_id = data.edit_id;
let updated_community = match blocking(pool, move |conn| { match blocking(pool, move |conn| {
Community::update(conn, edit_id, &community_form) Community::update(conn, edit_id, &community_form)
}) })
.await? .await?
@ -415,42 +411,77 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
}; };
// Mod tables // TODO there needs to be some kind of an apub update
if let Some(removed) = data.removed.to_owned() { // process for communities and users
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)), let edit_id = data.edit_id;
None => None, let community_view = blocking(pool, move |conn| {
}; CommunityView::read(conn, edit_id, Some(user_id))
let form = ModRemoveCommunityForm { })
mod_user_id: user_id, .await??;
community_id: data.edit_id,
removed: Some(removed), let res = CommunityResponse {
reason: data.reason.to_owned(), community: community_view,
expires, };
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??; send_community_websocket(&res, websocket_info, UserOperation::EditCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
} }
if let Some(deleted) = data.deleted.to_owned() { // Verify its the creator (only a creator can delete the community)
if deleted { let edit_id = data.edit_id;
updated_community let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
.send_delete(&user, &self.client, pool) if read_community.creator_id != user_id {
.await?; return Err(APIError::err("no_community_edit_allowed").into());
} else { }
updated_community
.send_undo_delete(&user, &self.client, pool) // Do the delete
.await?; let edit_id = data.edit_id;
} let deleted = data.deleted;
} else if let Some(removed) = data.removed.to_owned() { let updated_community = match blocking(pool, move |conn| {
if removed { Community::update_deleted(conn, edit_id, deleted)
updated_community })
.send_remove(&user, &self.client, pool) .await?
.await?; {
} else { Ok(community) => community,
updated_community Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
.send_undo_remove(&user, &self.client, pool) };
.await?;
} // 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; let edit_id = data.edit_id;
@ -463,20 +494,88 @@ impl Perform for Oper<EditCommunity> {
community: community_view, community: community_view,
}; };
if let Some(ws) = websocket_info { send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
ws.chatserver.do_send(SendCommunityRoomMessage { Ok(res)
op: UserOperation::EditCommunity, }
response: res_sent, }
community_id: data.edit_id,
my_id: ws.id, #[async_trait::async_trait(?Send)]
}); impl Perform for Oper<RemoveCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
} }
// Verify its an admin (only an admin can remove a community)
is_admin(pool, user_id).await?;
// Do the remove
let edit_id = data.edit_id;
let removed = data.removed;
let updated_community = match blocking(pool, move |conn| {
Community::update_removed(conn, edit_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_user_id: user_id,
community_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
// Apub messages
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse {
community: community_view,
};
send_community_websocket(&res, websocket_info, UserOperation::RemoveCommunity);
Ok(res) Ok(res)
} }
} }
@ -652,27 +751,10 @@ impl Perform for Oper<BanFromCommunity> {
let user_id = claims.id; let user_id = claims.id;
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id; let community_id = data.community_id;
community_moderators.append( // Verify that only mods or admins can ban
&mut blocking(pool, move |conn| { is_mod_or_admin(pool, user_id, community_id).await?;
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
let community_user_ban_form = CommunityUserBanForm { let community_user_ban_form = CommunityUserBanForm {
community_id: data.community_id, community_id: data.community_id,
@ -692,6 +774,7 @@ impl Perform for Oper<BanFromCommunity> {
} }
// Mod tables // Mod tables
// TODO eventually do correct expires
let expires = match data.expires { let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)), Some(time) => Some(naive_from_unix(time)),
None => None, None => None,
@ -751,27 +834,10 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id; let community_id = data.community_id;
community_moderators.append( // Verify that only mods or admins can add mod
&mut blocking(pool, move |conn| { is_mod_or_admin(pool, user_id, community_id).await?;
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
if data.added { if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
@ -850,26 +916,9 @@ impl Perform for Oper<TransferCommunity> {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
let community_form = CommunityForm {
name: read_community.name,
title: read_community.title,
description: read_community.description,
category_id: read_community.category_id,
creator_id: data.user_id, // This makes the new user the community creator
removed: None,
deleted: None,
nsfw: read_community.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
};
let community_id = data.community_id; let community_id = data.community_id;
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form); let new_creator = data.user_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(pool, update).await?.is_err() { if blocking(pool, update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into()); return Err(APIError::err("couldnt_update_community").into());
}; };
@ -939,8 +988,27 @@ impl Perform for Oper<TransferCommunity> {
Ok(GetCommunityResponse { Ok(GetCommunityResponse {
community: community_view, community: community_view,
moderators, moderators,
admins,
online: 0, online: 0,
}) })
} }
} }
pub fn send_community_websocket(
res: &CommunityResponse,
websocket_info: Option<WebsocketInfo>,
op: UserOperation,
) {
if let Some(ws) = websocket_info {
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
ws.chatserver.do_send(SendCommunityRoomMessage {
op,
response: res_sent,
community_id: res.community.id,
my_id: ws.id,
});
}
}

View file

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

View file

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

View file

@ -1,8 +1,9 @@
use super::user::Register; use super::user::Register;
use crate::{ use crate::{
api::{claims::Claims, APIError, Oper, Perform}, api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking, blocking,
version,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
DbPool, DbPool,
LemmyError, LemmyError,
@ -110,6 +111,7 @@ pub struct GetSiteResponse {
admins: Vec<UserView>, admins: Vec<UserView>,
banned: Vec<UserView>, banned: Vec<UserView>,
pub online: usize, pub online: usize,
version: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -255,10 +257,7 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; is_admin(pool, user_id).await?;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
let site_form = SiteForm { let site_form = SiteForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -309,10 +308,7 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id; let user_id = claims.id;
// Make sure user is an admin // Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??; is_admin(pool, user_id).await?;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
@ -424,6 +420,7 @@ impl Perform for Oper<GetSite> {
admins, admins,
banned, banned,
online, online,
version: version::VERSION.to_string(),
}) })
} }
} }
@ -666,6 +663,7 @@ impl Perform for Oper<TransferSite> {
admins, admins,
banned, banned,
online: 0, online: 0,
version: version::VERSION.to_string(),
}) })
} }
} }
@ -689,12 +687,7 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
// Only let admins read this // Only let admins read this
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; is_admin(pool, user_id).await?;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) {
return Err(APIError::err("not_an_admin").into());
}
let config_hjson = Settings::read_config_file()?; let config_hjson = Settings::read_config_file()?;

View file

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

View file

@ -10,41 +10,20 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams_new::base::AnyBase;
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{community::Community, user::User_}; use lemmy_db::{community::Community, user::User_};
use log::debug; use log::debug;
use serde::Serialize;
use std::fmt::Debug;
use url::Url; use url::Url;
pub fn populate_object_props( pub async fn send_activity_to_community(
props: &mut ObjectProperties,
addressed_ccs: Vec<String>,
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<A>(
creator: &User_, creator: &User_,
community: &Community, community: &Community,
to: Vec<String>, to: Vec<String>,
activity: A, activity: AnyBase,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<(), LemmyError> ) -> Result<(), LemmyError> {
where
A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
{
insert_activity(creator.id, activity.clone(), true, pool).await?; 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 // 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. /// Send an activity to a list of recipients, using the correct headers etc.
pub async fn send_activity<A>( pub async fn send_activity(
client: &Client, client: &Client,
activity: &A, activity: &AnyBase,
actor: &dyn ActorType, actor: &dyn ActorType,
to: Vec<String>, to: Vec<String>,
) -> Result<(), LemmyError> ) -> Result<(), LemmyError> {
where
A: Serialize,
{
let activity = serde_json::to_string(&activity)?; let activity = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", activity, to); debug!("Sending activitypub activity {} to {:?}", activity, to);

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity_to_community}, activities::send_activity_to_community,
create_apub_response, create_apub_response,
create_apub_tombstone_response, create_apub_tombstone_response,
create_tombstone, create_tombstone,
@ -21,13 +21,15 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams_new::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
base::AnyBase,
context, context,
link::Mention, 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 actix_web::{body::Body, client::Client, web::Path, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
use lemmy_db::{ use lemmy_db::{
@ -40,6 +42,8 @@ use lemmy_db::{
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData}; use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Error;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommentQuery { pub struct CommentQuery {
@ -66,8 +70,7 @@ impl ToApub for Comment {
type Response = Note; type Response = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut comment = Note::default(); let mut comment = Note::new();
let oprops: &mut ObjectProperties = comment.as_mut();
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; 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); 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) // Not needed when the Post is embedded in a collection (like for community outbox)
.set_context_xsd_any_uri(context())? .set_context(context())
.set_id(self.ap_id.to_owned())? .set_id(Url::parse(&self.ap_id)?)
.set_published(convert_datetime(self.published))? .set_published(convert_datetime(self.published))
.set_to_xsd_any_uri(community.actor_id)? .set_to(community.actor_id)
.set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)? .set_many_in_reply_tos(in_reply_to_vec)
.set_content_xsd_string(self.content.to_owned())? .set_content(self.content.to_owned())
.set_attributed_to_xsd_any_uri(creator.actor_id)?; .set_attributed_to(creator.actor_id);
if let Some(u) = self.updated { if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?; comment.set_updated(convert_datetime(u));
} }
Ok(comment) Ok(comment)
@ -110,7 +113,7 @@ impl ToApub for Comment {
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
self.updated, self.updated,
NoteType.to_string(), NoteType::Note.to_string(),
) )
} }
} }
@ -124,14 +127,25 @@ impl FromApub for CommentForm {
note: &Note, note: &Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
actor_id: &Url,
) -> Result<CommentForm, LemmyError> { ) -> Result<CommentForm, LemmyError> {
let oprops = &note.object_props; let creator_actor_id = &note
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); .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 mut in_reply_tos = note
let post_ap_id = in_reply_tos.next().unwrap().to_string(); .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. // 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?; 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 // For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<i32> = match in_reply_tos.next() { let parent_id: Option<i32> = match in_reply_tos.next() {
Some(parent_comment_uri) => { 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 = let parent_comment =
get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?; 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, creator_id: creator.id,
post_id: post.id, post_id: post.id,
parent_id, parent_id,
content: oprops content: note
.get_content_xsd_string() .content()
.map(|c| c.to_string()) .unwrap()
.unwrap(), .as_single_xsd_string()
.unwrap()
.to_string(),
removed: None, removed: None,
read: None, read: None,
published: oprops published: note.published().map(|u| u.to_owned().naive_local()),
.get_published() updated: note.updated().map(|u| u.to_owned().naive_local()),
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None, deleted: None,
ap_id: oprops.get_id().unwrap().to_string(), ap_id: note.id(actor_id.domain().unwrap())?.unwrap().to_string(),
local: false, local: false,
}) })
} }
@ -193,18 +205,24 @@ impl ApubObjectType for Comment {
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?; collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let mut create = Create::new(); let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
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)?;
create create
.create_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .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(()) Ok(())
} }
@ -227,18 +245,24 @@ impl ApubObjectType for Comment {
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?; collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let mut update = Update::new(); let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
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)?;
update update
.update_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .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(()) Ok(())
} }
@ -257,24 +281,18 @@ impl ApubObjectType for Comment {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete.into_any_base()?,
client, client,
pool, pool,
) )
@ -298,40 +316,28 @@ impl ApubObjectType for Comment {
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(delete)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )
@ -354,24 +360,18 @@ impl ApubObjectType for Comment {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove.into_any_base()?,
client, client,
pool, pool,
) )
@ -395,39 +395,27 @@ impl ApubObjectType for Comment {
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(remove)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )
@ -454,22 +442,18 @@ impl ApubLikeableType for Comment {
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like like
.like_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like.into_any_base()?,
client, client,
pool, pool,
) )
@ -493,22 +477,18 @@ impl ApubLikeableType for Comment {
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut dislike.object_props,
vec![community.get_followers_url()],
&id,
)?;
dislike dislike
.dislike_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike.into_any_base()?,
client, client,
pool, pool,
) )
@ -532,38 +512,28 @@ impl ApubLikeableType for Comment {
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like like
.like_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(like)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )
@ -578,6 +548,16 @@ struct MentionsAndAddresses {
tags: Vec<Mention>, tags: Vec<Mention>,
} }
impl MentionsAndAddresses {
fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
self
.tags
.iter()
.map(|t| t.to_owned().into_any_base())
.collect::<Result<Vec<AnyBase>, Error>>()
}
}
/// This takes a comment, and builds a list of to_addresses, inboxes, /// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to. /// and mention tags, so they know where to be sent to.
/// Addresses are the users / addresses that go in the cc field. /// 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? // TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention, client).await { if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
debug!("mention actor_id: {}", actor_id); 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 mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
let shared_inbox = mention_user.get_shared_inbox_url(); let shared_inbox = mention_user.get_shared_inbox_url();
mention_inboxes.push(shared_inbox); mention_inboxes.push(shared_inbox);
let mut mention_tag = Mention::new(); let mut mention_tag = Mention::new();
mention_tag mention_tag.set_href(actor_id).set_name(mention.full_name());
.link_props
.set_href(actor_id)?
.set_name_xsd_string(mention.full_name())?;
tags.push(mention_tag); tags.push(mention_tag);
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity}, activities::send_activity,
create_apub_response, create_apub_response,
create_apub_tombstone_response, create_apub_tombstone_response,
create_tombstone, create_tombstone,
@ -18,22 +18,16 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo},
Activity,
Base,
BaseBox,
};
use activitystreams_ext::Ext2; use activitystreams_ext::Ext2;
use activitystreams_new::{ use activitystreams_new::{
activity::Follow, activity::{Accept, Announce, Delete, Follow, Remove, Undo},
actor::{kind::GroupType, ApActor, Endpoints, Group}, actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt, base::{AnyBase, BaseExt},
collection::UnorderedCollection, collection::UnorderedCollection,
context, context,
object::Tombstone, object::Tombstone,
prelude::*, prelude::*,
primitives::{XsdAnyUri, XsdDateTime}, public,
}; };
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use itertools::Itertools; use itertools::Itertools;
@ -44,8 +38,8 @@ use lemmy_db::{
user::User_, user::User_,
}; };
use lemmy_utils::convert_datetime; use lemmy_utils::convert_datetime;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use std::{fmt::Debug, str::FromStr}; use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
@ -72,13 +66,13 @@ impl ToApub for Community {
let mut group = Group::new(); let mut group = Group::new();
group group
.set_context(context()) .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_name(self.name.to_owned())
.set_published(XsdDateTime::from(convert_datetime(self.published))) .set_published(convert_datetime(self.published))
.set_many_attributed_tos(moderators); .set_many_attributed_tos(moderators);
if let Some(u) = self.updated.to_owned() { 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() { if let Some(d) = self.description.to_owned() {
// TODO: this should be html, also add source field with raw markdown // TODO: this should be html, also add source field with raw markdown
@ -117,14 +111,14 @@ impl ToApub for Community {
self.deleted, self.deleted,
&self.actor_id, &self.actor_id,
self.updated, self.updated,
GroupType.to_string(), GroupType::Group.to_string(),
) )
} }
} }
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActorType for Community { impl ActorType for Community {
fn actor_id(&self) -> String { fn actor_id_str(&self) -> String {
self.actor_id.to_owned() 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. /// As a local community, accept the follow request from a remote user.
async fn send_accept_follow( async fn send_accept_follow(
&self, &self,
follow: &Follow, follow: Follow,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<(), LemmyError> { ) -> 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 id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
let mut accept = Accept::new(); let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
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 to = format!("{}/inbox", actor_uri); 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?; 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(()) Ok(())
} }
@ -172,17 +162,12 @@ impl ActorType for Community {
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![self.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
insert_activity(self.creator_id, delete.clone(), true, pool).await?; 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, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // 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(()) Ok(())
} }
@ -205,33 +190,22 @@ impl ActorType for Community {
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![self.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![self.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(delete)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
insert_activity(self.creator_id, undo.clone(), true, pool).await?; 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, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // 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(()) Ok(())
} }
@ -254,17 +228,12 @@ impl ActorType for Community {
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![self.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
insert_activity(mod_.id, remove.clone(), true, pool).await?; 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, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // 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(()) Ok(())
} }
@ -287,32 +256,21 @@ impl ActorType for Community {
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![self.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![self.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(remove)?; .set_to(public())
.set_many_ccs(vec![self.get_followers_url()]);
insert_activity(mod_.id, undo.clone(), true, pool).await?; 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, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for remove , the creator is the actor, and does the signing // 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(()) Ok(())
} }
@ -335,7 +293,7 @@ impl ActorType for Community {
.await??; .await??;
let inboxes = inboxes let inboxes = inboxes
.into_iter() .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()) .filter(|s| !s.is_empty())
.unique() .unique()
.collect(); .collect();
@ -367,8 +325,13 @@ impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> { async fn from_apub(
let creator_and_moderator_uris = group.attributed_to().unwrap(); group: &GroupExt,
client: &Client,
pool: &DbPool,
actor_id: &Url,
) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().unwrap();
let creator_uri = creator_and_moderator_uris let creator_uri = creator_and_moderator_uris
.as_many() .as_many()
.unwrap() .unwrap()
@ -378,26 +341,37 @@ impl FromApub for CommunityForm {
.as_xsd_any_uri() .as_xsd_any_uri()
.unwrap(); .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 { 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(), title: group.inner.preferred_username().unwrap().to_string(),
// TODO: should be parsed as html and tags like <script> removed (or use markdown source) // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc // -> same for post.content etc
description: group description: group
.inner
.content() .content()
.map(|s| s.as_single_xsd_string().unwrap().into()), .map(|s| s.as_single_xsd_string().unwrap().into()),
category_id: group.ext_one.category.identifier.parse::<i32>()?, category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id, creator_id: creator.id,
removed: None, removed: None,
published: group published: group.inner.published().map(|u| u.to_owned().naive_local()),
.published() updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
.map(|u| u.as_ref().to_owned().naive_local()),
updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
deleted: None, deleted: None,
nsfw: group.ext_one.sensitive, nsfw: group.ext_one.sensitive,
actor_id: group.id().unwrap().to_string(), actor_id: group
.inner
.id(actor_id.domain().unwrap())?
.unwrap()
.to_string(),
local: false, local: false,
private_key: None, private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem), public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
@ -450,26 +424,20 @@ pub async fn get_apub_community_followers(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
pub async fn do_announce<A>( pub async fn do_announce(
activity: A, activity: AnyBase,
community: &Community, community: &Community,
sender: &dyn ActorType, sender: &dyn ActorType,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<HttpResponse, LemmyError> ) -> Result<HttpResponse, LemmyError> {
where let id = format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4());
A: Activity + Base + Serialize + Debug, let mut announce = Announce::new(community.actor_id.to_owned(), activity);
{
let mut announce = Announce::default();
populate_object_props(
&mut announce.object_props,
vec![community.get_followers_url()],
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
)?;
announce announce
.announce_props .set_context(context())
.set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(activity)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
insert_activity(community.creator_id, announce.clone(), true, pool).await?; insert_activity(community.creator_id, announce.clone(), true, pool).await?;
@ -480,7 +448,7 @@ where
// this seems to be the "easiest" stable alternative for remove_item() // this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url()); to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(client, &announce, community, to).await?; send_activity(client, &announce.into_any_base()?, community, to).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -9,8 +9,10 @@ use crate::{
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
LemmyError, LemmyError,
}; };
use activitystreams::activity::Undo; use activitystreams_new::{
use activitystreams_new::activity::Follow; activity::{Follow, Undo},
prelude::*,
};
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{ use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
@ -32,14 +34,9 @@ impl CommunityAcceptedObjects {
fn follow(&self) -> Result<Follow, LemmyError> { fn follow(&self) -> Result<Follow, LemmyError> {
match self { match self {
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()), CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
CommunityAcceptedObjects::Undo(u) => Ok( CommunityAcceptedObjects::Undo(u) => {
u.undo_props Ok(Follow::from_any_base(u.object().as_one().unwrap().to_owned())?.unwrap())
.get_object_base_box() }
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Follow>()?,
),
} }
} }
} }
@ -72,11 +69,11 @@ pub async fn community_inbox(
&community.name, &input &community.name, &input
); );
let follow = input.follow()?; let follow = input.follow()?;
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let user_uri = follow.actor()?.as_single_xsd_any_uri().unwrap();
let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string(); let community_uri = follow.object().as_single_xsd_any_uri().unwrap();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &client, &db).await?; let community = get_or_fetch_and_upsert_remote_community(community_uri, &client, &db).await?;
verify(&request, &user)?; verify(&request, &user)?;
@ -108,7 +105,7 @@ async fn handle_follow(
}) })
.await?; .await?;
community.send_accept_follow(&follow, &client, &db).await?; community.send_accept_follow(follow, &client, &db).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -1,5 +1,6 @@
use crate::LemmyError; use crate::LemmyError;
use activitystreams::{ext::Extension, Actor}; use activitystreams_ext::UnparsedExtension;
use activitystreams_new::unparsed::UnparsedMutExt;
use diesel::PgConnection; use diesel::PgConnection;
use lemmy_db::{category::Category, Crud}; use lemmy_db::{category::Category, Crud};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -37,4 +38,22 @@ impl GroupExtension {
} }
} }
impl<T> Extension<T> for GroupExtension where T: Actor {} impl<U> UnparsedExtension<U> for GroupExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension {
category: unparsed_mut.remove("category")?,
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("category", self.category)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}
}

View file

@ -1,4 +1,5 @@
use activitystreams::{ext::Extension, Base}; use activitystreams_ext::UnparsedExtension;
use activitystreams_new::unparsed::UnparsedMutExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -8,4 +9,22 @@ pub struct PageExtension {
pub sensitive: bool, pub sensitive: bool,
} }
impl<T> Extension<T> for PageExtension where T: Base {} impl<U> UnparsedExtension<U> for PageExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PageExtension {
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}
}

View file

@ -1,5 +1,6 @@
use crate::{apub::ActorType, LemmyError}; use crate::{apub::ActorType, LemmyError};
use activitystreams::ext::Extension; use activitystreams_ext::UnparsedExtension;
use activitystreams_new::unparsed::UnparsedMutExt;
use actix_web::{client::ClientRequest, HttpRequest}; use actix_web::{client::ClientRequest, HttpRequest};
use http_signature_normalization_actix::{ use http_signature_normalization_actix::{
digest::{DigestClient, SignExt}, digest::{DigestClient, SignExt},
@ -24,7 +25,7 @@ pub async fn sign(
actor: &dyn ActorType, actor: &dyn ActorType,
activity: String, activity: String,
) -> Result<DigestClient<String>, LemmyError> { ) -> Result<DigestClient<String>, LemmyError> {
let signing_key_id = format!("{}#main-key", actor.actor_id()); let signing_key_id = format!("{}#main-key", actor.actor_id()?);
let private_key = actor.private_key(); let private_key = actor.private_key();
let digest_client = request let digest_client = request
@ -98,4 +99,20 @@ impl PublicKey {
} }
} }
impl<T> Extension<T> for PublicKeyExtension where T: activitystreams::Actor {} impl<U> UnparsedExtension<U> for PublicKeyExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PublicKeyExtension {
public_key: unparsed_mut.remove("publicKey")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("publicKey", self.public_key)?;
Ok(())
}
}

View file

@ -7,8 +7,7 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::object::Note; use activitystreams_new::{base::BaseExt, object::Note, prelude::*};
use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
use actix_web::client::Client; use actix_web::client::Client;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
@ -137,9 +136,10 @@ pub async fn search_by_apub_id(
users: vec![], users: vec![],
}; };
let domain = query_url.domain().unwrap();
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? { let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.id().unwrap().to_string(); let user_uri = p.inner.id(domain)?.unwrap();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
@ -148,10 +148,9 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Group(g) => { SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.id().unwrap().to_string(); let community_uri = g.inner.id(domain)?.unwrap();
let community = let community = get_or_fetch_and_upsert_remote_community(community_uri, client, pool).await?;
get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
// TODO Maybe at some point in the future, fetch all the history of a community // TODO Maybe at some point in the future, fetch all the history of a community
// fetch_community_outbox(&c, conn)?; // fetch_community_outbox(&c, conn)?;
@ -165,7 +164,7 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(p) => {
let post_form = PostForm::from_apub(&p, client, pool).await?; let post_form = PostForm::from_apub(&p, client, pool, &query_url).await?;
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??]; response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
@ -173,18 +172,13 @@ pub async fn search_by_apub_id(
response response
} }
SearchAcceptedObjects::Comment(c) => { SearchAcceptedObjects::Comment(c) => {
let post_url = c let post_url = c.in_reply_to().as_ref().unwrap().as_many().unwrap();
.object_props
.get_many_in_reply_to_xsd_any_uris()
.unwrap()
.next()
.unwrap()
.to_string();
// TODO: also fetch parent comments if any // TODO: also fetch parent comments if any
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?; let x = post_url.first().unwrap().as_xsd_any_uri().unwrap();
let post_form = PostForm::from_apub(&post, client, pool).await?; let post = fetch_remote_object(client, x).await?;
let comment_form = CommentForm::from_apub(&c, client, pool).await?; let post_form = PostForm::from_apub(&post, client, pool, &query_url).await?;
let comment_form = CommentForm::from_apub(&c, client, pool, &query_url).await?;
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??; blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??; let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
@ -200,13 +194,13 @@ pub async fn search_by_apub_id(
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user. /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
pub async fn get_or_fetch_and_upsert_remote_user( pub async fn get_or_fetch_and_upsert_remote_user(
apub_id: &str, apub_id: &Url,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<User_, LemmyError> { ) -> Result<User_, LemmyError> {
let apub_id_owned = apub_id.to_owned(); let apub_id_owned = apub_id.to_owned();
let user = blocking(pool, move |conn| { let user = blocking(pool, move |conn| {
User_::read_from_actor_id(conn, &apub_id_owned) User_::read_from_actor_id(conn, apub_id_owned.as_str())
}) })
.await?; .await?;
@ -214,9 +208,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
// If its older than a day, re-fetch it // If its older than a day, re-fetch it
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => { Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote user: {}", apub_id); debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
let mut uf = UserForm::from_apub(&person, client, pool).await?; let mut uf = UserForm::from_apub(&person, client, pool, apub_id).await?;
uf.last_refreshed_at = Some(naive_now()); uf.last_refreshed_at = Some(naive_now());
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??; let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
@ -225,9 +219,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
Ok(u) => Ok(u), Ok(u) => Ok(u),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id); debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?; let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
let uf = UserForm::from_apub(&person, client, pool).await?; let uf = UserForm::from_apub(&person, client, pool, apub_id).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??; let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user) Ok(user)
@ -252,22 +246,22 @@ fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community. /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
pub async fn get_or_fetch_and_upsert_remote_community( pub async fn get_or_fetch_and_upsert_remote_community(
apub_id: &str, apub_id: &Url,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<Community, LemmyError> { ) -> Result<Community, LemmyError> {
let apub_id_owned = apub_id.to_owned(); let apub_id_owned = apub_id.to_owned();
let community = blocking(pool, move |conn| { let community = blocking(pool, move |conn| {
Community::read_from_actor_id(conn, &apub_id_owned) Community::read_from_actor_id(conn, apub_id_owned.as_str())
}) })
.await?; .await?;
match community { match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => { Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id); debug!("Fetching and updating from remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let group = fetch_remote_object::<GroupExt>(client, apub_id).await?;
let mut cf = CommunityForm::from_apub(&group, client, pool).await?; let mut cf = CommunityForm::from_apub(&group, client, pool, apub_id).await?;
cf.last_refreshed_at = Some(naive_now()); cf.last_refreshed_at = Some(naive_now());
let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??; let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
@ -276,14 +270,14 @@ pub async fn get_or_fetch_and_upsert_remote_community(
Ok(c) => Ok(c), Ok(c) => Ok(c),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id); debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?; let group = fetch_remote_object::<GroupExt>(client, apub_id).await?;
let cf = CommunityForm::from_apub(&group, client, pool).await?; let cf = CommunityForm::from_apub(&group, client, pool, apub_id).await?;
let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??; let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
// Also add the community moderators too // Also add the community moderators too
let attributed_to = group.inner.attributed_to().unwrap(); let attributed_to = group.inner.attributed_to().unwrap();
let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to let creator_and_moderator_uris: Vec<&Url> = attributed_to
.as_many() .as_many()
.unwrap() .unwrap()
.iter() .iter()
@ -293,7 +287,7 @@ pub async fn get_or_fetch_and_upsert_remote_community(
let mut creator_and_moderators = Vec::new(); let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris { for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_remote_user(uri.as_str(), client, pool).await?; let c_or_m = get_or_fetch_and_upsert_remote_user(uri, client, pool).await?;
creator_and_moderators.push(c_or_m); creator_and_moderators.push(c_or_m);
} }
@ -328,13 +322,13 @@ fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyE
} }
pub async fn get_or_fetch_and_insert_remote_post( pub async fn get_or_fetch_and_insert_remote_post(
post_ap_id: &str, post_ap_id: &Url,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<Post, LemmyError> { ) -> Result<Post, LemmyError> {
let post_ap_id_owned = post_ap_id.to_owned(); let post_ap_id_owned = post_ap_id.to_owned();
let post = blocking(pool, move |conn| { let post = blocking(pool, move |conn| {
Post::read_from_apub_id(conn, &post_ap_id_owned) Post::read_from_apub_id(conn, post_ap_id_owned.as_str())
}) })
.await?; .await?;
@ -342,8 +336,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?; let post = fetch_remote_object::<PageExt>(client, post_ap_id).await?;
let post_form = PostForm::from_apub(&post, client, pool).await?; let post_form = PostForm::from_apub(&post, client, pool, post_ap_id).await?;
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??; let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
@ -363,13 +357,13 @@ fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Com
} }
pub async fn get_or_fetch_and_insert_remote_comment( pub async fn get_or_fetch_and_insert_remote_comment(
comment_ap_id: &str, comment_ap_id: &Url,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<Comment, LemmyError> { ) -> Result<Comment, LemmyError> {
let comment_ap_id_owned = comment_ap_id.to_owned(); let comment_ap_id_owned = comment_ap_id.to_owned();
let comment = blocking(pool, move |conn| { let comment = blocking(pool, move |conn| {
Comment::read_from_apub_id(conn, &comment_ap_id_owned) Comment::read_from_apub_id(conn, comment_ap_id_owned.as_str())
}) })
.await?; .await?;
@ -380,8 +374,8 @@ pub async fn get_or_fetch_and_insert_remote_comment(
"Fetching and creating remote comment and its parents: {}", "Fetching and creating remote comment and its parents: {}",
comment_ap_id comment_ap_id
); );
let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?; let comment = fetch_remote_object::<Note>(client, comment_ap_id).await?;
let comment_form = CommentForm::from_apub(&comment, client, pool).await?; let comment_form = CommentForm::from_apub(&comment, client, pool, comment_ap_id).await?;
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??; let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;

View file

@ -22,12 +22,11 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::object::Page;
use activitystreams_ext::{Ext1, Ext2}; use activitystreams_ext::{Ext1, Ext2};
use activitystreams_new::{ use activitystreams_new::{
activity::Follow, activity::Follow,
actor::{ApActor, Group, Person}, actor::{ApActor, Group, Person},
object::Tombstone, object::{Page, Tombstone},
prelude::*, prelude::*,
}; };
use actix_web::{body::Body, client::Client, HttpResponse}; use actix_web::{body::Body, client::Client, HttpResponse};
@ -37,7 +36,7 @@ use lemmy_db::{activity::do_insert_activity, user::User_};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData}; use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::{ParseError, Url};
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>; type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>; type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
@ -115,7 +114,7 @@ fn create_tombstone(
let mut tombstone = Tombstone::new(); let mut tombstone = Tombstone::new();
tombstone.set_id(object_id.parse()?); tombstone.set_id(object_id.parse()?);
tombstone.set_former_type(former_type); tombstone.set_former_type(former_type);
tombstone.set_deleted(convert_datetime(updated).into()); tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone) Ok(tombstone)
} else { } else {
Err(format_err!("Cant convert to tombstone because updated time was None.").into()) Err(format_err!("Cant convert to tombstone because updated time was None.").into())
@ -132,6 +131,7 @@ pub trait FromApub {
apub: &Self::ApubType, apub: &Self::ApubType,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
actor_id: &Url,
) -> Result<Self, LemmyError> ) -> Result<Self, LemmyError>
where where
Self: Sized; Self: Sized;
@ -199,13 +199,12 @@ pub trait ApubLikeableType {
) -> Result<(), LemmyError>; ) -> Result<(), LemmyError>;
} }
pub fn get_shared_inbox(actor_id: &str) -> String { pub fn get_shared_inbox(actor_id: &Url) -> String {
let url = Url::parse(actor_id).unwrap();
format!( format!(
"{}://{}{}/inbox", "{}://{}{}/inbox",
&url.scheme(), &actor_id.scheme(),
&url.host_str().unwrap(), &actor_id.host_str().unwrap(),
if let Some(port) = url.port() { if let Some(port) = actor_id.port() {
format!(":{}", port) format!(":{}", port)
} else { } else {
"".to_string() "".to_string()
@ -215,7 +214,7 @@ pub fn get_shared_inbox(actor_id: &str) -> String {
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait ActorType { pub trait ActorType {
fn actor_id(&self) -> String; fn actor_id_str(&self) -> String;
fn public_key(&self) -> String; fn public_key(&self) -> String;
fn private_key(&self) -> String; fn private_key(&self) -> String;
@ -239,7 +238,7 @@ pub trait ActorType {
#[allow(unused_variables)] #[allow(unused_variables)]
async fn send_accept_follow( async fn send_accept_follow(
&self, &self,
follow: &Follow, follow: Follow,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<(), LemmyError>; ) -> Result<(), LemmyError>;
@ -273,35 +272,40 @@ pub trait ActorType {
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>; async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
// TODO move these to the db rows fn actor_id(&self) -> Result<Url, ParseError> {
fn get_inbox_url(&self) -> String { Url::parse(&self.actor_id_str())
format!("{}/inbox", &self.actor_id())
} }
// TODO move these to the db rows
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id_str())
}
// TODO: make this return `Result<Url, ParseError>
fn get_shared_inbox_url(&self) -> String { fn get_shared_inbox_url(&self) -> String {
get_shared_inbox(&self.actor_id()) get_shared_inbox(&self.actor_id().unwrap())
} }
fn get_outbox_url(&self) -> String { fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id()) format!("{}/outbox", &self.actor_id_str())
} }
fn get_followers_url(&self) -> String { fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id()) format!("{}/followers", &self.actor_id_str())
} }
fn get_following_url(&self) -> String { fn get_following_url(&self) -> String {
format!("{}/following", &self.actor_id()) format!("{}/following", &self.actor_id_str())
} }
fn get_liked_url(&self) -> String { fn get_liked_url(&self) -> String {
format!("{}/liked", &self.actor_id()) format!("{}/liked", &self.actor_id_str())
} }
fn get_public_key_ext(&self) -> PublicKeyExtension { fn get_public_key_ext(&self) -> PublicKeyExtension {
PublicKey { PublicKey {
id: format!("{}#main-key", self.actor_id()), id: format!("{}#main-key", self.actor_id_str()),
owner: self.actor_id(), owner: self.actor_id_str(),
public_key_pem: self.public_key(), public_key_pem: self.public_key(),
} }
.to_ext() .to_ext()
@ -311,7 +315,7 @@ pub trait ActorType {
pub async fn fetch_webfinger_url( pub async fn fetch_webfinger_url(
mention: &MentionData, mention: &MentionData,
client: &Client, client: &Client,
) -> Result<String, LemmyError> { ) -> Result<Url, LemmyError> {
let fetch_url = format!( let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}", "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
get_apub_protocol_string(), get_apub_protocol_string(),
@ -336,6 +340,8 @@ pub async fn fetch_webfinger_url(
link link
.href .href
.to_owned() .to_owned()
.map(|u| Url::parse(&u))
.transpose()?
.ok_or_else(|| format_err!("No href found.").into()) .ok_or_else(|| format_err!("No href found.").into())
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
apub::{ apub::{
activities::{populate_object_props, send_activity_to_community}, activities::send_activity_to_community,
create_apub_response, create_apub_response,
create_apub_tombstone_response, create_apub_tombstone_response,
create_tombstone, create_tombstone,
@ -18,14 +18,14 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams_ext::Ext1;
use activitystreams_new::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
context, context,
object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page}, object::{kind::PageType, Image, Page, Tombstone},
BaseBox, prelude::*,
public,
}; };
use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use lemmy_db::{ use lemmy_db::{
community::Community, community::Community,
@ -35,6 +35,7 @@ use lemmy_db::{
}; };
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings}; use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
use serde::Deserialize; use serde::Deserialize;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PostQuery { pub struct PostQuery {
@ -62,8 +63,7 @@ impl ToApub for Post {
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = Page::default(); let mut page = Page::new();
let oprops: &mut ObjectProperties = page.as_mut();
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@ -71,54 +71,46 @@ impl ToApub for Post {
let community_id = self.community_id; let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
oprops page
// Not needed when the Post is embedded in a collection (like for community outbox) // Not needed when the Post is embedded in a collection (like for community outbox)
// TODO: need to set proper context defining sensitive/commentsEnabled fields // TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5 // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_context_xsd_any_uri(context())? .set_context(context())
.set_id(self.ap_id.to_owned())? .set_id(self.ap_id.parse::<Url>()?)
// Use summary field to be consistent with mastodon content warning. // Use summary field to be consistent with mastodon content warning.
// https://mastodon.xyz/@Louisa/103987265222901387.json // https://mastodon.xyz/@Louisa/103987265222901387.json
.set_summary_xsd_string(self.name.to_owned())? .set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))? .set_published(convert_datetime(self.published))
.set_to_xsd_any_uri(community.actor_id)? .set_to(community.actor_id)
.set_attributed_to_xsd_any_uri(creator.actor_id)?; .set_attributed_to(creator.actor_id);
if let Some(body) = &self.body { if let Some(body) = &self.body {
oprops.set_content_xsd_string(body.to_owned())?; page.set_content(body.to_owned());
} }
// TODO: hacky code because we get self.url == Some("") // TODO: hacky code because we get self.url == Some("")
// https://github.com/LemmyNet/lemmy/issues/602 // https://github.com/LemmyNet/lemmy/issues/602
let url = self.url.as_ref().filter(|u| !u.is_empty()); let url = self.url.as_ref().filter(|u| !u.is_empty());
if let Some(u) = url { if let Some(u) = url {
oprops.set_url_xsd_any_uri(u.to_owned())?; page.set_url(u.to_owned());
// Embeds // Embeds
let mut page_preview = Page::new(); let mut page_preview = Page::new();
page_preview page_preview.set_url(u.to_owned());
.object_props
.set_url_xsd_any_uri(u.to_owned())?;
if let Some(embed_title) = &self.embed_title { if let Some(embed_title) = &self.embed_title {
page_preview page_preview.set_name(embed_title.to_owned());
.object_props
.set_name_xsd_string(embed_title.to_owned())?;
} }
if let Some(embed_description) = &self.embed_description { if let Some(embed_description) = &self.embed_description {
page_preview page_preview.set_summary(embed_description.to_owned());
.object_props
.set_summary_xsd_string(embed_description.to_owned())?;
} }
if let Some(embed_html) = &self.embed_html { if let Some(embed_html) = &self.embed_html {
page_preview page_preview.set_content(embed_html.to_owned());
.object_props
.set_content_xsd_string(embed_html.to_owned())?;
} }
oprops.set_preview_base_box(page_preview)?; page.set_preview(page_preview.into_any_base()?);
} }
if let Some(thumbnail_url) = &self.thumbnail_url { if let Some(thumbnail_url) = &self.thumbnail_url {
@ -130,13 +122,12 @@ impl ToApub for Post {
); );
let mut image = Image::new(); let mut image = Image::new();
image.object_props.set_url_xsd_any_uri(full_url)?; image.set_url(full_url);
let any_image = AnyImage::from_concrete(image)?; page.set_image(image.into_any_base()?);
oprops.set_image_any_image(any_image)?;
} }
if let Some(u) = self.updated { if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?; page.set_updated(convert_datetime(u));
} }
let ext = PageExtension { let ext = PageExtension {
@ -151,7 +142,7 @@ impl ToApub for Post {
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
self.updated, self.updated,
PageType.to_string(), PageType::Page.to_string(),
) )
} }
} }
@ -165,63 +156,92 @@ impl FromApub for PostForm {
page: &PageExt, page: &PageExt,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
actor_id: &Url,
) -> Result<PostForm, LemmyError> { ) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one; let ext = &page.ext_one;
let oprops = &page.inner.object_props; let creator_actor_id = page
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); .inner
.attributed_to()
.as_ref()
.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 community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let community_actor_id = page
.inner
.to()
.as_ref()
.unwrap()
.as_single_xsd_any_uri()
.unwrap();
let community = let community =
get_or_fetch_and_upsert_remote_community(&community_actor_id, client, pool).await?; get_or_fetch_and_upsert_remote_community(community_actor_id, client, pool).await?;
let thumbnail_url = match oprops.get_image_any_image() { let thumbnail_url = match &page.inner.image() {
Some(any_image) => any_image Some(any_image) => Image::from_any_base(any_image.to_owned().as_one().unwrap().to_owned())?
.to_owned() .unwrap()
.into_concrete::<Image>()? .url()
.object_props .unwrap()
.get_url_xsd_any_uri() .as_single_xsd_any_uri()
.map(|u| u.to_string()), .map(|u| u.to_string()),
None => None, None => None,
}; };
let url = oprops.get_url_xsd_any_uri().map(|u| u.to_string()); let (embed_title, embed_description, embed_html) = match page.inner.preview() {
let (embed_title, embed_description, embed_html) = match oprops.get_preview_base_box() {
Some(preview) => { Some(preview) => {
let preview_page = preview.to_owned().into_concrete::<Page>()?; let preview_page = Page::from_any_base(preview.one().unwrap().to_owned())?.unwrap();
let name = preview_page let name = preview_page
.object_props .name()
.get_name_xsd_string() .map(|n| n.as_one().unwrap().as_xsd_string().unwrap().to_string());
.map(|n| n.to_string());
let summary = preview_page let summary = preview_page
.object_props .summary()
.get_summary_xsd_string() .map(|s| s.as_single_xsd_string().unwrap().to_string());
.map(|s| s.to_string());
let content = preview_page let content = preview_page
.object_props .content()
.get_content_xsd_string() .map(|c| c.as_single_xsd_string().unwrap().to_string());
.map(|c| c.to_string());
(name, summary, content) (name, summary, content)
} }
None => (None, None, None), None => (None, None, None),
}; };
let url = page
.inner
.url()
.as_ref()
.map(|u| u.as_single_xsd_string().unwrap().to_string());
let body = page
.inner
.content()
.as_ref()
.map(|c| c.as_single_xsd_string().unwrap().to_string());
Ok(PostForm { Ok(PostForm {
name: oprops.get_summary_xsd_string().unwrap().to_string(), name: page
.inner
.summary()
.as_ref()
.unwrap()
.as_single_xsd_string()
.unwrap()
.to_string(),
url, url,
body: oprops.get_content_xsd_string().map(|c| c.to_string()), body,
creator_id: creator.id, creator_id: creator.id,
community_id: community.id, community_id: community.id,
removed: None, removed: None,
locked: Some(!ext.comments_enabled), locked: Some(!ext.comments_enabled),
published: oprops published: page
.get_published() .inner
.map(|u| u.as_ref().to_owned().naive_local()), .published()
updated: oprops .as_ref()
.get_updated() .map(|u| u.to_owned().naive_local()),
.map(|u| u.as_ref().to_owned().naive_local()), updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None, deleted: None,
nsfw: ext.sensitive, nsfw: ext.sensitive,
stickied: None, // -> put it in "featured" collection of the community stickied: None, // -> put it in "featured" collection of the community
@ -229,7 +249,11 @@ impl FromApub for PostForm {
embed_description, embed_description,
embed_html, embed_html,
thumbnail_url, thumbnail_url,
ap_id: oprops.get_id().unwrap().to_string(), ap_id: page
.inner
.id(actor_id.domain().unwrap())?
.unwrap()
.to_string(),
local: false, local: false,
}) })
} }
@ -251,22 +275,18 @@ impl ApubObjectType for Post {
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let mut create = Create::new(); let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut create.object_props,
vec![community.get_followers_url()],
&id,
)?;
create create
.create_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
creator, creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
create, create.into_any_base()?,
client, client,
pool, pool,
) )
@ -288,22 +308,18 @@ impl ApubObjectType for Post {
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let mut update = Update::new(); let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut update.object_props,
vec![community.get_followers_url()],
&id,
)?;
update update
.update_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
creator, creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
update, update.into_any_base()?,
client, client,
pool, pool,
) )
@ -323,24 +339,18 @@ impl ApubObjectType for Post {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
creator, creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete.into_any_base()?,
client, client,
pool, pool,
) )
@ -360,40 +370,28 @@ impl ApubObjectType for Post {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(delete)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
creator, creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )
@ -413,24 +411,18 @@ impl ApubObjectType for Post {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
mod_, mod_,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove.into_any_base()?,
client, client,
pool, pool,
) )
@ -450,39 +442,27 @@ impl ApubObjectType for Post {
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove remove
.remove_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(remove)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
mod_, mod_,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )
@ -506,22 +486,18 @@ impl ApubLikeableType for Post {
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like like
.like_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like.into_any_base()?,
client, client,
pool, pool,
) )
@ -542,22 +518,18 @@ impl ApubLikeableType for Post {
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut dislike.object_props,
vec![community.get_followers_url()],
&id,
)?;
dislike dislike
.dislike_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike.into_any_base()?,
client, client,
pool, pool,
) )
@ -578,38 +550,28 @@ impl ApubLikeableType for Post {
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like like
.like_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo undo
.undo_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&undo_id)?)
.set_object_base_box(like)?; .set_to(public())
.set_many_ccs(vec![community.get_followers_url()]);
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo.into_any_base()?,
client, client,
pool, pool,
) )

View file

@ -12,12 +12,12 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams_new::{
activity::{Create, Delete, Undo, Update}, activity::{Create, Delete, Undo, Update},
context, context,
object::{kind::NoteType, properties::ObjectProperties, Note}, object::{kind::NoteType, Note, Tombstone},
prelude::*,
}; };
use activitystreams_new::object::Tombstone;
use actix_web::client::Client; use actix_web::client::Client;
use lemmy_db::{ use lemmy_db::{
private_message::{PrivateMessage, PrivateMessageForm}, private_message::{PrivateMessage, PrivateMessageForm},
@ -25,14 +25,14 @@ use lemmy_db::{
Crud, Crud,
}; };
use lemmy_utils::convert_datetime; use lemmy_utils::convert_datetime;
use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
type Response = Note; type Response = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut private_message = Note::default(); let mut private_message = Note::new();
let oprops: &mut ObjectProperties = private_message.as_mut();
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@ -40,16 +40,16 @@ impl ToApub for PrivateMessage {
let recipient_id = self.recipient_id; let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
oprops private_message
.set_context_xsd_any_uri(context())? .set_context(context())
.set_id(self.ap_id.to_owned())? .set_id(Url::parse(&self.ap_id.to_owned())?)
.set_published(convert_datetime(self.published))? .set_published(convert_datetime(self.published))
.set_content_xsd_string(self.content.to_owned())? .set_content(self.content.to_owned())
.set_to_xsd_any_uri(recipient.actor_id)? .set_to(recipient.actor_id)
.set_attributed_to_xsd_any_uri(creator.actor_id)?; .set_attributed_to(creator.actor_id);
if let Some(u) = self.updated { if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?; private_message.set_updated(convert_datetime(u));
} }
Ok(private_message) Ok(private_message)
@ -60,7 +60,7 @@ impl ToApub for PrivateMessage {
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
self.updated, self.updated,
NoteType.to_string(), NoteType::Note.to_string(),
) )
} }
} }
@ -74,32 +74,35 @@ impl FromApub for PrivateMessageForm {
note: &Note, note: &Note,
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
actor_id: &Url,
) -> Result<PrivateMessageForm, LemmyError> { ) -> Result<PrivateMessageForm, LemmyError> {
let oprops = &note.object_props; let creator_actor_id = note
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); .attributed_to()
.unwrap()
.clone()
.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 recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let recipient_actor_id = note.to().unwrap().clone().single_xsd_any_uri().unwrap();
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?; let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?;
Ok(PrivateMessageForm { Ok(PrivateMessageForm {
creator_id: creator.id, creator_id: creator.id,
recipient_id: recipient.id, recipient_id: recipient.id,
content: oprops content: note
.get_content_xsd_string() .content()
.map(|c| c.to_string()) .unwrap()
.unwrap(), .as_single_xsd_string()
published: oprops .unwrap()
.get_published() .to_string(),
.map(|u| u.as_ref().to_owned().naive_local()), published: note.published().map(|u| u.to_owned().naive_local()),
updated: oprops updated: note.updated().map(|u| u.to_owned().naive_local()),
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None, deleted: None,
read: None, read: None,
ap_id: oprops.get_id().unwrap().to_string(), ap_id: note.id(actor_id.domain().unwrap())?.unwrap().to_string(),
local: false, local: false,
}) })
} }
@ -120,21 +123,16 @@ impl ApubObjectType for PrivateMessage {
let recipient_id = self.recipient_id; let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut create = Create::new(); let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
create
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id); let to = format!("{}/inbox", recipient.actor_id);
create create
.create_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(to.clone());
insert_activity(creator.id, create.clone(), true, pool).await?; insert_activity(creator.id, create.clone(), true, pool).await?;
send_activity(client, &create, creator, vec![to]).await?; send_activity(client, &create.into_any_base()?, creator, vec![to]).await?;
Ok(()) Ok(())
} }
@ -151,21 +149,16 @@ impl ApubObjectType for PrivateMessage {
let recipient_id = self.recipient_id; let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut update = Update::new(); let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
update
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id); let to = format!("{}/inbox", recipient.actor_id);
update update
.update_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(to.clone());
insert_activity(creator.id, update.clone(), true, pool).await?; insert_activity(creator.id, update.clone(), true, pool).await?;
send_activity(client, &update, creator, vec![to]).await?; send_activity(client, &update.into_any_base()?, creator, vec![to]).await?;
Ok(()) Ok(())
} }
@ -181,21 +174,16 @@ impl ApubObjectType for PrivateMessage {
let recipient_id = self.recipient_id; let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id); let to = format!("{}/inbox", recipient.actor_id);
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(to.clone());
insert_activity(creator.id, delete.clone(), true, pool).await?; insert_activity(creator.id, delete.clone(), true, pool).await?;
send_activity(client, &delete, creator, vec![to]).await?; send_activity(client, &delete.into_any_base()?, creator, vec![to]).await?;
Ok(()) Ok(())
} }
@ -211,36 +199,25 @@ impl ApubObjectType for PrivateMessage {
let recipient_id = self.recipient_id; let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id); let to = format!("{}/inbox", recipient.actor_id);
delete delete
.delete_props .set_context(context())
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_id(Url::parse(&id)?)
.set_object_base_box(note)?; .set_to(to.clone());
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default(); let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
undo undo
.object_props .set_context(context())
.set_context_xsd_any_uri(context())? .set_id(Url::parse(&undo_id)?)
.set_id(undo_id)?; .set_to(to.clone());
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
insert_activity(creator.id, undo.clone(), true, pool).await?; insert_activity(creator.id, undo.clone(), true, pool).await?;
send_activity(client, &undo, creator, vec![to]).await?; send_activity(client, &undo.into_any_base()?, creator, vec![to]).await?;
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
use crate::{ use crate::{
api::claims::Claims,
apub::{ apub::{
activities::send_activity, activities::send_activity,
create_apub_response, create_apub_response,
@ -21,16 +20,15 @@ use activitystreams_new::{
context, context,
object::{Image, Tombstone}, object::{Image, Tombstone},
prelude::*, prelude::*,
primitives::{XsdAnyUri, XsdDateTime},
}; };
use actix_web::{body::Body, client::Client, web, HttpResponse}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use failure::_core::str::FromStr;
use lemmy_db::{ use lemmy_db::{
naive_now, naive_now,
user::{UserForm, User_}, user::{UserForm, User_},
}; };
use lemmy_utils::convert_datetime; use lemmy_utils::convert_datetime;
use serde::Deserialize; use serde::Deserialize;
use url::Url;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UserQuery { pub struct UserQuery {
@ -47,12 +45,12 @@ impl ToApub for User_ {
let mut person = Person::new(); let mut person = Person::new();
person person
.set_context(context()) .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_name(self.name.to_owned())
.set_published(XsdDateTime::from(convert_datetime(self.published))); .set_published(convert_datetime(self.published));
if let Some(u) = self.updated { if let Some(u) = self.updated {
person.set_updated(XsdDateTime::from(convert_datetime(u))); person.set_updated(convert_datetime(u));
} }
if let Some(avatar_url) = &self.avatar { if let Some(avatar_url) = &self.avatar {
@ -85,7 +83,7 @@ impl ToApub for User_ {
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ActorType for User_ { impl ActorType for User_ {
fn actor_id(&self) -> String { fn actor_id_str(&self) -> String {
self.actor_id.to_owned() self.actor_id.to_owned()
} }
@ -111,7 +109,7 @@ impl ActorType for User_ {
insert_activity(self.id, follow.clone(), true, pool).await?; insert_activity(self.id, follow.clone(), true, pool).await?;
send_activity(client, &follow, self, vec![to]).await?; send_activity(client, &follow.into_any_base()?, self, vec![to]).await?;
Ok(()) Ok(())
} }
@ -130,12 +128,12 @@ impl ActorType for User_ {
// TODO // TODO
// Undo that fake activity // Undo that fake activity
let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4()); let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?); let mut undo = Undo::new(Url::parse(&self.actor_id)?, follow.into_any_base()?);
undo.set_context(context()).set_id(undo_id.parse()?); undo.set_context(context()).set_id(undo_id.parse()?);
insert_activity(self.id, undo.clone(), true, pool).await?; insert_activity(self.id, undo.clone(), true, pool).await?;
send_activity(client, &undo, self, vec![to]).await?; send_activity(client, &undo.into_any_base()?, self, vec![to]).await?;
Ok(()) Ok(())
} }
@ -177,7 +175,7 @@ impl ActorType for User_ {
async fn send_accept_follow( async fn send_accept_follow(
&self, &self,
_follow: &Follow, _follow: Follow,
_client: &Client, _client: &Client,
_pool: &DbPool, _pool: &DbPool,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
@ -193,12 +191,17 @@ impl ActorType for User_ {
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> { async fn from_apub(
person: &PersonExt,
_: &Client,
_: &DbPool,
actor_id: &Url,
) -> Result<Self, LemmyError> {
let avatar = match person.icon() { let avatar = match person.icon() {
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone()) Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
.unwrap() .unwrap()
.unwrap() .unwrap()
.url .url()
.unwrap() .unwrap()
.as_single_xsd_any_uri() .as_single_xsd_any_uri()
.map(|u| u.to_string()), .map(|u| u.to_string()),
@ -209,18 +212,18 @@ impl FromApub for UserForm {
name: person name: person
.name() .name()
.unwrap() .unwrap()
.as_single_xsd_string() .one()
.unwrap() .unwrap()
.into(), .as_xsd_string()
.unwrap()
.to_string(),
preferred_username: person.inner.preferred_username().map(|u| u.to_string()), preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
password_encrypted: "".to_string(), password_encrypted: "".to_string(),
admin: false, admin: false,
banned: false, banned: false,
email: None, email: None,
avatar, avatar,
updated: person updated: person.updated().map(|u| u.to_owned().naive_local()),
.updated()
.map(|u| u.as_ref().to_owned().naive_local()),
show_nsfw: false, show_nsfw: false,
theme: "".to_string(), theme: "".to_string(),
default_sort_type: 0, default_sort_type: 0,
@ -229,8 +232,9 @@ impl FromApub for UserForm {
show_avatars: false, show_avatars: false,
send_notifications_to_email: false, send_notifications_to_email: false,
matrix_user_id: None, matrix_user_id: None,
actor_id: person.id().unwrap().to_string(), actor_id: person.id(actor_id.domain().unwrap())?.unwrap().to_string(),
bio: person bio: person
.inner
.summary() .summary()
.map(|s| s.as_single_xsd_string().unwrap().into()), .map(|s| s.as_single_xsd_string().unwrap().into()),
local: false, local: false,
@ -248,7 +252,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name; let user_name = info.into_inner().user_name;
let user = blocking(&db, move |conn| { let user = blocking(&db, move |conn| {
Claims::find_by_email_or_username(conn, &user_name) User_::find_by_email_or_username(conn, &user_name)
}) })
.await??; .await??;
let u = user.to_apub(&db).await?; let u = user.to_apub(&db).await?;

View file

@ -12,9 +12,10 @@ use crate::{
DbPool, DbPool,
LemmyError, LemmyError,
}; };
use activitystreams::{ use activitystreams_new::{
activity::{Accept, Create, Delete, Undo, Update}, activity::{Accept, Create, Delete, Undo, Update},
object::Note, object::Note,
prelude::*,
}; };
use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use lemmy_db::{ use lemmy_db::{
@ -79,11 +80,7 @@ async fn receive_accept(
client: &Client, client: &Client,
pool: &DbPool, pool: &DbPool,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let community_uri = accept let community_uri = accept.actor()?.to_owned().single_xsd_any_uri().unwrap();
.accept_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?; let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
verify(request, &community)?; verify(request, &community)?;
@ -116,26 +113,15 @@ async fn receive_create_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = create let user_uri = &create.actor()?.to_owned().single_xsd_any_uri().unwrap();
.create_props let note = Note::from_any_base(create.object().as_one().unwrap().to_owned())?.unwrap();
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = create let user = get_or_fetch_and_upsert_remote_user(user_uri, client, pool).await?;
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(user.id, create, false, pool).await?; insert_activity(user.id, create, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&note, client, pool, user_uri).await?;
let inserted_private_message = blocking(pool, move |conn| { let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message) PrivateMessage::create(conn, &private_message)
@ -168,26 +154,15 @@ async fn receive_update_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = update let user_uri = &update.actor()?.to_owned().single_xsd_any_uri().unwrap();
.update_props let note = Note::from_any_base(update.object().as_one().unwrap().to_owned())?.unwrap();
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(user.id, update, false, pool).await?; insert_activity(user.id, update, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool, user_uri).await?;
let private_message_ap_id = private_message_form.ap_id.clone(); let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -228,26 +203,15 @@ async fn receive_delete_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let note = delete let user_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
.delete_props let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool, user_uri).await?;
let private_message_ap_id = private_message_form.ap_id; let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| { let private_message = blocking(pool, move |conn| {
@ -300,34 +264,16 @@ async fn receive_undo_delete_private_message(
pool: &DbPool, pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let delete = undo let delete = Delete::from_any_base(undo.object().as_one().unwrap().to_owned())?.unwrap();
.undo_props let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
.get_object_base_box() let user_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Delete>()?;
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(user.id, delete, false, pool).await?; insert_activity(user.id, delete, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?; let private_message = PrivateMessageForm::from_apub(&note, client, pool, user_uri).await?;
let private_message_ap_id = private_message.ap_id.clone(); let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| { let private_message_id = blocking(pool, move |conn| {

View file

@ -30,7 +30,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
// Update the actor_id, private_key, and public_key, last_refreshed_at // Update the actor_id, private_key, and public_key, last_refreshed_at
let incorrect_users = user_ let incorrect_users = user_
.filter(actor_id.eq("http://fake.com")) .filter(actor_id.like("changeme_%"))
.filter(local.eq(true)) .filter(local.eq(true))
.load::<User_>(conn)?; .load::<User_>(conn)?;
@ -81,7 +81,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
// Update the actor_id, private_key, and public_key, last_refreshed_at // Update the actor_id, private_key, and public_key, last_refreshed_at
let incorrect_communities = community let incorrect_communities = community
.filter(actor_id.eq("http://fake.com")) .filter(actor_id.like("changeme_%"))
.filter(local.eq(true)) .filter(local.eq(true))
.load::<Community>(conn)?; .load::<Community>(conn)?;

View file

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

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.20"; pub const VERSION: &str = "v0.7.26";

View file

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

View file

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

2
ui/package.json vendored
View file

@ -37,6 +37,8 @@
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"prettier": "^2.0.4", "prettier": "^2.0.4",

View file

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

View file

@ -46,6 +46,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
admins: [], admins: [],
banned: [], banned: [],
online: null, online: null,
version: null,
}, },
siteConfigForm: { siteConfigForm: {
config_hjson: null, config_hjson: null,

View file

@ -1,8 +1,7 @@
import { Component, linkEvent } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { Prompt } from 'inferno-router';
import { import {
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
CommentForm as CommentFormI, CommentForm as CommentFormI,
@ -10,22 +9,11 @@ import {
UserOperation, UserOperation,
CommentResponse, CommentResponse,
} from '../interfaces'; } from '../interfaces';
import { import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
capitalizeFirstLetter,
mdToHtml,
randomStr,
markdownHelpUrl,
toast,
setupTribute,
wsJsonToRes,
pictrsDeleteToast,
} from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
import { MarkdownTextArea } from './markdown-textarea';
interface CommentFormProps { interface CommentFormProps {
postId?: number; postId?: number;
@ -39,15 +27,10 @@ interface CommentFormProps {
interface CommentFormState { interface CommentFormState {
commentForm: CommentFormI; commentForm: CommentFormI;
buttonTitle: string; buttonTitle: string;
previewMode: boolean; finished: boolean;
loading: boolean;
imageLoading: boolean;
} }
export class CommentForm extends Component<CommentFormProps, CommentFormState> { export class CommentForm extends Component<CommentFormProps, CommentFormState> {
private id = `comment-textarea-${randomStr()}`;
private formId = `comment-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommentFormState = { private emptyState: CommentFormState = {
commentForm: { commentForm: {
@ -65,15 +48,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
: this.props.edit : this.props.edit
? capitalizeFirstLetter(i18n.t('save')) ? capitalizeFirstLetter(i18n.t('save'))
: capitalizeFirstLetter(i18n.t('reply')), : capitalizeFirstLetter(i18n.t('reply')),
previewMode: false, finished: false,
loading: false,
imageLoading: false,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.tribute = setupTribute(); this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
this.handleReplyCancel = this.handleReplyCancel.bind(this);
this.state = this.emptyState; this.state = this.emptyState;
@ -98,167 +80,34 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
); );
} }
componentDidMount() {
let textarea: any = document.getElementById(this.id);
if (textarea) {
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.commentForm.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
// Quoting of selected text
let selectedText = window.getSelection().toString();
if (selectedText) {
let quotedText =
selectedText
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.commentForm.content = quotedText;
this.setState(this.state);
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}
if (this.props.focus) {
textarea.focus();
}
}
}
componentDidUpdate() {
if (this.state.commentForm.content) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
}
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
window.onbeforeunload = null;
} }
render() { render() {
return ( return (
<div class="mb-3"> <div class="mb-3">
<Prompt
when={this.state.commentForm.content}
message={i18n.t('block_leaving')}
/>
{UserService.Instance.user ? ( {UserService.Instance.user ? (
<form <MarkdownTextArea
id={this.formId} initialContent={this.state.commentForm.content}
onSubmit={linkEvent(this, this.handleCommentSubmit)} buttonTitle={this.state.buttonTitle}
> finished={this.state.finished}
<div class="form-group row"> replyType={!!this.props.node}
<div className={`col-sm-12`}> focus={this.props.focus}
<textarea disabled={this.props.disabled}
id={this.id} onSubmit={this.handleCommentSubmit}
className={`form-control ${ onReplyCancel={this.handleReplyCancel}
this.state.previewMode && 'd-none' />
}`}
value={this.state.commentForm.content}
onInput={linkEvent(this, this.handleCommentContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
required
disabled={this.props.disabled}
rows={2}
maxLength={10000}
/>
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content
)}
/>
)}
</div>
</div>
<div class="row">
<div class="col-sm-12">
<button
type="submit"
class="btn btn-sm btn-secondary mr-2"
disabled={this.props.disabled || this.state.loading}
>
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.state.buttonTitle}</span>
)}
</button>
{this.state.commentForm.content && (
<button
className={`btn btn-sm mr-2 btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.node && (
<button
type="button"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t('cancel')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
</svg>
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{this.state.imageLoading && (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
)}
</div>
</div>
</form>
) : ( ) : (
<div class="alert alert-warning" role="alert"> <div class="alert alert-light" role="alert">
<svg class="icon icon-inline mr-2"> <svg class="icon icon-inline mr-2">
<use xlinkHref="#icon-alert-triangle"></use> <use xlinkHref="#icon-alert-triangle"></use>
</svg> </svg>
<T i18nKey="must_login" class="d-inline"> <T i18nKey="must_login" class="d-inline">
#<Link to="/login">#</Link> #
<Link class="alert-link" to="/login">
#
</Link>
</T> </T>
</div> </div>
)} )}
@ -266,128 +115,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
); );
} }
handleFinished(op: UserOperation, data: CommentResponse) { handleCommentSubmit(msg: { val: string; formId: string }) {
let isReply = this.state.commentForm.content = msg.val;
this.props.node !== undefined && data.comment.parent_id !== null; this.state.commentForm.form_id = msg.formId;
let xor = if (this.props.edit) {
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined); WebSocketService.Instance.editComment(this.state.commentForm);
if (
(data.comment.creator_id == UserService.Instance.user.id &&
((op == UserOperation.CreateComment &&
// If its a reply, make sure parent child match
isReply &&
data.comment.parent_id == this.props.node.comment.id) ||
// Otherwise, check the XOR of the two
(!isReply && xor))) ||
// If its a comment edit, only check that its from your user, and that its a
// text edit only
(data.comment.creator_id == UserService.Instance.user.id &&
op == UserOperation.EditComment &&
data.comment.content)
) {
this.state.previewMode = false;
this.state.loading = false;
this.state.commentForm.content = '';
this.setState(this.state);
let form: any = document.getElementById(this.formId);
form.reset();
if (this.props.node) {
this.props.onReplyCancel();
}
autosize.update(form);
this.setState(this.state);
}
}
handleCommentSubmit(i: CommentForm, event: any) {
event.preventDefault();
if (i.props.edit) {
WebSocketService.Instance.editComment(i.state.commentForm);
} else { } else {
WebSocketService.Instance.createComment(i.state.commentForm); WebSocketService.Instance.createComment(this.state.commentForm);
} }
this.setState(this.state);
i.state.loading = true;
i.setState(i.state);
} }
handleCommentContentChange(i: CommentForm, event: any) { handleReplyCancel() {
i.state.commentForm.content = event.target.value; this.props.onReplyCancel();
i.setState(i.state);
}
handlePreviewToggle(i: CommentForm, event: any) {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
handleReplyCancel(i: CommentForm) {
i.props.onReplyCancel();
}
handleImageUploadPaste(i: CommentForm, event: any) {
let image = event.clipboardData.files[0];
if (image) {
i.handleImageUpload(i, image);
}
}
handleImageUpload(i: CommentForm, event: any) {
let file: any;
if (event.target) {
event.preventDefault();
file = event.target.files[0];
} else {
file = event;
}
const imageUploadUrl = `/pictrs/image`;
const formData = new FormData();
formData.append('images[]', file);
i.state.imageLoading = true;
i.setState(i.state);
fetch(imageUploadUrl, {
method: 'POST',
body: formData,
})
.then(res => res.json())
.then(res => {
console.log('pictrs upload:');
console.log(res);
if (res.msg == 'ok') {
let hash = res.files[0].file;
let url = `${window.location.origin}/pictrs/image/${hash}`;
let deleteToken = res.files[0].delete_token;
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
let imageMarkdown = `![](${url})`;
let content = i.state.commentForm.content;
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
i.state.commentForm.content = content;
i.state.imageLoading = false;
i.setState(i.state);
let textarea: any = document.getElementById(i.id);
autosize.update(textarea);
pictrsDeleteToast(
i18n.t('click_to_delete_picture'),
i18n.t('picture_deleted'),
deleteUrl
);
} else {
i.state.imageLoading = false;
i.setState(i.state);
toast(JSON.stringify(res), 'danger');
}
})
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
toast(error, 'danger');
});
} }
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
@ -395,12 +135,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// Only do the showing and hiding if logged in // Only do the showing and hiding if logged in
if (UserService.Instance.user) { if (UserService.Instance.user) {
if (res.op == UserOperation.CreateComment) { if (
res.op == UserOperation.CreateComment ||
res.op == UserOperation.EditComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
} else if (res.op == UserOperation.EditComment) { // This only finishes this form, if the randomly generated form_id matches the one received
let data = res.data as CommentResponse; if (this.state.commentForm.form_id == data.form_id) {
this.handleFinished(res.op, data); this.setState({ finished: true });
}
} }
} }
} }

View file

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

View file

@ -13,7 +13,7 @@ import {
GetSiteResponse, GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils'; import { wsJsonToRes, toast, getPageFromProps } from '../utils';
import { CommunityLink } from './community-link'; import { CommunityLink } from './community-link';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -27,12 +27,16 @@ interface CommunitiesState {
loading: boolean; loading: boolean;
} }
interface CommunitiesProps {
page: number;
}
export class Communities extends Component<any, CommunitiesState> { export class Communities extends Component<any, CommunitiesState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommunitiesState = { private emptyState: CommunitiesState = {
communities: [], communities: [],
loading: true, loading: true,
page: this.getPageFromProps(this.props), page: getPageFromProps(this.props),
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -50,19 +54,19 @@ export class Communities extends Component<any, CommunitiesState> {
WebSocketService.Instance.getSite(); WebSocketService.Instance.getSite();
} }
getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
}
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): CommunitiesProps {
componentWillReceiveProps(nextProps: any) { return {
if (nextProps.history.action == 'POP') { page: getPageFromProps(props),
this.state = this.emptyState; };
this.state.page = this.getPageFromProps(nextProps); }
componentDidUpdate(_: any, lastState: CommunitiesState) {
if (lastState.page !== this.state.page) {
this.setState({ loading: true });
this.refetch(); this.refetch();
} }
} }
@ -172,22 +176,17 @@ export class Communities extends Component<any, CommunitiesState> {
); );
} }
updateUrl() { updateUrl(paramUpdates: CommunitiesProps) {
this.props.history.push(`/communities/page/${this.state.page}`); const page = paramUpdates.page || this.state.page;
this.props.history.push(`/communities/page/${page}`);
} }
nextPage(i: Communities) { nextPage(i: Communities) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.refetch();
} }
prevPage(i: Communities) { prevPage(i: Communities) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.refetch();
} }
handleUnsubscribe(communityId: number) { handleUnsubscribe(communityId: number) {

View file

@ -11,18 +11,11 @@ import {
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
wsJsonToRes,
capitalizeFirstLetter,
toast,
randomStr,
setupTribute,
} from '../utils';
import Tribute from 'tributejs/src/Tribute.js';
import autosize from 'autosize';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { Community } from '../interfaces'; import { Community } from '../interfaces';
import { MarkdownTextArea } from './markdown-textarea';
interface CommunityFormProps { interface CommunityFormProps {
community?: Community; // If a community is given, that means this is an edit community?: Community; // If a community is given, that means this is an edit
@ -43,7 +36,6 @@ export class CommunityForm extends Component<
CommunityFormState CommunityFormState
> { > {
private id = `community-form-${randomStr()}`; private id = `community-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription; private subscription: Subscription;
private emptyState: CommunityFormState = { private emptyState: CommunityFormState = {
@ -60,9 +52,12 @@ export class CommunityForm extends Component<
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.tribute = setupTribute();
this.state = this.emptyState; this.state = this.emptyState;
this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
this
);
if (this.props.community) { if (this.props.community) {
this.state.communityForm = { this.state.communityForm = {
name: this.props.community.name, name: this.props.community.name,
@ -86,17 +81,6 @@ export class CommunityForm extends Component<
WebSocketService.Instance.listCategories(); WebSocketService.Instance.listCategories();
} }
componentDidMount() {
var textarea: any = document.getElementById(this.id);
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.communityForm.description = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
}
componentDidUpdate() { componentDidUpdate() {
if ( if (
!this.state.loading && !this.state.loading &&
@ -128,26 +112,27 @@ export class CommunityForm extends Component<
message={i18n.t('block_leaving')} message={i18n.t('block_leaving')}
/> />
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}> <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
<div class="form-group row"> {!this.props.community && (
<label class="col-12 col-form-label" htmlFor="community-name"> <div class="form-group row">
{i18n.t('name')} <label class="col-12 col-form-label" htmlFor="community-name">
</label> {i18n.t('name')}
<div class="col-12"> </label>
<input <div class="col-12">
type="text" <input
id="community-name" type="text"
class="form-control" id="community-name"
value={this.state.communityForm.name} class="form-control"
onInput={linkEvent(this, this.handleCommunityNameChange)} value={this.state.communityForm.name}
required onInput={linkEvent(this, this.handleCommunityNameChange)}
minLength={3} required
maxLength={20} minLength={3}
pattern="[a-z0-9_]+" maxLength={20}
title={i18n.t('community_reqs')} pattern="[a-z0-9_]+"
/> title={i18n.t('community_reqs')}
/>
</div>
</div> </div>
</div> )}
<div class="form-group row"> <div class="form-group row">
<label class="col-12 col-form-label" htmlFor="community-title"> <label class="col-12 col-form-label" htmlFor="community-title">
{i18n.t('title')} {i18n.t('title')}
@ -170,13 +155,9 @@ export class CommunityForm extends Component<
{i18n.t('sidebar')} {i18n.t('sidebar')}
</label> </label>
<div class="col-12"> <div class="col-12">
<textarea <MarkdownTextArea
id={this.id} initialContent={this.state.communityForm.description}
value={this.state.communityForm.description} onContentChange={this.handleCommunityDescriptionChange}
onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
class="form-control"
rows={3}
maxLength={10000}
/> />
</div> </div>
</div> </div>
@ -270,9 +251,9 @@ export class CommunityForm extends Component<
i.setState(i.state); i.setState(i.state);
} }
handleCommunityDescriptionChange(i: CommunityForm, event: any) { handleCommunityDescriptionChange(val: string) {
i.state.communityForm.description = event.target.value; this.state.communityForm.description = val;
i.setState(i.state); this.setState(this.state);
} }
handleCommunityCategoryChange(i: CommunityForm, event: any) { handleCommunityCategoryChange(i: CommunityForm, event: any) {

View file

@ -65,6 +65,18 @@ interface State {
site: Site; site: Site;
} }
interface CommunityProps {
dataType: DataType;
sort: SortType;
page: number;
}
interface UrlParams {
dataType?: string;
sort?: string;
page?: number;
}
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: State = { private emptyState: State = {
@ -143,16 +155,21 @@ export class Community extends Component<any, State> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): CommunityProps {
componentWillReceiveProps(nextProps: any) { return {
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: State) {
if ( if (
nextProps.history.action == 'POP' || lastState.dataType !== this.state.dataType ||
nextProps.history.action == 'PUSH' lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.dataType = getDataTypeFromProps(nextProps); this.setState({ loading: true });
this.state.sort = getSortTypeFromProps(nextProps);
this.state.page = getPageFromProps(nextProps);
this.setState(this.state);
this.fetchData(); this.fetchData();
} }
} }
@ -273,46 +290,33 @@ export class Community extends Component<any, State> {
} }
nextPage(i: Community) { nextPage(i: Community) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
prevPage(i: Community) { prevPage(i: Community) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleDataTypeChange(val: DataType) { handleDataTypeChange(val: DataType) {
this.state.dataType = val; this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let dataTypeStr = DataType[this.state.dataType].toLowerCase(); const dataTypeStr =
let sortStr = SortType[this.state.sort].toLowerCase(); paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}` `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
); );
} }
@ -351,12 +355,15 @@ export class Community extends Component<any, State> {
let data = res.data as GetCommunityResponse; let data = res.data as GetCommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.online = data.online; this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`; document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state); this.setState(this.state);
this.fetchData(); this.fetchData();
} else if (res.op == UserOperation.EditCommunity) { } else if (
res.op == UserOperation.EditCommunity ||
res.op == UserOperation.DeleteCommunity ||
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse; let data = res.data as CommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.setState(this.state); this.setState(this.state);
@ -372,7 +379,13 @@ export class Community extends Component<any, State> {
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (res.op == UserOperation.EditPost) { } else if (
res.op == UserOperation.EditPost ||
res.op == UserOperation.DeletePost ||
res.op == UserOperation.RemovePost ||
res.op == UserOperation.LockPost ||
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse; let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts); editPostFindRes(data, this.state.posts);
this.setState(this.state); this.setState(this.state);
@ -401,7 +414,11 @@ export class Community extends Component<any, State> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);
@ -424,6 +441,7 @@ export class Community extends Component<any, State> {
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse; let data = res.data as GetSiteResponse;
this.state.site = data.site; this.state.site = data.site;
this.state.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -70,7 +70,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
console.log(msg); console.log(msg);
let res = wsJsonToRes(msg); let res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), 'danger'); // Toast errors are already handled by community-form
return; return;
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse; let data = res.data as GetSiteResponse;

View file

@ -25,6 +25,12 @@ export class DataTypeSelect extends Component<
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): DataTypeSelectProps {
return {
type_: props.type_,
};
}
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
@ -42,8 +48,9 @@ export class DataTypeSelect extends Component<
{i18n.t('posts')} {i18n.t('posts')}
</label> </label>
<label <label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ == className={`pointer btn btn-sm btn-secondary ${
DataType.Comment && 'active'}`} this.state.type_ == DataType.Comment && 'active'
}`}
> >
<input <input
type="radio" type="radio"
@ -58,8 +65,6 @@ export class DataTypeSelect extends Component<
} }
handleTypeChange(i: DataTypeSelect, event: any) { handleTypeChange(i: DataTypeSelect, event: any) {
i.state.type_ = Number(event.target.value); i.props.onChange(Number(event.target.value));
i.setState(i.state);
i.props.onChange(i.state.type_);
} }
} }

View file

@ -1,12 +1,41 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { repoUrl } from '../utils';
import { version } from '../version';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { WebSocketService } from '../services';
import { repoUrl, wsJsonToRes } from '../utils';
import {
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces';
export class Footer extends Component<any, any> { interface FooterState {
version: string;
}
export class Footer extends Component<any, FooterState> {
private wsSub: Subscription;
emptyState: FooterState = {
version: null,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.state = this.emptyState;
this.wsSub = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
}
componentWillUnmount() {
this.wsSub.unsubscribe();
} }
render() { render() {
@ -15,7 +44,7 @@ export class Footer extends Component<any, any> {
<div className="navbar-collapse"> <div className="navbar-collapse">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<span class="navbar-text">{version}</span> <span class="navbar-text">{this.state.version}</span>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-link" to="/modlog"> <Link class="nav-link" to="/modlog">
@ -42,4 +71,12 @@ export class Footer extends Component<any, any> {
</nav> </nav>
); );
} }
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.setState({ version: data.version });
}
}
} }

View file

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

View file

@ -26,6 +26,12 @@ export class ListingTypeSelect extends Component<
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
return {
type_: props.type_,
};
}
render() { render() {
return ( return (
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
@ -45,8 +51,9 @@ export class ListingTypeSelect extends Component<
{i18n.t('subscribed')} {i18n.t('subscribed')}
</label> </label>
<label <label
className={`pointer btn btn-sm btn-secondary ${this.state.type_ == className={`pointer btn btn-sm btn-secondary ${
ListingType.All && 'active'}`} this.state.type_ == ListingType.All && 'active'
}`}
> >
<input <input
type="radio" type="radio"
@ -61,8 +68,6 @@ export class ListingTypeSelect extends Component<
} }
handleTypeChange(i: ListingTypeSelect, event: any) { handleTypeChange(i: ListingTypeSelect, event: any) {
i.state.type_ = Number(event.target.value); i.props.onChange(Number(event.target.value));
i.setState(i.state);
i.props.onChange(i.state.type_);
} }
} }

View file

@ -120,14 +120,15 @@ export class Login extends Component<any, State> {
class="form-control" class="form-control"
required required
/> />
<button {validEmail(this.state.loginForm.username_or_email) && (
type="button" <button
disabled={!validEmail(this.state.loginForm.username_or_email)} type="button"
onClick={linkEvent(this, this.handlePasswordReset)} onClick={linkEvent(this, this.handlePasswordReset)}
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold" className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
> >
{i18n.t('forgot_password')} {i18n.t('forgot_password')}
</button> </button>
)}
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -186,6 +187,14 @@ export class Login extends Component<any, State> {
onInput={linkEvent(this, this.handleRegisterEmailChange)} onInput={linkEvent(this, this.handleRegisterEmailChange)}
minLength={3} minLength={3}
/> />
{!validEmail(this.state.registerForm.email) && (
<div class="mt-2 mb-0 alert alert-light" role="alert">
<svg class="icon icon-inline mr-2">
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
{i18n.t('no_password_reset')}
</div>
)}
</div> </div>
</div> </div>

View file

@ -70,6 +70,20 @@ interface MainState {
page: number; page: number;
} }
interface MainProps {
listingType: ListingType;
dataType: DataType;
sort: SortType;
page: number;
}
interface UrlParams {
listingType?: string;
dataType?: string;
sort?: string;
page?: number;
}
export class Main extends Component<any, MainState> { export class Main extends Component<any, MainState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: MainState = { private emptyState: MainState = {
@ -93,6 +107,7 @@ export class Main extends Component<any, MainState> {
admins: [], admins: [],
banned: [], banned: [],
online: null, online: null,
version: null,
}, },
showEditSite: false, showEditSite: false,
loading: true, loading: true,
@ -141,17 +156,23 @@ export class Main extends Component<any, MainState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): MainProps {
componentWillReceiveProps(nextProps: any) { return {
listingType: getListingTypeFromProps(props),
dataType: getDataTypeFromProps(props),
sort: getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: MainState) {
if ( if (
nextProps.history.action == 'POP' || lastState.listingType !== this.state.listingType ||
nextProps.history.action == 'PUSH' lastState.dataType !== this.state.dataType ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.listingType = getListingTypeFromProps(nextProps); this.setState({ loading: true });
this.state.dataType = getDataTypeFromProps(nextProps);
this.state.sort = getSortTypeFromProps(nextProps);
this.state.page = getPageFromProps(nextProps);
this.setState(this.state);
this.fetchData(); this.fetchData();
} }
} }
@ -257,12 +278,17 @@ export class Main extends Component<any, MainState> {
); );
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let listingTypeStr = ListingType[this.state.listingType].toLowerCase(); const listingTypeStr =
let dataTypeStr = DataType[this.state.dataType].toLowerCase(); paramUpdates.listingType ||
let sortStr = SortType[this.state.sort].toLowerCase(); ListingType[this.state.listingType].toLowerCase();
const dataTypeStr =
paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}` `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
); );
} }
@ -529,50 +555,27 @@ export class Main extends Component<any, MainState> {
} }
nextPage(i: Main) { nextPage(i: Main) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
prevPage(i: Main) { prevPage(i: Main) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
i.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleListingTypeChange(val: ListingType) { handleListingTypeChange(val: ListingType) {
this.state.listingType = val; this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
handleDataTypeChange(val: DataType) { handleDataTypeChange(val: DataType) {
this.state.dataType = val; this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.state.loading = true;
this.setState(this.state);
this.updateUrl();
this.fetchData();
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@ -699,7 +702,11 @@ export class Main extends Component<any, MainState> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.EditComment) { } else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments); editCommentRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);

538
ui/src/components/markdown-textarea.tsx vendored Normal file
View file

@ -0,0 +1,538 @@
import { Component, linkEvent } from 'inferno';
import { Prompt } from 'inferno-router';
import {
mdToHtml,
randomStr,
markdownHelpUrl,
toast,
setupTribute,
pictrsDeleteToast,
setupTippy,
} from '../utils';
import { UserService } from '../services';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import { i18n } from '../i18next';
interface MarkdownTextAreaProps {
initialContent: string;
finished?: boolean;
buttonTitle?: string;
replyType?: boolean;
focus?: boolean;
disabled?: boolean;
onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any;
onReplyCancel?(): any;
}
interface MarkdownTextAreaState {
content: string;
previewMode: boolean;
loading: boolean;
imageLoading: boolean;
}
export class MarkdownTextArea extends Component<
MarkdownTextAreaProps,
MarkdownTextAreaState
> {
private id = `comment-textarea-${randomStr()}`;
private formId = `comment-form-${randomStr()}`;
private tribute: Tribute;
private emptyState: MarkdownTextAreaState = {
content: this.props.initialContent,
previewMode: false,
loading: false,
imageLoading: false,
};
constructor(props: any, context: any) {
super(props, context);
this.tribute = setupTribute();
this.state = this.emptyState;
}
componentDidMount() {
let textarea: any = document.getElementById(this.id);
if (textarea) {
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
this.quoteInsert();
if (this.props.focus) {
textarea.focus();
}
// TODO this is slow for some reason
setupTippy();
}
}
componentDidUpdate() {
if (this.state.content) {
window.onbeforeunload = () => true;
} else {
window.onbeforeunload = undefined;
}
}
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
if (nextProps.finished) {
this.state.previewMode = false;
this.state.loading = false;
this.state.content = '';
this.setState(this.state);
if (this.props.replyType) {
this.props.onReplyCancel();
}
let textarea: any = document.getElementById(this.id);
let form: any = document.getElementById(this.formId);
form.reset();
setTimeout(() => autosize.update(textarea), 10);
this.setState(this.state);
}
}
componentWillUnmount() {
window.onbeforeunload = null;
}
render() {
return (
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
<Prompt when={this.state.content} message={i18n.t('block_leaving')} />
<div class="form-group row">
<div className={`col-sm-12`}>
<textarea
id={this.id}
className={`form-control ${this.state.previewMode && 'd-none'}`}
value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
required
disabled={this.props.disabled}
rows={2}
maxLength={10000}
/>
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
/>
)}
</div>
</div>
<div class="row">
<div class="col-sm-12 d-flex flex-wrap">
{this.props.buttonTitle && (
<button
type="submit"
class="btn btn-sm btn-secondary mr-2"
disabled={this.props.disabled || this.state.loading}
>
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<span>{this.props.buttonTitle}</span>
)}
</button>
)}
{this.props.replyType && (
<button
type="button"
class="btn btn-sm btn-secondary mr-2"
onClick={linkEvent(this, this.handleReplyCancel)}
>
{i18n.t('cancel')}
</button>
)}
{this.state.content && (
<button
className={`btn btn-sm btn-secondary mr-2 ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{/* A flex expander */}
<div class="flex-grow-1"></div>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('bold')}
onClick={linkEvent(this, this.handleInsertBold)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-bold"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('italic')}
onClick={linkEvent(this, this.handleInsertItalic)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-italic"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('link')}
onClick={linkEvent(this, this.handleInsertLink)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-link"></use>
</svg>
</button>
<form class="btn btn-sm text-muted font-weight-bold">
<label
htmlFor={`file-upload-${this.id}`}
className={`mb-0 ${UserService.Instance.user && 'pointer'}`}
data-tippy-content={i18n.t('upload_image')}
>
{this.state.imageLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
<svg class="icon icon-inline">
<use xlinkHref="#icon-image"></use>
</svg>
)}
</label>
<input
id={`file-upload-${this.id}`}
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('header')}
onClick={linkEvent(this, this.handleInsertHeader)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-header"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('strikethrough')}
onClick={linkEvent(this, this.handleInsertStrikethrough)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-strikethrough"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('quote')}
onClick={linkEvent(this, this.handleInsertQuote)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-format_quote"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('list')}
onClick={linkEvent(this, this.handleInsertList)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-list"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('code')}
onClick={linkEvent(this, this.handleInsertCode)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-code"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('subscript')}
onClick={linkEvent(this, this.handleInsertSubscript)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-subscript"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('superscript')}
onClick={linkEvent(this, this.handleInsertSuperscript)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-superscript"></use>
</svg>
</button>
<button
class="btn btn-sm text-muted"
data-tippy-content={i18n.t('spoiler')}
onClick={linkEvent(this, this.handleInsertSpoiler)}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
</button>
<a
href={markdownHelpUrl}
target="_blank"
class="btn btn-sm text-muted font-weight-bold"
title={i18n.t('formatting_help')}
rel="noopener"
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
</div>
</div>
</form>
);
}
handleImageUploadPaste(i: MarkdownTextArea, event: any) {
let image = event.clipboardData.files[0];
if (image) {
i.handleImageUpload(i, image);
}
}
handleImageUpload(i: MarkdownTextArea, event: any) {
let file: any;
if (event.target) {
event.preventDefault();
file = event.target.files[0];
} else {
file = event;
}
const imageUploadUrl = `/pictrs/image`;
const formData = new FormData();
formData.append('images[]', file);
i.state.imageLoading = true;
i.setState(i.state);
fetch(imageUploadUrl, {
method: 'POST',
body: formData,
})
.then(res => res.json())
.then(res => {
console.log('pictrs upload:');
console.log(res);
if (res.msg == 'ok') {
let hash = res.files[0].file;
let url = `${window.location.origin}/pictrs/image/${hash}`;
let deleteToken = res.files[0].delete_token;
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
let imageMarkdown = `![](${url})`;
let content = i.state.content;
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
i.state.content = content;
i.state.imageLoading = false;
i.setState(i.state);
let textarea: any = document.getElementById(i.id);
autosize.update(textarea);
pictrsDeleteToast(
i18n.t('click_to_delete_picture'),
i18n.t('picture_deleted'),
deleteUrl
);
} else {
i.state.imageLoading = false;
i.setState(i.state);
toast(JSON.stringify(res), 'danger');
}
})
.catch(error => {
i.state.imageLoading = false;
i.setState(i.state);
toast(error, 'danger');
});
}
handleContentChange(i: MarkdownTextArea, event: any) {
i.state.content = event.target.value;
i.setState(i.state);
if (i.props.onContentChange) {
i.props.onContentChange(i.state.content);
}
}
handlePreviewToggle(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.state.previewMode = !i.state.previewMode;
i.setState(i.state);
}
handleSubmit(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.state.loading = true;
i.setState(i.state);
let msg = { val: i.state.content, formId: i.formId };
i.props.onSubmit(msg);
}
handleReplyCancel(i: MarkdownTextArea) {
i.props.onReplyCancel();
}
handleInsertLink(i: MarkdownTextArea, event: any) {
event.preventDefault();
if (!i.state.content) {
i.state.content = '';
}
let textarea: any = document.getElementById(i.id);
let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd;
if (start !== end) {
let selectedText = i.state.content.substring(start, end);
i.state.content = `${i.state.content.substring(
0,
start
)} [${selectedText}]() ${i.state.content.substring(end)}`;
textarea.focus();
setTimeout(() => (textarea.selectionEnd = end + 4), 10);
} else {
i.state.content += '[]()';
textarea.focus();
setTimeout(() => (textarea.selectionEnd -= 1), 10);
}
i.setState(i.state);
}
simpleSurround(chars: string) {
this.simpleSurroundBeforeAfter(chars, chars);
}
simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
if (!this.state.content) {
this.state.content = '';
}
let textarea: any = document.getElementById(this.id);
let start: number = textarea.selectionStart;
let end: number = textarea.selectionEnd;
if (start !== end) {
let selectedText = this.state.content.substring(start, end);
this.state.content = `${this.state.content.substring(
0,
start - 1
)} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
end + 1
)}`;
} else {
this.state.content += `${beforeChars}___${afterChars}`;
}
this.setState(this.state);
setTimeout(() => {
autosize.update(textarea);
}, 10);
}
handleInsertBold(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('**');
}
handleInsertItalic(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('*');
}
handleInsertCode(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('`');
}
handleInsertStrikethrough(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('~~');
}
handleInsertList(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleInsert('-');
}
handleInsertQuote(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleInsert('>');
}
handleInsertHeader(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleInsert('#');
}
handleInsertSubscript(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('~');
}
handleInsertSuperscript(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.simpleSurround('^');
}
simpleInsert(chars: string) {
if (!this.state.content) {
this.state.content = `${chars} `;
} else {
this.state.content += `\n${chars} `;
}
let textarea: any = document.getElementById(this.id);
textarea.focus();
setTimeout(() => {
autosize.update(textarea);
}, 10);
this.setState(this.state);
}
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
event.preventDefault();
let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
let afterChars = '\n:::\n';
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
}
quoteInsert() {
let textarea: any = document.getElementById(this.id);
let selectedText = window.getSelection().toString();
if (selectedText) {
let quotedText =
selectedText
.split('\n')
.map(t => `> ${t}`)
.join('\n') + '\n\n';
this.state.content = quotedText;
this.setState(this.state);
// Not sure why this needs a delay
setTimeout(() => autosize.update(textarea), 10);
}
}
}

View file

@ -30,7 +30,6 @@ import {
messageToastify, messageToastify,
md, md,
} from '../utils'; } from '../utils';
import { version } from '../version';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface NavbarState { interface NavbarState {
@ -41,6 +40,7 @@ interface NavbarState {
messages: Array<PrivateMessage>; messages: Array<PrivateMessage>;
unreadCount: number; unreadCount: number;
siteName: string; siteName: string;
version: string;
admins: Array<UserView>; admins: Array<UserView>;
searchParam: string; searchParam: string;
toggleSearch: boolean; toggleSearch: boolean;
@ -58,6 +58,7 @@ export class Navbar extends Component<any, NavbarState> {
messages: [], messages: [],
expanded: false, expanded: false,
siteName: undefined, siteName: undefined,
version: undefined,
admins: [], admins: [],
searchParam: '', searchParam: '',
toggleSearch: false, toggleSearch: false,
@ -150,7 +151,7 @@ export class Navbar extends Component<any, NavbarState> {
navbar() { navbar() {
return ( return (
<nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3"> <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
<Link title={version} class="navbar-brand" to="/"> <Link title={this.state.version} class="navbar-brand" to="/">
{this.state.siteName} {this.state.siteName}
</Link> </Link>
{this.state.isLoggedIn && ( {this.state.isLoggedIn && (
@ -395,6 +396,7 @@ export class Navbar extends Component<any, NavbarState> {
if (data.site && !this.state.siteName) { if (data.site && !this.state.siteName) {
this.state.siteName = data.site.name; this.state.siteName = data.site.name;
this.state.version = data.version;
this.state.admins = data.admins; this.state.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }

View file

@ -1,6 +1,7 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Prompt } from 'inferno-router'; import { Prompt } from 'inferno-router';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { MarkdownTextArea } from './markdown-textarea';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {
@ -24,22 +25,16 @@ import {
getPageTitle, getPageTitle,
validURL, validURL,
capitalizeFirstLetter, capitalizeFirstLetter,
markdownHelpUrl,
archiveUrl, archiveUrl,
mdToHtml,
debounce, debounce,
isImage, isImage,
toast, toast,
randomStr, randomStr,
setupTribute,
setupTippy, setupTippy,
hostname, hostname,
pictrsDeleteToast, pictrsDeleteToast,
validTitle, validTitle,
} from '../utils'; } from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import emojiShortName from 'emoji-short-name';
import Choices from 'choices.js'; import Choices from 'choices.js';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -68,7 +63,6 @@ interface PostFormState {
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
private id = `post-form-${randomStr()}`; private id = `post-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription; private subscription: Subscription;
private choices: Choices; private choices: Choices;
private emptyState: PostFormState = { private emptyState: PostFormState = {
@ -77,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
nsfw: false, nsfw: false,
auth: null, auth: null,
community_id: null, community_id: null,
creator_id: UserService.Instance.user
? UserService.Instance.user.id
: null,
}, },
communities: [], communities: [],
loading: false, loading: false,
@ -94,8 +85,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
super(props, context); super(props, context);
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this); this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this); this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
this.tribute = setupTribute();
this.state = this.emptyState; this.state = this.emptyState;
@ -106,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: this.props.post.name, name: this.props.post.name,
community_id: this.props.post.community_id, community_id: this.props.post.community_id,
edit_id: this.props.post.id, edit_id: this.props.post.id,
creator_id: this.props.post.creator_id,
url: this.props.post.url, url: this.props.post.url,
nsfw: this.props.post.nsfw, nsfw: this.props.post.nsfw,
auth: null, auth: null,
@ -140,14 +129,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
componentDidMount() { componentDidMount() {
var textarea: any = document.getElementById(this.id);
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.postForm.body = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
setupTippy(); setupTippy();
} }
@ -166,7 +147,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.choices && this.choices.destroy(); /* this.choices && this.choices.destroy(); */
window.onbeforeunload = null; window.onbeforeunload = null;
} }
@ -305,41 +286,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
{i18n.t('body')} {i18n.t('body')}
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea <MarkdownTextArea
id={this.id} initialContent={this.state.postForm.body}
value={this.state.postForm.body} onContentChange={this.handlePostBodyChange}
onInput={linkEvent(this, this.handlePostBodyChange)}
className={`form-control ${this.state.previewMode && 'd-none'}`}
rows={4}
maxLength={10000}
/> />
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
/>
)}
{this.state.postForm.body && (
<button
className={`mt-1 mr-2 btn btn-sm btn-secondary ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
<a
href={markdownHelpUrl}
target="_blank"
rel="noopener"
class="d-inline-block float-right text-muted font-weight-bold"
title={i18n.t('formatting_help')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
</div> </div>
</div> </div>
{!this.props.post && ( {!this.props.post && (
@ -499,9 +449,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state); this.setState(this.state);
} }
handlePostBodyChange(i: PostForm, event: any) { handlePostBodyChange(val: string) {
i.state.postForm.body = event.target.value; this.state.postForm.body = val;
i.setState(i.state); this.setState(this.state);
} }
handlePostCommunityChange(i: PostForm, event: any) { handlePostCommunityChange(i: PostForm, event: any) {

View file

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

View file

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

View file

@ -18,17 +18,12 @@ import {
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { import {
capitalizeFirstLetter, capitalizeFirstLetter,
markdownHelpUrl,
mdToHtml,
wsJsonToRes, wsJsonToRes,
toast, toast,
randomStr,
setupTribute,
setupTippy, setupTippy,
} from '../utils'; } from '../utils';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import Tribute from 'tributejs/src/Tribute.js'; import { MarkdownTextArea } from './markdown-textarea';
import autosize from 'autosize';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -52,8 +47,6 @@ export class PrivateMessageForm extends Component<
PrivateMessageFormProps, PrivateMessageFormProps,
PrivateMessageFormState PrivateMessageFormState
> { > {
private id = `message-form-${randomStr()}`;
private tribute: Tribute;
private subscription: Subscription; private subscription: Subscription;
private emptyState: PrivateMessageFormState = { private emptyState: PrivateMessageFormState = {
privateMessageForm: { privateMessageForm: {
@ -69,9 +62,10 @@ export class PrivateMessageForm extends Component<
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.tribute = setupTribute();
this.state = this.emptyState; this.state = this.emptyState;
this.handleContentChange = this.handleContentChange.bind(this);
if (this.props.privateMessage) { if (this.props.privateMessage) {
this.state.privateMessageForm = { this.state.privateMessageForm = {
content: this.props.privateMessage.content, content: this.props.privateMessage.content,
@ -99,14 +93,6 @@ export class PrivateMessageForm extends Component<
} }
componentDidMount() { componentDidMount() {
var textarea: any = document.getElementById(this.id);
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.privateMessageForm.content = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
setupTippy(); setupTippy();
} }
@ -153,24 +139,23 @@ export class PrivateMessageForm extends Component<
</div> </div>
)} )}
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">{i18n.t('message')}</label> <label class="col-sm-2 col-form-label">
{i18n.t('message')}
<span
onClick={linkEvent(this, this.handleShowDisclaimer)}
class="ml-2 pointer text-danger"
data-tippy-content={i18n.t('disclaimer')}
>
<svg class={`icon icon-inline`}>
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
</span>
</label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea <MarkdownTextArea
id={this.id} initialContent={this.state.privateMessageForm.content}
value={this.state.privateMessageForm.content} onContentChange={this.handleContentChange}
onInput={linkEvent(this, this.handleContentChange)}
className={`form-control ${this.state.previewMode && 'd-none'}`}
rows={4}
maxLength={10000}
/> />
{this.state.previewMode && (
<div
className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(
this.state.privateMessageForm.content
)}
/>
)}
</div> </div>
</div> </div>
@ -184,7 +169,7 @@ export class PrivateMessageForm extends Component<
class="alert-link" class="alert-link"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
href="https://about.riot.im/" href="https://element.io/get-started"
> >
# #
</a> </a>
@ -210,16 +195,6 @@ export class PrivateMessageForm extends Component<
capitalizeFirstLetter(i18n.t('send_message')) capitalizeFirstLetter(i18n.t('send_message'))
)} )}
</button> </button>
{this.state.privateMessageForm.content && (
<button
className={`btn btn-secondary mr-2 ${
this.state.previewMode && 'active'
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
</button>
)}
{this.props.privateMessage && ( {this.props.privateMessage && (
<button <button
type="button" type="button"
@ -230,30 +205,7 @@ export class PrivateMessageForm extends Component<
</button> </button>
)} )}
<ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold"> <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
<li class="list-inline-item"> <li class="list-inline-item"></li>
<span
onClick={linkEvent(this, this.handleShowDisclaimer)}
class="pointer"
data-tippy-content={i18n.t('disclaimer')}
>
<svg class={`icon icon-inline`}>
<use xlinkHref="#icon-alert-triangle"></use>
</svg>
</span>
</li>
<li class="list-inline-item">
<a
href={markdownHelpUrl}
target="_blank"
rel="noopener"
class="text-muted"
title={i18n.t('formatting_help')}
>
<svg class="icon icon-inline">
<use xlinkHref="#icon-help-circle"></use>
</svg>
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -284,9 +236,9 @@ export class PrivateMessageForm extends Component<
i.setState(i.state); i.setState(i.state);
} }
handleContentChange(i: PrivateMessageForm, event: any) { handleContentChange(val: string) {
i.state.privateMessageForm.content = event.target.value; this.state.privateMessageForm.content = val;
i.setState(i.state); this.setState(this.state);
} }
handleCancel(i: PrivateMessageForm) { handleCancel(i: PrivateMessageForm) {
@ -311,7 +263,11 @@ export class PrivateMessageForm extends Component<
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
return; return;
} else if (res.op == UserOperation.EditPrivateMessage) { } else if (
res.op == UserOperation.EditPrivateMessage ||
res.op == UserOperation.DeletePrivateMessage ||
res.op == UserOperation.MarkPrivateMessageAsRead
) {
let data = res.data as PrivateMessageResponse; let data = res.data as PrivateMessageResponse;
this.state.loading = false; this.state.loading = false;
this.props.onEdit(data.message); this.props.onEdit(data.message);

View file

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

View file

@ -28,6 +28,7 @@ import {
createCommentLikeRes, createCommentLikeRes,
createPostLikeFindRes, createPostLikeFindRes,
commentsToFlatNodes, commentsToFlatNodes,
getPageFromProps,
} from '../utils'; } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
@ -44,15 +45,31 @@ interface SearchState {
searchResponse: SearchResponse; searchResponse: SearchResponse;
loading: boolean; loading: boolean;
site: Site; site: Site;
searchText: string;
}
interface SearchProps {
q: string;
type_: SearchType;
sort: SortType;
page: number;
}
interface UrlParams {
q?: string;
type_?: string;
sort?: string;
page?: number;
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
private subscription: Subscription; private subscription: Subscription;
private emptyState: SearchState = { private emptyState: SearchState = {
q: this.getSearchQueryFromProps(this.props), q: Search.getSearchQueryFromProps(this.props),
type_: this.getSearchTypeFromProps(this.props), type_: Search.getSearchTypeFromProps(this.props),
sort: this.getSortTypeFromProps(this.props), sort: Search.getSortTypeFromProps(this.props),
page: this.getPageFromProps(this.props), page: getPageFromProps(this.props),
searchText: Search.getSearchQueryFromProps(this.props),
searchResponse: { searchResponse: {
type_: null, type_: null,
posts: [], posts: [],
@ -77,26 +94,22 @@ export class Search extends Component<any, SearchState> {
}, },
}; };
getSearchQueryFromProps(props: any): string { static getSearchQueryFromProps(props: any): string {
return props.match.params.q ? props.match.params.q : ''; return props.match.params.q ? props.match.params.q : '';
} }
getSearchTypeFromProps(props: any): SearchType { static getSearchTypeFromProps(props: any): SearchType {
return props.match.params.type return props.match.params.type
? routeSearchTypeToEnum(props.match.params.type) ? routeSearchTypeToEnum(props.match.params.type)
: SearchType.All; : SearchType.All;
} }
getSortTypeFromProps(props: any): SortType { static getSortTypeFromProps(props: any): SortType {
return props.match.params.sort return props.match.params.sort
? routeSortTypeToEnum(props.match.params.sort) ? routeSortTypeToEnum(props.match.params.sort)
: SortType.TopAll; : SortType.TopAll;
} }
getPageFromProps(props: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1;
}
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
@ -122,17 +135,23 @@ export class Search extends Component<any, SearchState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): SearchProps {
componentWillReceiveProps(nextProps: any) { return {
q: Search.getSearchQueryFromProps(props),
type_: Search.getSearchTypeFromProps(props),
sort: Search.getSortTypeFromProps(props),
page: getPageFromProps(props),
};
}
componentDidUpdate(_: any, lastState: SearchState) {
if ( if (
nextProps.history.action == 'POP' || lastState.q !== this.state.q ||
nextProps.history.action == 'PUSH' lastState.type_ !== this.state.type_ ||
lastState.sort !== this.state.sort ||
lastState.page !== this.state.page
) { ) {
this.state.q = this.getSearchQueryFromProps(nextProps); this.setState({ loading: true, searchText: this.state.q });
this.state.type_ = this.getSearchTypeFromProps(nextProps);
this.state.sort = this.getSortTypeFromProps(nextProps);
this.state.page = this.getPageFromProps(nextProps);
this.setState(this.state);
this.search(); this.search();
} }
} }
@ -163,7 +182,7 @@ export class Search extends Component<any, SearchState> {
<input <input
type="text" type="text"
class="form-control mr-2" class="form-control mr-2"
value={this.state.q} value={this.state.searchText}
placeholder={`${i18n.t('search')}...`} placeholder={`${i18n.t('search')}...`}
onInput={linkEvent(this, this.handleQChange)} onInput={linkEvent(this, this.handleQChange)}
required required
@ -413,17 +432,11 @@ export class Search extends Component<any, SearchState> {
} }
nextPage(i: Search) { nextPage(i: Search) {
i.state.page++; i.updateUrl({ page: i.state.page + 1 });
i.setState(i.state);
i.updateUrl();
i.search();
} }
prevPage(i: Search) { prevPage(i: Search) {
i.state.page--; i.updateUrl({ page: i.state.page - 1 });
i.setState(i.state);
i.updateUrl();
i.search();
} }
search() { search() {
@ -441,37 +454,39 @@ export class Search extends Component<any, SearchState> {
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.setState(this.state);
this.updateUrl();
} }
handleTypeChange(i: Search, event: any) { handleTypeChange(i: Search, event: any) {
i.state.type_ = Number(event.target.value); i.updateUrl({
i.state.page = 1; type_: SearchType[Number(event.target.value)].toLowerCase(),
i.setState(i.state); page: 1,
i.updateUrl(); });
} }
handleSearchSubmit(i: Search, event: any) { handleSearchSubmit(i: Search, event: any) {
event.preventDefault(); event.preventDefault();
i.state.loading = true; i.updateUrl({
i.search(); q: i.state.searchText,
i.setState(i.state); type_: SearchType[i.state.type_].toLowerCase(),
i.updateUrl(); sort: SortType[i.state.sort].toLowerCase(),
page: i.state.page,
});
} }
handleQChange(i: Search, event: any) { handleQChange(i: Search, event: any) {
i.state.q = event.target.value; i.setState({ searchText: event.target.value });
i.setState(i.state);
} }
updateUrl() { updateUrl(paramUpdates: UrlParams) {
let typeStr = SearchType[this.state.type_].toLowerCase(); const qStr = paramUpdates.q || this.state.q;
let sortStr = SortType[this.state.sort].toLowerCase(); const typeStr =
paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
const sortStr =
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
const page = paramUpdates.page || this.state.page;
this.props.history.push( this.props.history.push(
`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}` `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
); );
} }

View file

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

View file

@ -1,10 +1,9 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Prompt } from 'inferno-router'; import { Prompt } from 'inferno-router';
import { MarkdownTextArea } from './markdown-textarea';
import { Site, SiteForm as SiteFormI } from '../interfaces'; import { Site, SiteForm as SiteFormI } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils'; import { capitalizeFirstLetter, randomStr } from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
interface SiteFormProps { interface SiteFormProps {
@ -19,7 +18,6 @@ interface SiteFormState {
export class SiteForm extends Component<SiteFormProps, SiteFormState> { export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private id = `site-form-${randomStr()}`; private id = `site-form-${randomStr()}`;
private tribute: Tribute;
private emptyState: SiteFormState = { private emptyState: SiteFormState = {
siteForm: { siteForm: {
enable_downvotes: true, enable_downvotes: true,
@ -33,8 +31,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.tribute = setupTribute();
this.state = this.emptyState; this.state = this.emptyState;
this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
this
);
if (this.props.site) { if (this.props.site) {
this.state.siteForm = { this.state.siteForm = {
@ -47,17 +47,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
} }
} }
componentDidMount() {
var textarea: any = document.getElementById(this.id);
autosize(textarea);
this.tribute.attach(textarea);
textarea.addEventListener('tribute-replaced', () => {
this.state.siteForm.description = textarea.value;
this.setState(this.state);
autosize.update(textarea);
});
}
// Necessary to stop the loading // Necessary to stop the loading
componentWillReceiveProps() { componentWillReceiveProps() {
this.state.loading = false; this.state.loading = false;
@ -119,13 +108,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
{i18n.t('sidebar')} {i18n.t('sidebar')}
</label> </label>
<div class="col-12"> <div class="col-12">
<textarea <MarkdownTextArea
id={this.id} initialContent={this.state.siteForm.description}
value={this.state.siteForm.description} onContentChange={this.handleSiteDescriptionChange}
onInput={linkEvent(this, this.handleSiteDescriptionChange)}
class="form-control"
rows={3}
maxLength={10000}
/> />
</div> </div>
</div> </div>
@ -238,9 +223,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
i.setState(i.state); i.setState(i.state);
} }
handleSiteDescriptionChange(i: SiteForm, event: any) { handleSiteDescriptionChange(val: string) {
i.state.siteForm.description = event.target.value; this.state.siteForm.description = val;
i.setState(i.state); this.setState(this.state);
} }
handleSiteEnableNsfwChange(i: SiteForm, event: any) { handleSiteEnableNsfwChange(i: SiteForm, event: any) {

View file

@ -23,6 +23,12 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
this.state = this.emptyState; this.state = this.emptyState;
} }
static getDerivedStateFromProps(props: any): SortSelectState {
return {
sort: props.sort,
};
}
render() { render() {
return ( return (
<> <>
@ -59,8 +65,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
} }
handleSortChange(i: SortSelect, event: any) { handleSortChange(i: SortSelect, event: any) {
i.state.sort = Number(event.target.value); i.props.onChange(event.target.value);
i.setState(i.state);
i.props.onChange(i.state.sort);
} }
} }

View file

@ -17,6 +17,8 @@ interface SilverUser {
} }
let general = [ let general = [
'Rachel Schmitz',
'comradeda',
'ybaumy', 'ybaumy',
'dude in phx', 'dude in phx',
'twilight loki', 'twilight loki',

File diff suppressed because one or more lines are too long

304
ui/src/components/user-details.tsx vendored Normal file
View file

@ -0,0 +1,304 @@
import { Component, linkEvent } from 'inferno';
import { WebSocketService, UserService } from '../services';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { i18n } from '../i18next';
import {
UserOperation,
Post,
Comment,
CommunityUser,
SortType,
UserDetailsResponse,
UserView,
WebSocketJsonResponse,
UserDetailsView,
CommentResponse,
BanUserResponse,
PostResponse,
} from '../interfaces';
import {
wsJsonToRes,
toast,
commentsToFlatNodes,
setupTippy,
editCommentRes,
saveCommentRes,
createCommentLikeRes,
createPostLikeFindRes,
} from '../utils';
import { PostListing } from './post-listing';
import { CommentNodes } from './comment-nodes';
interface UserDetailsProps {
username?: string;
user_id?: number;
page: number;
limit: number;
sort: string;
enableDownvotes: boolean;
enableNsfw: boolean;
view: UserDetailsView;
onPageChange(page: number): number | any;
admins: Array<UserView>;
}
interface UserDetailsState {
follows: Array<CommunityUser>;
moderates: Array<CommunityUser>;
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
}
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
private subscription: Subscription;
constructor(props: any, context: any) {
super(props, context);
this.state = {
follows: [],
moderates: [],
comments: [],
posts: [],
saved: [],
};
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
componentDidMount() {
this.fetchUserData();
}
componentDidUpdate(lastProps: UserDetailsProps) {
for (const key of Object.keys(lastProps)) {
if (lastProps[key] !== this.props[key]) {
this.fetchUserData();
break;
}
}
setupTippy();
}
fetchUserData() {
WebSocketService.Instance.getUserDetails({
user_id: this.props.user_id,
username: this.props.username,
sort: this.props.sort,
saved_only: this.props.view === UserDetailsView.Saved,
page: this.props.page,
limit: this.props.limit,
});
}
render() {
return (
<div>
{this.viewSelector(this.props.view)}
{this.paginator()}
</div>
);
}
viewSelector(view: UserDetailsView) {
if (view === UserDetailsView.Overview || view === UserDetailsView.Saved) {
return this.overview();
}
if (view === UserDetailsView.Comments) {
return this.comments();
}
if (view === UserDetailsView.Posts) {
return this.posts();
}
}
overview() {
const comments = this.state.comments.map((c: Comment) => {
return { type: 'comments', data: c };
});
const posts = this.state.posts.map((p: Post) => {
return { type: 'posts', data: p };
});
const combined: Array<{ type: string; data: Comment | Post }> = [
...comments,
...posts,
];
// Sort it
if (SortType[this.props.sort] === SortType.New) {
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
} else {
combined.sort((a, b) => b.data.score - a.data.score);
}
return (
<div>
{combined.map(i => (
<div>
{i.type === 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.props.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
/>
)}
</div>
))}
</div>
);
}
comments() {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.props.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
/>
</div>
);
}
posts() {
return (
<div>
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
))}
</div>
);
}
paginator() {
return (
<div class="my-2">
{this.props.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
</button>
)}
</div>
);
}
nextPage(i: UserDetails) {
i.props.onPageChange(i.props.page + 1);
}
prevPage(i: UserDetails) {
i.props.onPageChange(i.props.page - 1);
}
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
const res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
return;
} else if (msg.reconnect) {
this.fetchUserData();
} else if (res.op == UserOperation.GetUserDetails) {
const data = res.data as UserDetailsResponse;
this.setState({
comments: data.comments,
follows: data.follows,
moderates: data.moderates,
posts: data.posts,
});
} else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
const data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (res.op == UserOperation.CreateComment) {
const data = res.data as CommentResponse;
if (
UserService.Instance.user &&
data.comment.creator_id == UserService.Instance.user.id
) {
toast(i18n.t('reply_sent'));
}
} else if (res.op == UserOperation.SaveComment) {
const data = res.data as CommentResponse;
saveCommentRes(data, this.state.comments);
this.setState({
comments: this.state.comments,
});
} else if (res.op == UserOperation.CreatePostLike) {
const data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.posts);
this.setState({
posts: this.state.posts,
});
} else if (res.op == UserOperation.BanUser) {
const data = res.data as BanUserResponse;
this.state.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.state.posts
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.setState({
posts: this.state.posts,
comments: this.state.comments,
});
}
}
}

View file

@ -4,24 +4,18 @@ import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {
UserOperation, UserOperation,
Post,
Comment,
CommunityUser, CommunityUser,
GetUserDetailsForm,
SortType, SortType,
ListingType, ListingType,
UserDetailsResponse,
UserView, UserView,
CommentResponse,
UserSettingsForm, UserSettingsForm,
LoginResponse, LoginResponse,
BanUserResponse,
AddAdminResponse,
DeleteAccountForm, DeleteAccountForm,
PostResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse, GetSiteResponse,
Site, UserDetailsView,
UserDetailsResponse,
AddAdminResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -34,28 +28,15 @@ import {
languages, languages,
showAvatars, showAvatars,
toast, toast,
editCommentRes,
saveCommentRes,
createCommentLikeRes,
createPostLikeFindRes,
commentsToFlatNodes,
setupTippy, setupTippy,
} from '../utils'; } from '../utils';
import { PostListing } from './post-listing';
import { UserListing } from './user-listing'; import { UserListing } from './user-listing';
import { SortSelect } from './sort-select'; import { SortSelect } from './sort-select';
import { ListingTypeSelect } from './listing-type-select'; import { ListingTypeSelect } from './listing-type-select';
import { CommentNodes } from './comment-nodes';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import moment from 'moment'; import moment from 'moment';
import { UserDetails } from './user-details';
enum View {
Overview,
Comments,
Posts,
Saved,
}
interface UserState { interface UserState {
user: UserView; user: UserView;
@ -63,11 +44,7 @@ interface UserState {
username: string; username: string;
follows: Array<CommunityUser>; follows: Array<CommunityUser>;
moderates: Array<CommunityUser>; moderates: Array<CommunityUser>;
comments: Array<Comment>; view: UserDetailsView;
posts: Array<Post>;
saved?: Array<Post>;
admins: Array<UserView>;
view: View;
sort: SortType; sort: SortType;
page: number; page: number;
loading: boolean; loading: boolean;
@ -77,7 +54,21 @@ interface UserState {
deleteAccountLoading: boolean; deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean; deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm; deleteAccountForm: DeleteAccountForm;
site: Site; siteRes: GetSiteResponse;
}
interface UserProps {
view: UserDetailsView;
sort: SortType;
page: number;
user_id: number | null;
username: string;
}
interface UrlParams {
view?: string;
sort?: string;
page?: number;
} }
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
@ -102,14 +93,11 @@ export class User extends Component<any, UserState> {
username: null, username: null,
follows: [], follows: [],
moderates: [], moderates: [],
comments: [],
posts: [],
admins: [],
loading: true, loading: true,
avatarLoading: false, avatarLoading: false,
view: this.getViewFromProps(this.props), view: User.getViewFromProps(this.props.match.view),
sort: this.getSortTypeFromProps(this.props), sort: User.getSortTypeFromProps(this.props.match.sort),
page: this.getPageFromProps(this.props), page: User.getPageFromProps(this.props.match.page),
userSettingsForm: { userSettingsForm: {
show_nsfw: null, show_nsfw: null,
theme: null, theme: null,
@ -126,19 +114,24 @@ export class User extends Component<any, UserState> {
deleteAccountForm: { deleteAccountForm: {
password: null, password: null,
}, },
site: { siteRes: {
id: undefined, admins: [],
name: undefined, banned: [],
creator_id: undefined, online: undefined,
published: undefined, site: {
creator_name: undefined, id: undefined,
number_of_users: undefined, name: undefined,
number_of_posts: undefined, creator_id: undefined,
number_of_comments: undefined, published: undefined,
number_of_communities: undefined, creator_name: undefined,
enable_downvotes: undefined, number_of_users: undefined,
open_registration: undefined, number_of_posts: undefined,
enable_nsfw: undefined, number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
}, },
}; };
@ -153,8 +146,9 @@ export class User extends Component<any, UserState> {
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
this this
); );
this.handlePageChange = this.handlePageChange.bind(this);
this.state.user_id = Number(this.props.match.params.id); this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username; this.state.username = this.props.match.params.username;
this.subscription = WebSocketService.Instance.subject this.subscription = WebSocketService.Instance.subject
@ -165,7 +159,6 @@ export class User extends Component<any, UserState> {
() => console.log('complete') () => console.log('complete')
); );
this.refetch();
WebSocketService.Instance.getSite(); WebSocketService.Instance.getSite();
} }
@ -176,38 +169,32 @@ export class User extends Component<any, UserState> {
); );
} }
getViewFromProps(props: any): View { static getViewFromProps(view: any): UserDetailsView {
return props.match.params.view return view
? View[capitalizeFirstLetter(props.match.params.view)] ? UserDetailsView[capitalizeFirstLetter(view)]
: View.Overview; : UserDetailsView.Overview;
} }
getSortTypeFromProps(props: any): SortType { static getSortTypeFromProps(sort: any): SortType {
return props.match.params.sort return sort ? routeSortTypeToEnum(sort) : SortType.New;
? routeSortTypeToEnum(props.match.params.sort)
: SortType.New;
} }
getPageFromProps(props: any): number { static getPageFromProps(page: any): number {
return props.match.params.page ? Number(props.match.params.page) : 1; return page ? Number(page) : 1;
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
// Necessary for back button for some reason static getDerivedStateFromProps(props: any): UserProps {
componentWillReceiveProps(nextProps: any) { return {
if ( view: this.getViewFromProps(props.match.params.view),
nextProps.history.action == 'POP' || sort: this.getSortTypeFromProps(props.match.params.sort),
nextProps.history.action == 'PUSH' page: this.getPageFromProps(props.match.params.page),
) { user_id: Number(props.match.params.id) || null,
this.state.view = this.getViewFromProps(nextProps); username: props.match.params.username,
this.state.sort = this.getSortTypeFromProps(nextProps); };
this.state.page = this.getPageFromProps(nextProps);
this.setState(this.state);
this.refetch();
}
} }
componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) { componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
@ -219,46 +206,58 @@ export class User extends Component<any, UserState> {
// Couldnt get a refresh working. This does for now. // Couldnt get a refresh working. This does for now.
location.reload(); location.reload();
} }
document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
setupTippy();
} }
render() { render() {
return ( return (
<div class="container"> <div class="container">
{this.state.loading ? ( <div class="row">
<h5> <div class="col-12 col-md-8">
<svg class="icon icon-spinner spin"> <h5>
<use xlinkHref="#icon-spinner"></use> {this.state.user.avatar && showAvatars() && (
</svg> <img
</h5> height="80"
) : ( width="80"
<div class="row"> src={this.state.user.avatar}
<div class="col-12 col-md-8"> class="rounded-circle mr-2"
/>
)}
<span>/u/{this.state.username}</span>
</h5>
{this.state.loading ? (
<h5> <h5>
{this.state.user.avatar && showAvatars() && ( <svg class="icon icon-spinner spin">
<img <use xlinkHref="#icon-spinner"></use>
height="80" </svg>
width="80"
src={this.state.user.avatar}
class="rounded-circle mr-2"
/>
)}
<span>/u/{this.state.user.name}</span>
</h5> </h5>
{this.selects()} ) : (
{this.state.view == View.Overview && this.overview()} this.selects()
{this.state.view == View.Comments && this.comments()} )}
{this.state.view == View.Posts && this.posts()} <UserDetails
{this.state.view == View.Saved && this.overview()} user_id={this.state.user_id}
{this.paginator()} username={this.state.username}
</div> sort={SortType[this.state.sort]}
page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
admins={this.state.siteRes.admins}
view={this.state.view}
onPageChange={this.handlePageChange}
/>
</div>
{!this.state.loading && (
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
{this.userInfo()} {this.userInfo()}
{this.isCurrentUser && this.userSettings()} {this.isCurrentUser && this.userSettings()}
{this.moderates()} {this.moderates()}
{this.follows()} {this.follows()}
</div> </div>
</div> )}
)} </div>
</div> </div>
); );
} }
@ -268,52 +267,52 @@ export class User extends Component<any, UserState> {
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle">
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Overview && 'active'} ${this.state.view == UserDetailsView.Overview && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Overview} value={UserDetailsView.Overview}
checked={this.state.view == View.Overview} checked={this.state.view === UserDetailsView.Overview}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('overview')} {i18n.t('overview')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Comments && 'active'} ${this.state.view == UserDetailsView.Comments && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Comments} value={UserDetailsView.Comments}
checked={this.state.view == View.Comments} checked={this.state.view == UserDetailsView.Comments}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('comments')} {i18n.t('comments')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Posts && 'active'} ${this.state.view == UserDetailsView.Posts && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Posts} value={UserDetailsView.Posts}
checked={this.state.view == View.Posts} checked={this.state.view == UserDetailsView.Posts}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('posts')} {i18n.t('posts')}
</label> </label>
<label <label
className={`btn btn-sm btn-secondary pointer btn-outline-light className={`btn btn-sm btn-secondary pointer btn-outline-light
${this.state.view == View.Saved && 'active'} ${this.state.view == UserDetailsView.Saved && 'active'}
`} `}
> >
<input <input
type="radio" type="radio"
value={View.Saved} value={UserDetailsView.Saved}
checked={this.state.view == View.Saved} checked={this.state.view == UserDetailsView.Saved}
onChange={linkEvent(this, this.handleViewChange)} onChange={linkEvent(this, this.handleViewChange)}
/> />
{i18n.t('saved')} {i18n.t('saved')}
@ -347,84 +346,6 @@ export class User extends Component<any, UserState> {
); );
} }
overview() {
let combined: Array<{ type_: string; data: Comment | Post }> = [];
let comments = this.state.comments.map(e => {
return { type_: 'comments', data: e };
});
let posts = this.state.posts.map(e => {
return { type_: 'posts', data: e };
});
combined.push(...comments);
combined.push(...posts);
// Sort it
if (this.state.sort == SortType.New) {
combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
} else {
combined.sort((a, b) => b.data.score - a.data.score);
}
return (
<div>
{combined.map(i => (
<div>
{i.type_ == 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins}
noIndent
showCommunity
showContext
enableDownvotes={this.state.site.enable_downvotes}
/>
)}
</div>
))}
</div>
);
}
comments() {
return (
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins}
noIndent
showCommunity
showContext
enableDownvotes={this.state.site.enable_downvotes}
/>
</div>
);
}
posts() {
return (
<div>
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
))}
</div>
);
}
userInfo() { userInfo() {
let user = this.state.user; let user = this.state.user;
return ( return (
@ -722,7 +643,7 @@ export class User extends Component<any, UserState> {
/> />
</div> </div>
</div> </div>
{this.state.site.enable_nsfw && ( {this.state.siteRes.site.enable_nsfw && (
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input
@ -896,77 +817,30 @@ export class User extends Component<any, UserState> {
); );
} }
paginator() { updateUrl(paramUpdates: UrlParams) {
return ( const page = paramUpdates.page || this.state.page;
<div class="my-2"> const viewStr =
{this.state.page > 1 && ( paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
<button const sortStr =
class="btn btn-sm btn-secondary mr-1" paramUpdates.sort || SortType[this.state.sort].toLowerCase();
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
</button>
)}
</div>
);
}
updateUrl() {
let viewStr = View[this.state.view].toLowerCase();
let sortStr = SortType[this.state.sort].toLowerCase();
this.props.history.push( this.props.history.push(
`/u/${this.state.user.name}/view/${viewStr}/sort/${sortStr}/page/${this.state.page}` `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
); );
} }
nextPage(i: User) { handlePageChange(page: number) {
i.state.page++; this.updateUrl({ page });
i.setState(i.state);
i.updateUrl();
i.refetch();
}
prevPage(i: User) {
i.state.page--;
i.setState(i.state);
i.updateUrl();
i.refetch();
}
refetch() {
let form: GetUserDetailsForm = {
user_id: this.state.user_id,
username: this.state.username,
sort: SortType[this.state.sort],
saved_only: this.state.view == View.Saved,
page: this.state.page,
limit: fetchLimit,
};
WebSocketService.Instance.getUserDetails(form);
} }
handleSortChange(val: SortType) { handleSortChange(val: SortType) {
this.state.sort = val; this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
this.state.page = 1;
this.setState(this.state);
this.updateUrl();
this.refetch();
} }
handleViewChange(i: User, event: any) { handleViewChange(i: User, event: any) {
i.state.view = Number(event.target.value); i.updateUrl({
i.state.page = 1; view: UserDetailsView[Number(event.target.value)].toLowerCase(),
i.setState(i.state); page: 1,
i.updateUrl(); });
i.refetch();
} }
handleUserSettingsShowNsfwChange(i: User, event: any) { handleUserSettingsShowNsfwChange(i: User, event: any) {
@ -1137,100 +1011,69 @@ export class User extends Component<any, UserState> {
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
console.log(msg); console.log(msg);
let res = wsJsonToRes(msg); const res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {
toast(i18n.t(msg.error), 'danger'); toast(i18n.t(msg.error), 'danger');
this.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') { if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/'); this.context.router.history.push('/');
} }
this.setState(this.state); this.setState({
deleteAccountLoading: false,
avatarLoading: false,
userSettingsLoading: false,
});
return; return;
} else if (msg.reconnect) {
this.refetch();
} else if (res.op == UserOperation.GetUserDetails) { } else if (res.op == UserOperation.GetUserDetails) {
let data = res.data as UserDetailsResponse; // Since the UserDetails contains posts/comments as well as some general user info we listen here as well
this.state.user = data.user; // and set the parent state if it is not set or differs
this.state.comments = data.comments; const data = res.data as UserDetailsResponse;
this.state.follows = data.follows;
this.state.moderates = data.moderates; if (this.state.user.id !== data.user.id) {
this.state.posts = data.posts; this.state.user = data.user;
this.state.admins = data.admins; this.state.follows = data.follows;
this.state.loading = false; this.state.moderates = data.moderates;
if (this.isCurrentUser) {
this.state.userSettingsForm.show_nsfw = if (this.isCurrentUser) {
UserService.Instance.user.show_nsfw; this.state.userSettingsForm.show_nsfw =
this.state.userSettingsForm.theme = UserService.Instance.user.theme UserService.Instance.user.show_nsfw;
? UserService.Instance.user.theme this.state.userSettingsForm.theme = UserService.Instance.user.theme
: 'darkly'; ? UserService.Instance.user.theme
this.state.userSettingsForm.default_sort_type = : 'darkly';
UserService.Instance.user.default_sort_type; this.state.userSettingsForm.default_sort_type =
this.state.userSettingsForm.default_listing_type = UserService.Instance.user.default_sort_type;
UserService.Instance.user.default_listing_type; this.state.userSettingsForm.default_listing_type =
this.state.userSettingsForm.lang = UserService.Instance.user.lang; UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar; this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.email = this.state.user.email; this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email; this.state.userSettingsForm.email = this.state.user.email;
this.state.userSettingsForm.show_avatars = this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
UserService.Instance.user.show_avatars; this.state.userSettingsForm.show_avatars =
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id; UserService.Instance.user.show_avatars;
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
}
this.state.loading = false;
this.setState(this.state);
} }
document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreateComment) {
let data = res.data as CommentResponse;
if (
UserService.Instance.user &&
data.comment.creator_id == UserService.Instance.user.id
) {
toast(i18n.t('reply_sent'));
}
} else if (res.op == UserOperation.SaveComment) {
let data = res.data as CommentResponse;
saveCommentRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreateCommentLike) {
let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments);
this.setState(this.state);
} else if (res.op == UserOperation.CreatePostLike) {
let data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.posts);
this.setState(this.state);
} else if (res.op == UserOperation.BanUser) {
let data = res.data as BanUserResponse;
this.state.comments
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.state.posts
.filter(c => c.creator_id == data.user.id)
.forEach(c => (c.banned = data.banned));
this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) {
let data = res.data as AddAdminResponse;
this.state.admins = data.admins;
this.setState(this.state);
} else if (res.op == UserOperation.SaveUserSettings) { } else if (res.op == UserOperation.SaveUserSettings) {
let data = res.data as LoginResponse; const data = res.data as LoginResponse;
this.state.userSettingsLoading = false;
this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
this.setState({
userSettingsLoading: false,
});
window.scrollTo(0, 0);
} else if (res.op == UserOperation.DeleteAccount) { } else if (res.op == UserOperation.DeleteAccount) {
this.state.deleteAccountLoading = false; this.setState({
this.state.deleteAccountShowConfirm = false; deleteAccountLoading: false,
this.setState(this.state); deleteAccountShowConfirm: false,
});
this.context.router.history.push('/'); this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) { } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse; const data = res.data as GetSiteResponse;
this.state.site = data.site; this.state.siteRes = data;
this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

131
ui/src/interfaces.ts vendored
View file

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

View file

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

9
ui/src/utils.ts vendored
View file

@ -49,6 +49,8 @@ import { UserService, WebSocketService } from './services';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import markdown_it from 'markdown-it'; import markdown_it from 'markdown-it';
import markdown_it_sub from 'markdown-it-sub';
import markdown_it_sup from 'markdown-it-sup';
import markdownitEmoji from 'markdown-it-emoji/light'; import markdownitEmoji from 'markdown-it-emoji/light';
import markdown_it_container from 'markdown-it-container'; import markdown_it_container from 'markdown-it-container';
import emojiShortName from 'emoji-short-name'; import emojiShortName from 'emoji-short-name';
@ -148,6 +150,8 @@ export const md = new markdown_it({
linkify: true, linkify: true,
typographer: true, typographer: true,
}) })
.use(markdown_it_sub)
.use(markdown_it_sup)
.use(markdown_it_container, 'spoiler', { .use(markdown_it_container, 'spoiler', {
validate: function (params: any) { validate: function (params: any) {
return params.trim().match(/^spoiler\s+(.*)$/); return params.trim().match(/^spoiler\s+(.*)$/);
@ -825,6 +829,11 @@ export function editPostRes(data: PostResponse, post: Post) {
post.url = data.post.url; post.url = data.post.url;
post.name = data.post.name; post.name = data.post.name;
post.nsfw = data.post.nsfw; post.nsfw = data.post.nsfw;
post.deleted = data.post.deleted;
post.removed = data.post.removed;
post.stickied = data.post.stickied;
post.body = data.post.body;
post.locked = data.post.locked;
} }
} }

1
ui/src/version.ts vendored
View file

@ -1 +0,0 @@
export const version: string = 'v0.7.20';

View file

@ -51,6 +51,15 @@
"unsticky": "unsticky", "unsticky": "unsticky",
"link": "link", "link": "link",
"archive_link": "archive link", "archive_link": "archive link",
"bold": "bold",
"italic": "italic",
"subscript": "subscript",
"superscript": "superscript",
"header": "header",
"strikethrough": "strikethrough",
"quote": "quote",
"spoiler": "spoiler",
"list": "list",
"mod": "mod", "mod": "mod",
"mods": "mods", "mods": "mods",
"moderates": "Moderates", "moderates": "Moderates",
@ -154,7 +163,7 @@
"email": "Email", "email": "Email",
"matrix_user_id": "Matrix User", "matrix_user_id": "Matrix User",
"private_message_disclaimer": "private_message_disclaimer":
"Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.", "Warning: Private messages in Lemmy are not secure. Please create an account on <1>Element.io</1> for secure messaging.",
"send_notifications_to_email": "Send notifications to Email", "send_notifications_to_email": "Send notifications to Email",
"optional": "Optional", "optional": "Optional",
"expires": "Expires", "expires": "Expires",
@ -247,12 +256,14 @@
"couldnt_save_post": "Couldn't save post.", "couldnt_save_post": "Couldn't save post.",
"no_slurs": "No slurs.", "no_slurs": "No slurs.",
"not_an_admin": "Not an admin.", "not_an_admin": "Not an admin.",
"not_a_moderator": "Not a moderator.",
"site_already_exists": "Site already exists.", "site_already_exists": "Site already exists.",
"couldnt_update_site": "Couldn't update site.", "couldnt_update_site": "Couldn't update site.",
"couldnt_find_that_username_or_email": "couldnt_find_that_username_or_email":
"Couldn't find that username or email.", "Couldn't find that username or email.",
"password_incorrect": "Password incorrect.", "password_incorrect": "Password incorrect.",
"passwords_dont_match": "Passwords do not match.", "passwords_dont_match": "Passwords do not match.",
"no_password_reset": "You will not be able to reset your password without an email.",
"invalid_username": "Invalid username.", "invalid_username": "Invalid username.",
"admin_already_created": "Sorry, there's already an admin.", "admin_already_created": "Sorry, there's already an admin.",
"user_already_exists": "User already exists.", "user_already_exists": "User already exists.",
@ -269,5 +280,6 @@
"what_is": "What is", "what_is": "What is",
"cake_day_title": "Cake day:", "cake_day_title": "Cake day:",
"cake_day_info": "It's {{ creator_name }}'s cake day today!", "cake_day_info": "It's {{ creator_name }}'s cake day today!",
"invalid_post_title": "Invalid post title" "invalid_post_title": "Invalid post title",
"invalid_url": "Invalid URL."
} }

View file

@ -29,19 +29,19 @@
"unlock": "malŝlosi", "unlock": "malŝlosi",
"lock": "ŝlosi", "lock": "ŝlosi",
"link": "ligilo", "link": "ligilo",
"mod": "moderanto", "mod": "reguligisto",
"mods": "moderantoj", "mods": "reguligistoj",
"moderates": "Moderigas", "moderates": "reguligas",
"settings": "Agordoj", "settings": "Agordoj",
"remove_as_mod": "forigi per moderanto", "remove_as_mod": "forigi kiel reguligisto",
"appoint_as_mod": "nomumi per moderanto", "appoint_as_mod": "nomumi reguligisto",
"modlog": "Moderlogo", "modlog": "Protokolo de reguligado",
"admin": "administranto", "admin": "administranto",
"admins": "administrantoj", "admins": "administrantoj",
"remove_as_admin": "forigi kiel administranto", "remove_as_admin": "forigi kiel administranto",
"appoint_as_admin": "nomumi administranto", "appoint_as_admin": "nomumi administranto",
"remove": "forigi", "remove": "forigi",
"removed": "fortirita", "removed": "forigita de reguligisto",
"locked": "ŝlosita", "locked": "ŝlosita",
"reason": "Kialo", "reason": "Kialo",
"mark_as_read": "marki legita", "mark_as_read": "marki legita",
@ -77,7 +77,7 @@
"next": "Pluen", "next": "Pluen",
"sidebar": "Flankobreto", "sidebar": "Flankobreto",
"sort_type": "Ordigilo", "sort_type": "Ordigilo",
"hot": "Varmaj", "hot": "Furoraj",
"new": "Novaj", "new": "Novaj",
"top_day": "Supraj tagaj", "top_day": "Supraj tagaj",
"week": "Semajno", "week": "Semajno",
@ -138,7 +138,7 @@
"transfer_community": "transdoni la komunumon", "transfer_community": "transdoni la komunumon",
"transfer_site": "transdoni la retejon", "transfer_site": "transdoni la retejon",
"powered_by": "Konstruita per", "powered_by": "Konstruita per",
"landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Dankon al niaj kontribuintoj: </15> dessalines, Nutomic, asonix, zacanger, kaj iav.",
"not_logged_in": "Nesalutinta.", "not_logged_in": "Nesalutinta.",
"community_ban": "Vi estas forbarita de la komunumo.", "community_ban": "Vi estas forbarita de la komunumo.",
"site_ban": "Vi estas forbarita de la retejo", "site_ban": "Vi estas forbarita de la retejo",
@ -152,7 +152,7 @@
"couldnt_find_community": "Ne povis trovi la komunumon.", "couldnt_find_community": "Ne povis trovi la komunumon.",
"couldnt_update_community": "Ne povis ĝisdatigi la komunumon.", "couldnt_update_community": "Ne povis ĝisdatigi la komunumon.",
"community_already_exists": "Komunumo jam ekzistas.", "community_already_exists": "Komunumo jam ekzistas.",
"community_moderator_already_exists": "Komunuma moderanto jam ekzistas.", "community_moderator_already_exists": "Reguligisto de komunumo jam ekzistas.",
"community_follower_already_exists": "Abonanto de komunumo jam ekzistas.", "community_follower_already_exists": "Abonanto de komunumo jam ekzistas.",
"community_user_already_banned": "Uzanto de komunumo jam estas forbarita.", "community_user_already_banned": "Uzanto de komunumo jam estas forbarita.",
"couldnt_create_post": "Ne povis krei la afiŝon.", "couldnt_create_post": "Ne povis krei la afiŝon.",
@ -256,5 +256,11 @@
"number_of_upvotes_plural": "{{count}} porvoĉoj", "number_of_upvotes_plural": "{{count}} porvoĉoj",
"downvote": "Kontraŭvoĉi", "downvote": "Kontraŭvoĉi",
"number_of_downvotes": "{{count}} kontraŭvoĉo", "number_of_downvotes": "{{count}} kontraŭvoĉo",
"number_of_downvotes_plural": "{{count}} kontraŭvoĉoj" "number_of_downvotes_plural": "{{count}} kontraŭvoĉoj",
"what_is": "Kio estas",
"must_login": "Vi devas <1>saluti aŭ registriĝi</1> por komenti.",
"no_password_reset": "Vi ne povos restarigi vian pasvorton sen retpoŝtadreso.",
"cake_day_title": "Tortotago:",
"cake_day_info": "Hodiaŭ estas tortotago de {{ creator_name }}!",
"invalid_post_title": "Nevalida titolo de afiŝo"
} }

View file

@ -28,7 +28,7 @@
"create_private_message": "Luo yksityisviesti", "create_private_message": "Luo yksityisviesti",
"send_secure_message": "Lähetä suojattu viesti", "send_secure_message": "Lähetä suojattu viesti",
"send_message": "Lähetä viesti", "send_message": "Lähetä viesti",
"message": "Viesti", "message": "Lähetä",
"edit": "muokkaa", "edit": "muokkaa",
"reply": "vastaa", "reply": "vastaa",
"cancel": "Peru", "cancel": "Peru",
@ -49,12 +49,12 @@
"mods": "moderaattorit", "mods": "moderaattorit",
"moderates": "Moderoi", "moderates": "Moderoi",
"settings": "Asetukset", "settings": "Asetukset",
"remove_as_mod": "Poista moderaattorina", "remove_as_mod": "Poista moderaattorin asemasta",
"appoint_as_mod": "Nimitä moderaattoriksi", "appoint_as_mod": "Nimitä moderaattoriksi",
"modlog": "Moderoinnin loki", "modlog": "Moderoinnin loki",
"admin": "ylläpitäjä", "admin": "ylläpitäjä",
"admins": "ylläpitäjät", "admins": "ylläpitäjät",
"remove_as_admin": "poista ylläpitäjänä", "remove_as_admin": "poista ylläpitäjän asemasta",
"appoint_as_admin": "nimitä ylläpitäjäksi", "appoint_as_admin": "nimitä ylläpitäjäksi",
"remove": "poista", "remove": "poista",
"removed": "poistettu", "removed": "poistettu",
@ -126,7 +126,7 @@
"login_sign_up": "Kirjaudu sisään / Rekisteröidy", "login_sign_up": "Kirjaudu sisään / Rekisteröidy",
"login": "Kirjaudu sisään", "login": "Kirjaudu sisään",
"sign_up": "Rekisteröidy", "sign_up": "Rekisteröidy",
"notifications_error": "Työpöydän ilmoitukset eivät ole saatavilla selaimellesi. Yritä Firefoxia tai Chromea.", "notifications_error": "Työpöydän ilmoitukset eivät ole saatavilla selaimellesi. Kokeile Firefoxilla tai Chromella.",
"unread_messages": "Lukemattomat viestit", "unread_messages": "Lukemattomat viestit",
"messages": "Viestit", "messages": "Viestit",
"password": "Salasana", "password": "Salasana",
@ -169,31 +169,31 @@
"theme": "Teema", "theme": "Teema",
"sponsors": "Sponsorit", "sponsors": "Sponsorit",
"sponsors_of_lemmy": "Lemmy-sponsorit", "sponsors_of_lemmy": "Lemmy-sponsorit",
"sponsor_message": "Lemmy on vapaa, <1>avoimen lähdekoodin</1> -ohjelmisto, eli mainontaa, rahantekemistä, tai pääomasijoitusta täällä ei tule ikinä olemaan. Lahjoituksesi tukevat suoraan projektin täysipäiväistä kehitystä. Kiitokset seuraaville ihmisille:", "sponsor_message": "Lemmy on vapaa, <1>avoimen lähdekoodin</1> -ohjelmisto, eli tällä ei tulla tekemään rahaa. Lahjoituksesi tukevat suoraan projektin täysipäiväistä kehitystä. Kiitokset seuraaville lahjoittajille:",
"support_on_patreon": "Tue Patreonissa", "support_on_patreon": "Tue Patreonissa",
"donate_to_lemmy": "Lahjoita Lemmylle", "donate_to_lemmy": "Lahjoita Lemmylle",
"donate": "Lahjoita", "donate": "Lahjoita",
"general_sponsors": "Yleisiä sponsoreja ovat he, jotka lupaavat 10-39 dollaria Lemmylle.", "general_sponsors": "Yleiset sponsorit lupaavat 10-39 dollaria Lemmylle.",
"crypto": "Krypto", "crypto": "Krypto",
"bitcoin": "Bitcoin", "bitcoin": "Bitcoin",
"ethereum": "Ethereum", "ethereum": "Ethereum",
"monero": "Monero", "monero": "Monero",
"code": "Code", "code": "Lähdekoodi",
"joined": "Liittyi", "joined": "Liittyi",
"by": "käyttäjältä", "by": "käyttäjältä",
"to": "yhteisössä", "to": "yhteisössä",
"from": "paikasta", "from": "paikasta",
"transfer_community": "siirron yhteisö", "transfer_community": "siirrä yhteisö",
"transfer_site": "siirron määrä", "transfer_site": "siirrä sivusto",
"are_you_sure": "oletko varma?", "are_you_sure": "oletko varma?",
"yes": "kyllä", "yes": "kyllä",
"no": "ei", "no": "ei",
"powered_by": "Vauhdittajana", "powered_by": "Vauhdittajana",
"landing_0": "Lemmy on <1>linkinkerääjä</1> / Reddit-vaihtoehto, tarkoitettu toimimaan <2>fediversessä</2>.<3></3>Sitä voi isännöidä itse, siinä on tosiaikaisesti päivittyvät kommenttiketjut, ja se on pieni (<4>~80 kilotavua</4>). Federointi ActivityPub-verkkoon on suunnittelun alla. <5></5>Tämä on <6>hyvin varhainen betaversio</6>, ja monet ominaisuudet ovat toistaiseksi rikki tai poissa. <7></7>Ehdota uusia ominaisuuksia tai raportoi bugeja <8>tänne.</8><9></9>Tehty teknologioilla <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing_0": "Lemmy on <2>fediversessä</2> toimiva <1>linkinkerääjä</1> / Reddit-vaihtoehto.<3></3>Sitä voi ylläpitää itse, siinä on tosiaikaisesti päivittyvät kommenttiketjut, ja se on pieni (<4>~80 kilotavua</4>). Federointi ActivityPub-verkkoon on suunnittelun alla. <5></5>Tämä on <6>hyvin varhainen betaversio</6>, ja monet ominaisuudet ovat toistaiseksi rikki tai poissa. <7></7>Ehdota uusia ominaisuuksia tai raportoi bugeja <8>tänne.</8><9></9>Tehty teknologioilla <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> ja <13>Typescript</13>. Kiitoksia projektin kehitykseen osallistuneille käyttäjille dessalines, Nutomic, asonix, zacanger ja iav.",
"not_logged_in": "Ei kirjautunut sisään.", "not_logged_in": "Ei kirjautunut sisään.",
"logged_in": "Kirjautunut sisään.", "logged_in": "Kirjautunut sisään.",
"community_ban": "Sinulle on asetettu porttikielto tähän yhteisöön.", "community_ban": "Sinulle on asetettu porttikielto tähän yhteisöön.",
"site_ban": "Sinut on asetettu porttikieltoon tältä sivustolta", "site_ban": "Sinut on asetettu porttikieltoon tällä sivustolla",
"couldnt_create_comment": "Kommenttia ei pystytty luomaan.", "couldnt_create_comment": "Kommenttia ei pystytty luomaan.",
"couldnt_like_comment": "Kommentista ei voitu tykätä.", "couldnt_like_comment": "Kommentista ei voitu tykätä.",
"couldnt_update_comment": "Kommenttia ei voitu päivittää.", "couldnt_update_comment": "Kommenttia ei voitu päivittää.",
@ -220,6 +220,7 @@
"couldnt_find_that_username_or_email": "Käyttäjänimeä tai sähköpostia ei onnistuttu löytämään.", "couldnt_find_that_username_or_email": "Käyttäjänimeä tai sähköpostia ei onnistuttu löytämään.",
"password_incorrect": "Salasana on väärin.", "password_incorrect": "Salasana on väärin.",
"passwords_dont_match": "Salasanat eivät täsmää.", "passwords_dont_match": "Salasanat eivät täsmää.",
"no_password_reset": "Et voi nollata salasanaasi ilman sähköpostia.",
"admin_already_created": "Anteeksi, mutta täällä on jo ylläpitäjä.", "admin_already_created": "Anteeksi, mutta täällä on jo ylläpitäjä.",
"user_already_exists": "Käyttäjä on jo olemassa.", "user_already_exists": "Käyttäjä on jo olemassa.",
"email_already_exists": "Sähköposti on jo olemassa.", "email_already_exists": "Sähköposti on jo olemassa.",
@ -229,7 +230,7 @@
"no_private_message_edit_allowed": "Sinulla ei ole oikeutta muokata yksityisviestiä.", "no_private_message_edit_allowed": "Sinulla ei ole oikeutta muokata yksityisviestiä.",
"couldnt_update_private_message": "Yksityisviestiä ei voitu päivittää.", "couldnt_update_private_message": "Yksityisviestiä ei voitu päivittää.",
"more": "lisää", "more": "lisää",
"cross_posted_to": "ristipostattu: ", "cross_posted_to": "jaettu ristiin: ",
"sorting_help": "apua lajitteluun", "sorting_help": "apua lajitteluun",
"show_context": "Näytä yhteys", "show_context": "Näytä yhteys",
"admin_settings": "Ylläpitäjän asetukset", "admin_settings": "Ylläpitäjän asetukset",
@ -250,6 +251,10 @@
"block_leaving": "Haluatko varmasti poistua?", "block_leaving": "Haluatko varmasti poistua?",
"silver_sponsors": "Hopeasponsoreita ovat ne, jotka lupaavat 40 dollaria Lemmylle.", "silver_sponsors": "Hopeasponsoreita ovat ne, jotka lupaavat 40 dollaria Lemmylle.",
"post_title_too_long": "Viestin otsikko on liian pitkä.", "post_title_too_long": "Viestin otsikko on liian pitkä.",
"support_on_open_collective": "Tie OpenCollectivessa", "support_on_open_collective": "Tue OpenCollectivessa",
"site_saved": "Sivu tallennettu." "site_saved": "Sivu tallennettu.",
"what_is": "Mikä on",
"cake_day_title": "Kakkupäivä:",
"cake_day_info": "Tänään on käyttäjän {{ creator_name }} kakkupäivä!",
"invalid_post_title": "Väärä viestin otsikko"
} }

View file

@ -192,7 +192,7 @@
"yes": "oui", "yes": "oui",
"no": "non", "no": "non",
"powered_by": "Propulsé par", "powered_by": "Propulsé par",
"landing": "Lemmy est un <1>aggrégateur de liens</1>, similaire à Reddit et conçu pour fonctionner sur le <2>Fédivers</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via ActivityPub est prévue dans sa feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6> et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez signaler des bugs et suggérer de nouvelles fonctionnalités <8>ici.</8><9></9>Créé avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy est un <1>aggrégateur de liens</1>, similaire à Reddit et conçu pour fonctionner sur le <2>Fédivers</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via ActivityPub est prévue dans sa feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6> et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez signaler des bugs et suggérer de nouvelles fonctionnalités <8>ici.</8><9></9>Créé avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Merci à nos contributeurs : </15> dessalines, Nutomic, asonix, zacanger, et iav.",
"not_logged_in": "Vous nêtes pas connecté.", "not_logged_in": "Vous nêtes pas connecté.",
"logged_in": "Vous êtes connecté.", "logged_in": "Vous êtes connecté.",
"community_ban": "Vous avez été banni de cette communauté.", "community_ban": "Vous avez été banni de cette communauté.",
@ -256,5 +256,10 @@
"invalid_username": "Nom d'utilisateur invalide.", "invalid_username": "Nom d'utilisateur invalide.",
"invalid_community_name": "Nom invalide.", "invalid_community_name": "Nom invalide.",
"click_to_delete_picture": "Cliquer pour supprimer l'image.", "click_to_delete_picture": "Cliquer pour supprimer l'image.",
"picture_deleted": "Image supprimée." "picture_deleted": "Image supprimée.",
"invalid_post_title": "Titre du post invalide",
"must_login": "Vous devez vous être <1>connecté ou enregistré</1> pour commenter.",
"no_password_reset": "Vous ne pourrez pas réinitialiser votre mot de passe sans un e-mail.",
"what_is": "Combien font",
"cake_day_title": "Lemmyversaire:"
} }

View file

@ -160,7 +160,7 @@
"yes": "sì", "yes": "sì",
"no": "no", "no": "no",
"powered_by": "Offerto da", "powered_by": "Offerto da",
"landing": "Lemmy è un <1>aggregatore di link</1> / alternativa a reddit, creato per integrarsi con il <2>fediverso</2>. <3></3>È self-hosted, i commenti sono aggiornati in tempo reale ed è molto piccolo (<4>~80kB</4>). La federazione con la rete ActivityPub sarà implementata nel futuro. <5></5>Questa versione è una <6>beta molto giovane</6> e molte funzionalità sono incomplete o mancanti. <7></7>Suggerisci nuove funzionalità o segnala errori a <8>questa pagina.</8><9></9>Sviluppato con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy è un <1>aggregatore di link</1> / alternativa a reddit, creato per integrarsi con il <2>fediverso</2>. <3></3>È self-hosted, i commenti sono aggiornati in tempo reale ed è molto piccolo (<4>~80kB</4>). La federazione con la rete ActivityPub sarà implementata nel futuro. <5></5>Questa versione è una <6>beta molto giovane</6> e molte funzionalità sono incomplete o mancanti. <7></7>Suggerisci nuove funzionalità o segnala errori a <8>questa pagina.</8><9></9>Sviluppato con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.<14></14> <15>Un grazie ai nostri sostenitori: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
"not_logged_in": "Non hai effettuato l'accesso.", "not_logged_in": "Non hai effettuato l'accesso.",
"community_ban": "Sei stato escluso da questa comunità.", "community_ban": "Sei stato escluso da questa comunità.",
"site_ban": "Sei stato escluso dal sito", "site_ban": "Sei stato escluso dal sito",
@ -256,5 +256,11 @@
"click_to_delete_picture": "Clicca per eliminare la foto.", "click_to_delete_picture": "Clicca per eliminare la foto.",
"picture_deleted": "Foto eliminata.", "picture_deleted": "Foto eliminata.",
"select_a_community": "Seleziona una comunità", "select_a_community": "Seleziona una comunità",
"invalid_username": "Nome utente non valido." "invalid_username": "Nome utente non valido.",
"what_is": "Cos'è",
"must_login": "Devi <1>effettuare l'accesso o registrarti</1> per commentare.",
"no_password_reset": "Non sarai in grado di resettare la tua password senza una email.",
"cake_day_title": "Cake day:",
"cake_day_info": "Oggi è il cake day di {{ creator_name }}!",
"invalid_post_title": "Titolo della pubblicazione non valido"
} }

View file

@ -192,7 +192,7 @@
"yes": "sim", "yes": "sim",
"no": "não", "no": "não",
"powered_by": "Fornecido por", "powered_by": "Fornecido por",
"landing_0": "Lemmy é um <1>agregador de links</1> / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso</2>.<3></3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB</4>). A federação com a rede ActivityPub está no roteiro do projeto. <5></5>Esta é uma <6>versão beta bastante antecipada</6>, e muitas funcionalidades ainda estão quebradas ou ausentes. <7></7>Sugira novas funcionalidades ou reporte erros <8>aqui.</8><9></9>Feito com <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy é um <1>agregador de links</1> / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso</2>.<3></3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB</4>). A federação com a rede ActivityPub está no roteiro do projeto. <5></5>Esta é uma <6>versão beta bastante antecipada</6>, e muitas funcionalidades ainda estão quebradas ou ausentes. <7></7>Sugira novas funcionalidades ou reporte erros <8>aqui.</8><9></9>Feito com <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Agradecemos aos nossos contribuidores: </15> dessalines, Nutomic, asonix, zacanger, e iav.",
"not_logged_in": "Não autenticado.", "not_logged_in": "Não autenticado.",
"logged_in": "Autenticado.", "logged_in": "Autenticado.",
"community_ban": "Você foi banido desta comunidade.", "community_ban": "Você foi banido desta comunidade.",
@ -256,5 +256,10 @@
"site_saved": "Site Salvo.", "site_saved": "Site Salvo.",
"emoji_picker": "Selecionador de Emoji", "emoji_picker": "Selecionador de Emoji",
"select_a_community": "Selecione uma comunidade", "select_a_community": "Selecione uma comunidade",
"invalid_username": "Nome de usuário inválido." "invalid_username": "Nome de usuário inválido.",
"must_login": "Você precisa <1>entrar ou registrar-se</1> para comentar.",
"no_password_reset": "Você não conseguirá redefinir sua senha sem um e-mail.",
"invalid_post_title": "Título de publicação inválido",
"cake_day_info": "Hoje é o dia do bolo de {{ creator_name }}!",
"cake_day_title": "Dia do bolo:"
} }

View file

@ -135,7 +135,7 @@
"code": "Код", "code": "Код",
"joined": "Присоединился", "joined": "Присоединился",
"powered_by": "Работает на", "powered_by": "Работает на",
"landing_0": "Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.<14></14><15>Спасибо нашим помощникам:</15>dessalines, Nutomic, asonix, zacanger, и iav.",
"not_logged_in": "Не авторизованы.", "not_logged_in": "Не авторизованы.",
"community_ban": "Вы были заблокированы на данном сообществе.", "community_ban": "Вы были заблокированы на данном сообществе.",
"site_ban": "Вы были заблокированы на данном сайте", "site_ban": "Вы были заблокированы на данном сайте",
@ -235,7 +235,7 @@
"matrix_user_id": "Матрица пользователя", "matrix_user_id": "Матрица пользователя",
"are_you_sure": "вы уверены?", "are_you_sure": "вы уверены?",
"archive_link": "архивировать ссылку", "archive_link": "архивировать ссылку",
"logged_in": "Войти в систему.", "logged_in": "Вошли в систему.",
"couldnt_get_comments": "Не удалось получить комментарии.", "couldnt_get_comments": "Не удалось получить комментарии.",
"from": "от", "from": "от",
"transfer_site": "трансфер сайт", "transfer_site": "трансфер сайт",
@ -265,5 +265,6 @@
"monero": "Monero", "monero": "Monero",
"emoji_picker": "Сборщик эмодзи", "emoji_picker": "Сборщик эмодзи",
"select_a_community": "Выбрать сообщество", "select_a_community": "Выбрать сообщество",
"invalid_username": "Неверное имя пользователя." "invalid_username": "Неверное имя пользователя.",
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать."
} }

Some files were not shown because too many files have changed in this diff Show more