Merge branch 'main' into fix_config_location
This commit is contained in:
commit
cc84316600
103 changed files with 5218 additions and 3639 deletions
6
README.md
vendored
6
README.md
vendored
|
@ -1,7 +1,7 @@
|
|||
<div align="center">
|
||||
|
||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
|
||||
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy)
|
||||
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=main)](https://travis-ci.org/LemmyNet/lemmy)
|
||||
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
||||
[![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
|
||||
|
@ -26,7 +26,7 @@
|
|||
·
|
||||
<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>
|
||||
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
Front Page|Post
|
||||
---|---
|
||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
|
||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
|
||||
|
||||
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
|||
v0.7.20
|
||||
v0.7.26
|
||||
|
|
8
ansible/templates/nginx.conf
vendored
8
ansible/templates/nginx.conf
vendored
|
@ -1,4 +1,3 @@
|
|||
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
|
||||
limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s;
|
||||
|
||||
server {
|
||||
|
@ -65,13 +64,6 @@ server {
|
|||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Proxy Cache
|
||||
proxy_cache lemmy_frontend_cache;
|
||||
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_min_uses 5;
|
||||
}
|
||||
|
||||
# Redirect pictshare images to pictrs
|
||||
|
|
6
docker/prod/deploy.sh
vendored
6
docker/prod/deploy.sh
vendored
|
@ -1,10 +1,10 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
git checkout master
|
||||
git checkout main
|
||||
|
||||
# Import translations
|
||||
git fetch weblate
|
||||
git merge weblate/master
|
||||
git merge weblate/main
|
||||
|
||||
# Creating the new tag
|
||||
new_tag="$1"
|
||||
|
@ -12,8 +12,6 @@ third_semver=$(echo $new_tag | cut -d "." -f 3)
|
|||
|
||||
# Setting the version on the front end
|
||||
cd ../../
|
||||
echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
|
||||
git add "ui/src/version.ts"
|
||||
# Setting the version on the backend
|
||||
echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
|
||||
git add "server/src/version.rs"
|
||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
|||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.7.20
|
||||
image: dessalines/lemmy:v0.7.26
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
|
2
docs/src/about.md
vendored
2
docs/src/about.md
vendored
|
@ -2,7 +2,7 @@
|
|||
|
||||
Front Page|Post
|
||||
---|---
|
||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
|
||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
|
||||
|
||||
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
|
|
2
docs/src/about_guide.md
vendored
2
docs/src/about_guide.md
vendored
|
@ -35,6 +35,8 @@ Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule <br><hr>
|
|||
\`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'
|
||||
::: 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/)
|
||||
|
||||
|
|
2
docs/src/about_ranking.md
vendored
2
docs/src/about_ranking.md
vendored
|
@ -26,4 +26,4 @@ Gravity = Decay gravity, 1.8 is default
|
|||
|
||||
A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k.
|
||||
|
||||
![](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/rank_algorithm.png)
|
||||
![](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/rank_algorithm.png)
|
||||
|
|
2
docs/src/administration_configuration.md
vendored
2
docs/src/administration_configuration.md
vendored
|
@ -1,7 +1,7 @@
|
|||
# Configuration
|
||||
|
||||
The configuration is based on the file
|
||||
[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/master/server/config/defaults.hjson).
|
||||
[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/main/server/config/defaults.hjson).
|
||||
This file also contains documentation for all the available options. To override the defaults, you
|
||||
can copy the options you want to change into your local `config.hjson` file.
|
||||
|
||||
|
|
2
docs/src/administration_install_ansible.md
vendored
2
docs/src/administration_install_ansible.md
vendored
|
@ -19,7 +19,7 @@ ansible-playbook lemmy.yml --become
|
|||
|
||||
To update to a new version, just run the following in your local Lemmy repo:
|
||||
```bash
|
||||
git pull origin master
|
||||
git pull origin main
|
||||
cd ansible
|
||||
ansible-playbook lemmy.yml --become
|
||||
```
|
||||
|
|
12
docs/src/administration_install_docker.md
vendored
12
docs/src/administration_install_docker.md
vendored
|
@ -8,9 +8,9 @@ mkdir /lemmy
|
|||
cd /lemmy
|
||||
|
||||
# download default config files
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/iframely.config.local.js
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/lemmy.hjson
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/iframely.config.local.js
|
||||
|
||||
# Set correct permissions for pictrs folder
|
||||
mkdir -p volumes/pictrs
|
||||
|
@ -21,10 +21,10 @@ After this, have a look at the [config file](administration_configuration.md) na
|
|||
|
||||
`docker-compose up -d`
|
||||
|
||||
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf), could be setup with:
|
||||
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf), could be setup with:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf
|
||||
# Replace the {{ vars }}
|
||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||
```
|
||||
|
@ -36,6 +36,6 @@ You will also need to setup TLS, for example with [Let's Encrypt](https://letsen
|
|||
To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
||||
|
|
49
docs/src/contributing_local_development.md
vendored
49
docs/src/contributing_local_development.md
vendored
|
@ -1,16 +1,26 @@
|
|||
### Ubuntu
|
||||
|
||||
|
||||
#### Build requirements:
|
||||
### Install build requirements
|
||||
#### Ubuntu
|
||||
```
|
||||
sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 git
|
||||
sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2
|
||||
# install yarn
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt update && sudo apt install yarn
|
||||
```
|
||||
|
||||
#### Get the source code
|
||||
#### macOS
|
||||
|
||||
Install Rust using [the recommended option on rust-lang.org](https://www.rust-lang.org/tools/install) (rustup).
|
||||
|
||||
Then, install [Homebrew](https://brew.sh/) if you don't already have it installed.
|
||||
|
||||
Finally, install Node and Yarn.
|
||||
|
||||
```
|
||||
brew install node yarn
|
||||
```
|
||||
|
||||
### Get the source code
|
||||
```
|
||||
git clone https://github.com/LemmyNet/lemmy.git
|
||||
# or alternatively from gitea
|
||||
|
@ -20,36 +30,49 @@ git clone https://github.com/LemmyNet/lemmy.git
|
|||
All the following commands need to be run either in `lemmy/server` or `lemmy/ui`, as indicated
|
||||
by the `cd` command.
|
||||
|
||||
#### Build the backend (Rust)
|
||||
### Build the backend (Rust)
|
||||
```
|
||||
cd server
|
||||
cargo build
|
||||
# for development, use `cargo check` instead)
|
||||
```
|
||||
|
||||
#### Build the frontend (Typescript)
|
||||
### Build the frontend (Typescript)
|
||||
```
|
||||
cd ui
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
#### Setup postgresql
|
||||
### Setup postgresql
|
||||
#### Ubuntu
|
||||
```
|
||||
sudo apt install postgresql
|
||||
sudo systemctl start postgresql
|
||||
# initialize postgres database
|
||||
|
||||
# Either execute server/db-init.sh, or manually initialize the postgres database:
|
||||
sudo -u postgres psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||
sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
# or execute server/db-init.sh
|
||||
```
|
||||
|
||||
#### Run a local development instance
|
||||
#### macOS
|
||||
```
|
||||
brew install postgresql
|
||||
brew services start postgresql
|
||||
/usr/local/opt/postgres/bin/createuser -s postgres
|
||||
|
||||
# Either execute server/db-init.sh, or manually initialize the postgres database:
|
||||
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
```
|
||||
|
||||
### Run a local development instance
|
||||
```
|
||||
# run each of these in a seperate terminal
|
||||
cd server && cargo run
|
||||
ui & yarn start
|
||||
cd ui && yarn start
|
||||
```
|
||||
|
||||
Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit
|
||||
|
|
579
docs/src/contributing_websocket_http_api.md
vendored
579
docs/src/contributing_websocket_http_api.md
vendored
|
@ -17,6 +17,7 @@
|
|||
- [Errors](#errors)
|
||||
- [API documentation](#api-documentation)
|
||||
* [Sort Types](#sort-types)
|
||||
* [Undoing actions](#undoing-actions)
|
||||
* [Websocket vs HTTP](#websocket-vs-http)
|
||||
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
|
||||
+ [Login](#login)
|
||||
|
@ -43,142 +44,198 @@
|
|||
- [Request](#request-5)
|
||||
- [Response](#response-5)
|
||||
- [HTTP](#http-6)
|
||||
+ [Edit User Mention](#edit-user-mention)
|
||||
+ [Mark User Mention as read](#mark-user-mention-as-read)
|
||||
- [Request](#request-6)
|
||||
- [Response](#response-6)
|
||||
- [HTTP](#http-7)
|
||||
+ [Mark All As Read](#mark-all-as-read)
|
||||
+ [Get Private Messages](#get-private-messages)
|
||||
- [Request](#request-7)
|
||||
- [Response](#response-7)
|
||||
- [HTTP](#http-8)
|
||||
+ [Delete Account](#delete-account)
|
||||
+ [Create Private Message](#create-private-message)
|
||||
- [Request](#request-8)
|
||||
- [Response](#response-8)
|
||||
- [HTTP](#http-9)
|
||||
+ [Add admin](#add-admin)
|
||||
+ [Edit Private Message](#edit-private-message)
|
||||
- [Request](#request-9)
|
||||
- [Response](#response-9)
|
||||
- [HTTP](#http-10)
|
||||
+ [Ban user](#ban-user)
|
||||
+ [Delete Private Message](#delete-private-message)
|
||||
- [Request](#request-10)
|
||||
- [Response](#response-10)
|
||||
- [HTTP](#http-11)
|
||||
* [Site](#site)
|
||||
+ [List Categories](#list-categories)
|
||||
+ [Mark Private Message as Read](#mark-private-message-as-read)
|
||||
- [Request](#request-11)
|
||||
- [Response](#response-11)
|
||||
- [HTTP](#http-12)
|
||||
+ [Search](#search)
|
||||
+ [Mark All As Read](#mark-all-as-read)
|
||||
- [Request](#request-12)
|
||||
- [Response](#response-12)
|
||||
- [HTTP](#http-13)
|
||||
+ [Get Modlog](#get-modlog)
|
||||
+ [Delete Account](#delete-account)
|
||||
- [Request](#request-13)
|
||||
- [Response](#response-13)
|
||||
- [HTTP](#http-14)
|
||||
+ [Create Site](#create-site)
|
||||
+ [Add admin](#add-admin)
|
||||
- [Request](#request-14)
|
||||
- [Response](#response-14)
|
||||
- [HTTP](#http-15)
|
||||
+ [Edit Site](#edit-site)
|
||||
+ [Ban user](#ban-user)
|
||||
- [Request](#request-15)
|
||||
- [Response](#response-15)
|
||||
- [HTTP](#http-16)
|
||||
+ [Get Site](#get-site)
|
||||
* [Site](#site)
|
||||
+ [List Categories](#list-categories)
|
||||
- [Request](#request-16)
|
||||
- [Response](#response-16)
|
||||
- [HTTP](#http-17)
|
||||
+ [Transfer Site](#transfer-site)
|
||||
+ [Search](#search)
|
||||
- [Request](#request-17)
|
||||
- [Response](#response-17)
|
||||
- [HTTP](#http-18)
|
||||
+ [Get Site Config](#get-site-config)
|
||||
+ [Get Modlog](#get-modlog)
|
||||
- [Request](#request-18)
|
||||
- [Response](#response-18)
|
||||
- [HTTP](#http-19)
|
||||
+ [Save Site Config](#save-site-config)
|
||||
+ [Create Site](#create-site)
|
||||
- [Request](#request-19)
|
||||
- [Response](#response-19)
|
||||
- [HTTP](#http-20)
|
||||
* [Community](#community)
|
||||
+ [Get Community](#get-community)
|
||||
+ [Edit Site](#edit-site)
|
||||
- [Request](#request-20)
|
||||
- [Response](#response-20)
|
||||
- [HTTP](#http-21)
|
||||
+ [Create Community](#create-community)
|
||||
+ [Get Site](#get-site)
|
||||
- [Request](#request-21)
|
||||
- [Response](#response-21)
|
||||
- [HTTP](#http-22)
|
||||
+ [List Communities](#list-communities)
|
||||
+ [Transfer Site](#transfer-site)
|
||||
- [Request](#request-22)
|
||||
- [Response](#response-22)
|
||||
- [HTTP](#http-23)
|
||||
+ [Ban from Community](#ban-from-community)
|
||||
+ [Get Site Config](#get-site-config)
|
||||
- [Request](#request-23)
|
||||
- [Response](#response-23)
|
||||
- [HTTP](#http-24)
|
||||
+ [Add Mod to Community](#add-mod-to-community)
|
||||
+ [Save Site Config](#save-site-config)
|
||||
- [Request](#request-24)
|
||||
- [Response](#response-24)
|
||||
- [HTTP](#http-25)
|
||||
+ [Edit Community](#edit-community)
|
||||
* [Community](#community)
|
||||
+ [Get Community](#get-community)
|
||||
- [Request](#request-25)
|
||||
- [Response](#response-25)
|
||||
- [HTTP](#http-26)
|
||||
+ [Follow Community](#follow-community)
|
||||
+ [Create Community](#create-community)
|
||||
- [Request](#request-26)
|
||||
- [Response](#response-26)
|
||||
- [HTTP](#http-27)
|
||||
+ [Get Followed Communities](#get-followed-communities)
|
||||
+ [List Communities](#list-communities)
|
||||
- [Request](#request-27)
|
||||
- [Response](#response-27)
|
||||
- [HTTP](#http-28)
|
||||
+ [Transfer Community](#transfer-community)
|
||||
+ [Ban from Community](#ban-from-community)
|
||||
- [Request](#request-28)
|
||||
- [Response](#response-28)
|
||||
- [HTTP](#http-29)
|
||||
* [Post](#post)
|
||||
+ [Create Post](#create-post)
|
||||
+ [Add Mod to Community](#add-mod-to-community)
|
||||
- [Request](#request-29)
|
||||
- [Response](#response-29)
|
||||
- [HTTP](#http-30)
|
||||
+ [Get Post](#get-post)
|
||||
+ [Edit Community](#edit-community)
|
||||
- [Request](#request-30)
|
||||
- [Response](#response-30)
|
||||
- [HTTP](#http-31)
|
||||
+ [Get Posts](#get-posts)
|
||||
+ [Delete Community](#delete-community)
|
||||
- [Request](#request-31)
|
||||
- [Response](#response-31)
|
||||
- [HTTP](#http-32)
|
||||
+ [Create Post Like](#create-post-like)
|
||||
+ [Remove Community](#remove-community)
|
||||
- [Request](#request-32)
|
||||
- [Response](#response-32)
|
||||
- [HTTP](#http-33)
|
||||
+ [Edit Post](#edit-post)
|
||||
+ [Follow Community](#follow-community)
|
||||
- [Request](#request-33)
|
||||
- [Response](#response-33)
|
||||
- [HTTP](#http-34)
|
||||
+ [Save Post](#save-post)
|
||||
+ [Get Followed Communities](#get-followed-communities)
|
||||
- [Request](#request-34)
|
||||
- [Response](#response-34)
|
||||
- [HTTP](#http-35)
|
||||
* [Comment](#comment)
|
||||
+ [Create Comment](#create-comment)
|
||||
+ [Transfer Community](#transfer-community)
|
||||
- [Request](#request-35)
|
||||
- [Response](#response-35)
|
||||
- [HTTP](#http-36)
|
||||
+ [Edit Comment](#edit-comment)
|
||||
* [Post](#post)
|
||||
+ [Create Post](#create-post)
|
||||
- [Request](#request-36)
|
||||
- [Response](#response-36)
|
||||
- [HTTP](#http-37)
|
||||
+ [Save Comment](#save-comment)
|
||||
+ [Get Post](#get-post)
|
||||
- [Request](#request-37)
|
||||
- [Response](#response-37)
|
||||
- [HTTP](#http-38)
|
||||
+ [Create Comment Like](#create-comment-like)
|
||||
+ [Get Posts](#get-posts)
|
||||
- [Request](#request-38)
|
||||
- [Response](#response-38)
|
||||
- [HTTP](#http-39)
|
||||
+ [Create Post Like](#create-post-like)
|
||||
- [Request](#request-39)
|
||||
- [Response](#response-39)
|
||||
- [HTTP](#http-40)
|
||||
+ [Edit Post](#edit-post)
|
||||
- [Request](#request-40)
|
||||
- [Response](#response-40)
|
||||
- [HTTP](#http-41)
|
||||
+ [Delete Post](#delete-post)
|
||||
- [Request](#request-41)
|
||||
- [Response](#response-41)
|
||||
- [HTTP](#http-42)
|
||||
+ [Remove Post](#remove-post)
|
||||
- [Request](#request-42)
|
||||
- [Response](#response-42)
|
||||
- [HTTP](#http-43)
|
||||
+ [Lock Post](#lock-post)
|
||||
- [Request](#request-43)
|
||||
- [Response](#response-43)
|
||||
- [HTTP](#http-44)
|
||||
+ [Sticky Post](#sticky-post)
|
||||
- [Request](#request-44)
|
||||
- [Response](#response-44)
|
||||
- [HTTP](#http-45)
|
||||
+ [Save Post](#save-post)
|
||||
- [Request](#request-45)
|
||||
- [Response](#response-45)
|
||||
- [HTTP](#http-46)
|
||||
* [Comment](#comment)
|
||||
+ [Create Comment](#create-comment)
|
||||
- [Request](#request-46)
|
||||
- [Response](#response-46)
|
||||
- [HTTP](#http-47)
|
||||
+ [Edit Comment](#edit-comment)
|
||||
- [Request](#request-47)
|
||||
- [Response](#response-47)
|
||||
- [HTTP](#http-48)
|
||||
+ [Delete Comment](#delete-comment)
|
||||
- [Request](#request-48)
|
||||
- [Response](#response-48)
|
||||
- [HTTP](#http-49)
|
||||
+ [Remove Comment](#remove-comment)
|
||||
- [Request](#request-49)
|
||||
- [Response](#response-49)
|
||||
- [HTTP](#http-50)
|
||||
+ [Mark Comment as Read](#mark-comment-as-read)
|
||||
- [Request](#request-50)
|
||||
- [Response](#response-50)
|
||||
- [HTTP](#http-51)
|
||||
+ [Save Comment](#save-comment)
|
||||
- [Request](#request-51)
|
||||
- [Response](#response-51)
|
||||
- [HTTP](#http-52)
|
||||
+ [Create Comment Like](#create-comment-like)
|
||||
- [Request](#request-52)
|
||||
- [Response](#response-52)
|
||||
- [HTTP](#http-53)
|
||||
* [RSS / Atom feeds](#rss--atom-feeds)
|
||||
+ [All](#all)
|
||||
+ [Community](#community-1)
|
||||
|
@ -281,6 +338,10 @@ These go wherever there is a `sort` field. The available sort types are:
|
|||
- `TopYear` - the most upvoted posts/communities of the current year.
|
||||
- `TopAll` - the most upvoted posts/communities on the current instance.
|
||||
|
||||
### Undoing actions
|
||||
|
||||
Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`.
|
||||
|
||||
### Websocket vs HTTP
|
||||
|
||||
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
|
||||
|
@ -464,14 +525,17 @@ Only the first user will be able to be the admin.
|
|||
|
||||
`GET /user/mentions`
|
||||
|
||||
#### Edit User Mention
|
||||
#### Mark User Mention as read
|
||||
|
||||
Only the recipient can do this.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "EditUserMention",
|
||||
op: "MarkUserMentionAsRead",
|
||||
data: {
|
||||
user_mention_id: i32,
|
||||
read: Option<bool>,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
|
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
|
|||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "EditUserMention",
|
||||
op: "MarkUserMentionAsRead",
|
||||
data: {
|
||||
mention: UserMentionView,
|
||||
}
|
||||
|
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
|
|||
```
|
||||
##### HTTP
|
||||
|
||||
`PUT /user/mention`
|
||||
`POST /user/mention/mark_as_read`
|
||||
|
||||
#### Get Private Messages
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "GetPrivateMessages",
|
||||
data: {
|
||||
unread_only: bool,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "GetPrivateMessages",
|
||||
data: {
|
||||
messages: Vec<PrivateMessageView>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`GET /private_message/list`
|
||||
|
||||
#### Create Private Message
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "CreatePrivateMessage",
|
||||
data: {
|
||||
content: String,
|
||||
recipient_id: i32,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "CreatePrivateMessage",
|
||||
data: {
|
||||
message: PrivateMessageView,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /private_message`
|
||||
|
||||
#### Edit Private Message
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "EditPrivateMessage",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
content: String,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "EditPrivateMessage",
|
||||
data: {
|
||||
message: PrivateMessageView,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`PUT /private_message`
|
||||
|
||||
#### Delete Private Message
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "DeletePrivateMessage",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "DeletePrivateMessage",
|
||||
data: {
|
||||
message: PrivateMessageView,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /private_message/delete`
|
||||
|
||||
#### Mark Private Message as Read
|
||||
|
||||
Only the recipient can do this.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "MarkPrivateMessageAsRead",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "MarkPrivateMessageAsRead",
|
||||
data: {
|
||||
message: PrivateMessageView,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /private_message/mark_as_read`
|
||||
|
||||
#### Mark All As Read
|
||||
|
||||
|
@ -754,6 +952,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
site: Option<SiteView>,
|
||||
admins: 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: {
|
||||
community: CommunityView,
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
admins: Vec<UserView>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -971,7 +1170,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
`POST /community/mod`
|
||||
|
||||
#### Edit Community
|
||||
Mods and admins can remove and lock a community, creators can delete it.
|
||||
Only mods can edit a community.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
|
@ -979,14 +1178,9 @@ Mods and admins can remove and lock a community, creators can delete it.
|
|||
op: "EditCommunity",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
name: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
category_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
reason: Option<String>,
|
||||
expires: Option<i64>,
|
||||
auth: String
|
||||
}
|
||||
}
|
||||
|
@ -1004,6 +1198,62 @@ Mods and admins can remove and lock a community, creators can delete it.
|
|||
|
||||
`PUT /community`
|
||||
|
||||
#### Delete Community
|
||||
Only a creator can delete a community
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "DeleteCommunity",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "DeleteCommunity",
|
||||
data: {
|
||||
community: CommunityView
|
||||
}
|
||||
}
|
||||
```
|
||||
##### HTTP
|
||||
|
||||
`POST /community/delete`
|
||||
|
||||
#### Remove Community
|
||||
Only admins can remove a community.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "RemoveCommunity",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
removed: bool,
|
||||
reason: Option<String>,
|
||||
expires: Option<i64>,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "RemoveCommunity",
|
||||
data: {
|
||||
community: CommunityView
|
||||
}
|
||||
}
|
||||
```
|
||||
##### HTTP
|
||||
|
||||
`POST /community/remove`
|
||||
|
||||
#### Follow Community
|
||||
##### Request
|
||||
```rust
|
||||
|
@ -1089,8 +1339,9 @@ Mods and admins can remove and lock a community, creators can delete it.
|
|||
name: String,
|
||||
url: Option<String>,
|
||||
body: Option<String>,
|
||||
nsfw: bool,
|
||||
community_id: i32,
|
||||
auth: String
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -1127,7 +1378,6 @@ Mods and admins can remove and lock a community, creators can delete it.
|
|||
comments: Vec<CommentView>,
|
||||
community: CommunityView,
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
admins: Vec<UserView>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -1196,25 +1446,17 @@ Post listing types are `All, Subscribed, Community`
|
|||
`POST /post/like`
|
||||
|
||||
#### Edit Post
|
||||
|
||||
Mods and admins can remove and lock a post, creators can delete it.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "EditPost",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
creator_id: i32,
|
||||
community_id: i32,
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
body: Option<String>,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
locked: Option<bool>,
|
||||
reason: Option<String>,
|
||||
auth: String
|
||||
nsfw: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -1232,6 +1474,120 @@ Mods and admins can remove and lock a post, creators can delete it.
|
|||
|
||||
`PUT /post`
|
||||
|
||||
#### Delete Post
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "DeletePost",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "DeletePost",
|
||||
data: {
|
||||
post: PostView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /post/delete`
|
||||
|
||||
#### Remove Post
|
||||
|
||||
Only admins and mods can remove a post.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "RemovePost",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
removed: bool,
|
||||
reason: Option<String>,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "RemovePost",
|
||||
data: {
|
||||
post: PostView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /post/remove`
|
||||
|
||||
#### Lock Post
|
||||
|
||||
Only admins and mods can lock a post.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "LockPost",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
locked: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "LockPost",
|
||||
data: {
|
||||
post: PostView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /post/lock`
|
||||
|
||||
#### Sticky Post
|
||||
|
||||
Only admins and mods can sticky a post.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "StickyPost",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
stickied: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "StickyPost",
|
||||
data: {
|
||||
post: PostView
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### HTTP
|
||||
|
||||
`POST /post/sticky`
|
||||
|
||||
#### Save Post
|
||||
##### Request
|
||||
```rust
|
||||
|
@ -1266,8 +1622,8 @@ Mods and admins can remove and lock a post, creators can delete it.
|
|||
data: {
|
||||
content: String,
|
||||
parent_id: Option<i32>,
|
||||
edit_id: Option<i32>,
|
||||
post_id: i32,
|
||||
form_id: Option<String>, // An optional form id, so you know which message came back
|
||||
auth: String
|
||||
}
|
||||
}
|
||||
|
@ -1288,7 +1644,7 @@ Mods and admins can remove and lock a post, creators can delete it.
|
|||
|
||||
#### Edit Comment
|
||||
|
||||
Mods and admins can remove a comment, creators can delete it.
|
||||
Only the creator can edit the comment.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
|
@ -1296,15 +1652,9 @@ Mods and admins can remove a comment, creators can delete it.
|
|||
op: "EditComment",
|
||||
data: {
|
||||
content: String,
|
||||
parent_id: Option<i32>,
|
||||
edit_id: i32,
|
||||
creator_id: i32,
|
||||
post_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
reason: Option<String>,
|
||||
read: Option<bool>,
|
||||
auth: String
|
||||
form_id: Option<String>,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -1321,6 +1671,92 @@ Mods and admins can remove a comment, creators can delete it.
|
|||
|
||||
`PUT /comment`
|
||||
|
||||
#### Delete Comment
|
||||
|
||||
Only the creator can delete the comment.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "DeleteComment",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "DeleteComment",
|
||||
data: {
|
||||
comment: CommentView
|
||||
}
|
||||
}
|
||||
```
|
||||
##### HTTP
|
||||
|
||||
`POST /comment/delete`
|
||||
|
||||
|
||||
#### Remove Comment
|
||||
|
||||
Only a mod or admin can remove the comment.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "RemoveComment",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
removed: bool,
|
||||
reason: Option<String>,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "RemoveComment",
|
||||
data: {
|
||||
comment: CommentView
|
||||
}
|
||||
}
|
||||
```
|
||||
##### HTTP
|
||||
|
||||
`POST /comment/remove`
|
||||
|
||||
#### Mark Comment as Read
|
||||
|
||||
Only the recipient can do this.
|
||||
|
||||
##### Request
|
||||
```rust
|
||||
{
|
||||
op: "MarkCommentAsRead",
|
||||
data: {
|
||||
edit_id: i32,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Response
|
||||
```rust
|
||||
{
|
||||
op: "MarkCommentAsRead",
|
||||
data: {
|
||||
comment: CommentView
|
||||
}
|
||||
}
|
||||
```
|
||||
##### HTTP
|
||||
|
||||
`POST /comment/mark_as_read`
|
||||
|
||||
#### Save Comment
|
||||
##### Request
|
||||
```rust
|
||||
|
@ -1356,7 +1792,6 @@ Mods and admins can remove a comment, creators can delete it.
|
|||
op: "CreateCommentLike",
|
||||
data: {
|
||||
comment_id: i32,
|
||||
post_id: i32,
|
||||
score: i16,
|
||||
auth: String
|
||||
}
|
||||
|
|
355
server/Cargo.lock
generated
vendored
355
server/Cargo.lock
generated
vendored
|
@ -1,35 +1,9 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "activitystreams"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "464cb473bfb402b857cc15b1153974c203a43f1485da4dda15cd17a738548958"
|
||||
dependencies = [
|
||||
"activitystreams-derive",
|
||||
"chrono",
|
||||
"mime",
|
||||
"serde 1.0.114",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activitystreams-derive"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activitystreams-ext"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.asonix.dog/asonix/activitystreams-ext#e5c97f4ea9f60e49bc7ff27fb0fb515d3190fd25"
|
||||
source = "git+https://yerbamate.dev/asonix/activitystreams-ext?branch=main#2799a4c606467a2f577e1f45f93c6828ec83cfdf"
|
||||
dependencies = [
|
||||
"activitystreams-new",
|
||||
"serde 1.0.114",
|
||||
|
@ -39,12 +13,14 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "activitystreams-new"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.asonix.dog/asonix/activitystreams-sketch#99c7e9aa5596eda846a1ebd5978ca72d11d4c08a"
|
||||
source = "git+https://yerbamate.dev/asonix/activitystreams-new?branch=main#857d5167dfa13054dd0d21d3d54f8147eea0d546"
|
||||
dependencies = [
|
||||
"activitystreams",
|
||||
"chrono",
|
||||
"mime",
|
||||
"serde 1.0.114",
|
||||
"serde_json",
|
||||
"typed-builder",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -111,9 +87,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-files"
|
||||
version = "0.3.0-alpha.1"
|
||||
version = "0.3.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b32e0fdd5998c2712549cbc39dff46c8754d55e3dd9f4d017d9e28de30cac6"
|
||||
checksum = "627f597ad98061816766201db8afc7444752992f2919b2e60f53a7fa27f01aed"
|
||||
dependencies = [
|
||||
"actix-http",
|
||||
"actix-service",
|
||||
|
@ -132,9 +108,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "2.0.0-alpha.4"
|
||||
version = "2.0.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd7ea0568480d199952a51de70271946da57c33cc0e8b83f54383e70958dff21"
|
||||
checksum = "33f501768e82e8548763b7f55309e2f8bcc7f9f4273c75b47af99ac2b2581f37"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-connect",
|
||||
|
@ -147,6 +123,7 @@ dependencies = [
|
|||
"bitflags",
|
||||
"brotli2",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"copyless",
|
||||
"derive_more",
|
||||
"either",
|
||||
|
@ -160,6 +137,7 @@ dependencies = [
|
|||
"http",
|
||||
"httparse",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -171,7 +149,7 @@ dependencies = [
|
|||
"serde 1.0.114",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha-1",
|
||||
"sha-1 0.9.1",
|
||||
"slab",
|
||||
"time 0.2.16",
|
||||
]
|
||||
|
@ -260,16 +238,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-threadpool"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff"
|
||||
checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"futures-channel",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"num_cpus",
|
||||
"parking_lot 0.10.2",
|
||||
"parking_lot 0.11.0",
|
||||
"threadpool",
|
||||
]
|
||||
|
||||
|
@ -313,9 +291,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "3.0.0-alpha.3"
|
||||
version = "3.0.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bd6df56ec5f9a1a0d8335f156f36e1e8f76dbd736fa0cc0f6bc3a69be1e6124"
|
||||
checksum = "9125c29b7d9911bfdb4d0d4d8f1cf4fee4f21515cf2a405a423c30c245364297"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
@ -353,24 +331,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "actix-web-actors"
|
||||
version = "3.0.0-alpha.1"
|
||||
version = "3.0.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b5efeb3907582f9c724ce27be093ab8aafabd97be828bc6750c0d467f5e1aa3"
|
||||
checksum = "55ef22b33c49a28dda61866d5573c5b8ceb080a099cd59e7371b78b48bbf1bc0"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-web",
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"pin-project",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766"
|
||||
checksum = "df9679f5b1f4c819de08b63b0a61a131b2fdc30b367c2c208984fda8eaa07fa0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -390,24 +369,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.12.2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
|
||||
checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10"
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
|
||||
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
|
@ -481,9 +454,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
|||
|
||||
[[package]]
|
||||
name = "awc"
|
||||
version = "2.0.0-alpha.2"
|
||||
version = "2.0.0-beta.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7038a9747cd5159b9f0550895eaf865c0143baa7e4eee834e9294d0a7e0e4be"
|
||||
checksum = "374057b508d4083208996be82141891c2e14c8885f45991b21c1621200ab6df3"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
|
@ -505,14 +478,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.49"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
|
||||
checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.3.7",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
@ -590,7 +563,7 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"generic-array 0.14.2",
|
||||
"generic-array 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -599,7 +572,7 @@ version = "0.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
|
||||
dependencies = [
|
||||
"generic-array 0.14.2",
|
||||
"generic-array 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -642,6 +615,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buf-min"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bufstream"
|
||||
version = "0.1.4"
|
||||
|
@ -668,12 +650,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
|
|||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b"
|
||||
dependencies = [
|
||||
"loom",
|
||||
]
|
||||
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
|
@ -686,9 +665,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe"
|
||||
checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
|
@ -698,9 +677,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4"
|
||||
checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits 0.2.12",
|
||||
|
@ -770,6 +749,16 @@ dependencies = [
|
|||
"serde-hjson",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca761767cf3fa9068cc893ec8c247a22d0fd0535848e65640c0548bd1f8bbb36"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copyless"
|
||||
version = "0.1.5"
|
||||
|
@ -794,9 +783,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
|
|||
|
||||
[[package]]
|
||||
name = "cpuid-bool"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4"
|
||||
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
|
@ -809,12 +798,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
|
||||
checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -950,7 +939,7 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array 0.14.2",
|
||||
"generic-array 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1142,7 +1131,7 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"crc32fast",
|
||||
"libc",
|
||||
"miniz_oxide 0.4.0",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1292,19 +1281,6 @@ dependencies = [
|
|||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"log",
|
||||
"rustc_version",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.3"
|
||||
|
@ -1316,9 +1292,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.2"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
|
||||
checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check 0.9.2",
|
||||
|
@ -1337,15 +1313,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
|
||||
checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff"
|
||||
checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
|
@ -1354,10 +1330,19 @@ dependencies = [
|
|||
"futures-util",
|
||||
"http",
|
||||
"indexmap",
|
||||
"log",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util 0.3.1",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1371,9 +1356,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
|
||||
checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -1472,18 +1457,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
|
||||
checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
|
||||
dependencies = [
|
||||
"autocfg 1.0.0",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69da7ce1490173c2bf4d26bc8be429aaeeaf4cce6c4b970b7949651fa17655fe"
|
||||
checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485"
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
|
@ -1523,9 +1509,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.41"
|
||||
version = "0.3.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916"
|
||||
checksum = "52732a3d3ad72c58ad2dc70624f9c17b46ecd0943b9a4f1ee37c4c18c5d983e2"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
@ -1573,19 +1559,21 @@ dependencies = [
|
|||
"bcrypt",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"regex",
|
||||
"serde 1.0.114",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lemmy_server"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"activitystreams",
|
||||
"activitystreams-ext",
|
||||
"activitystreams-new",
|
||||
"actix",
|
||||
|
@ -1693,9 +1681,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.71"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
|
||||
checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
|
@ -1724,33 +1712,22 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de302ce1fe7482db13738fbaf2e21cfb06a986b89c0bf38d88abf16681aada4e"
|
||||
checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.8"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
|
||||
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
|
@ -1778,12 +1755,6 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.3"
|
||||
|
@ -1827,15 +1798,6 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.0"
|
||||
|
@ -2060,7 +2022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api 0.4.0",
|
||||
"lock_api 0.4.1",
|
||||
"parking_lot_core 0.8.0",
|
||||
]
|
||||
|
||||
|
@ -2150,7 +2112,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
|
|||
dependencies = [
|
||||
"maplit",
|
||||
"pest",
|
||||
"sha-1",
|
||||
"sha-1 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2187,9 +2149,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
|
||||
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
|
@ -2220,9 +2182,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.18"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
|
||||
checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
@ -2434,9 +2396,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.56"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
|
||||
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
|
@ -2559,12 +2521,6 @@ dependencies = [
|
|||
"parking_lot 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -2703,6 +2659,19 @@ dependencies = [
|
|||
"opaque-debug 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if",
|
||||
"cpuid-bool",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.6.0"
|
||||
|
@ -2734,9 +2703,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618"
|
||||
checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"num-bigint",
|
||||
|
@ -2751,9 +2720,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
|||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
|
||||
checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
|
@ -2869,9 +2838,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.33"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
|
||||
checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3076,6 +3045,26 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2e2a2de6b0d5cbb13fc21193a2296888eaab62b6044479aafb3c54c01c29fcd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ae75f0d28ae10786f3b1895c55fe72e79928fd5ccdebb5438c75e93fec178f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-proto"
|
||||
version = "0.19.5"
|
||||
|
@ -3132,17 +3121,6 @@ version = "1.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fc4459191c621a53ef6c6ca5642e6e0e5ccc61f3e5b8ad6b6ab5317f0200fb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.12.0"
|
||||
|
@ -3251,18 +3229,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "v_escape"
|
||||
version = "0.7.4"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
|
||||
checksum = "b66158ce426982197fd44266d68125fd4000f1d42f5ee33ef02b500b4b6b0024"
|
||||
dependencies = [
|
||||
"buf-min",
|
||||
"v_escape_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v_escape_derive"
|
||||
version = "0.5.6"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
|
||||
checksum = "cae7cffca0b1f9af9b20610f6fdeee9ffcce61417b5ad186a5d482dc904e24cd"
|
||||
dependencies = [
|
||||
"nom 4.2.3",
|
||||
"proc-macro2",
|
||||
|
@ -3272,9 +3251,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "v_htmlescape"
|
||||
version = "0.4.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
|
||||
checksum = "f5fd25529cb2f78527b5ee507bcfb357b26d057b5e480853c26d49a4ead5c629"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"v_escape",
|
||||
|
@ -3312,9 +3291,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.64"
|
||||
version = "0.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2"
|
||||
checksum = "f3edbcc9536ab7eababcc6d2374a0b7bfe13a2b6d562c5e07f370456b1a8f33d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
|
@ -3322,9 +3301,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.64"
|
||||
version = "0.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df"
|
||||
checksum = "89ed2fb8c84bfad20ea66b26a3743f3e7ba8735a69fe7d95118c33ec8fc1244d"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
|
@ -3337,9 +3316,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.64"
|
||||
version = "0.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8"
|
||||
checksum = "eb071268b031a64d92fc6cf691715ca5a40950694d8f683c5bb43db7c730929e"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
@ -3347,9 +3326,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.64"
|
||||
version = "0.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75"
|
||||
checksum = "cf592c807080719d1ff2f245a687cbadb3ed28b2077ed7084b47aba8b691f2c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3360,15 +3339,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.64"
|
||||
version = "0.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae"
|
||||
checksum = "72b6c0220ded549d63860c78c38f3bcc558d1ca3f4efa74942c536ddbbb55e87"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.41"
|
||||
version = "0.3.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d"
|
||||
checksum = "8be2398f326b7ba09815d0b403095f34dd708579220d099caae89be0b32137b2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
5
server/Cargo.toml
vendored
5
server/Cargo.toml
vendored
|
@ -18,9 +18,8 @@ lemmy_db = { path = "./lemmy_db" }
|
|||
diesel = "1.4.4"
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.15.0"
|
||||
activitystreams = "0.6.2"
|
||||
activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" }
|
||||
activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
|
||||
activitystreams-new = { git = "https://yerbamate.dev/asonix/activitystreams-new", branch = "main" }
|
||||
activitystreams-ext = { git = "https://yerbamate.dev/asonix/activitystreams-ext", branch = "main" }
|
||||
bcrypt = "0.8.0"
|
||||
chrono = { version = "0.4.7", features = ["serde"] }
|
||||
serde_json = { version = "1.0.52", features = ["preserve_order"]}
|
||||
|
|
3
server/lemmy_db/Cargo.toml
vendored
3
server/lemmy_db/Cargo.toml
vendored
|
@ -13,3 +13,6 @@ strum_macros = "0.18.0"
|
|||
log = "0.4.0"
|
||||
sha2 = "0.9"
|
||||
bcrypt = "0.8.0"
|
||||
url = { version = "2.1.1", features = ["serde"] }
|
||||
lazy_static = "1.3.0"
|
||||
regex = "1.3.5"
|
||||
|
|
|
@ -117,7 +117,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_862362".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::{post::Post, *};
|
||||
use crate::schema::{comment, comment_like, comment_saved};
|
||||
use url::{ParseError, Url};
|
||||
|
||||
// WITH RECURSIVE MyTree AS (
|
||||
// SELECT * FROM comment WHERE parent_id IS NULL
|
||||
|
@ -42,6 +43,12 @@ pub struct CommentForm {
|
|||
pub local: bool,
|
||||
}
|
||||
|
||||
impl CommentForm {
|
||||
pub fn get_ap_id(&self) -> Result<Url, ParseError> {
|
||||
Url::parse(&self.ap_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud<CommentForm> for Comment {
|
||||
fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
|
@ -90,14 +97,6 @@ impl Comment {
|
|||
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set(read.eq(true))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
|
||||
|
@ -109,6 +108,46 @@ impl Comment {
|
|||
))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_deleted(
|
||||
conn: &PgConnection,
|
||||
comment_id: i32,
|
||||
new_deleted: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set(deleted.eq(new_deleted))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_removed(
|
||||
conn: &PgConnection,
|
||||
comment_id: i32,
|
||||
new_removed: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set(removed.eq(new_removed))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set(read.eq(new_read))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_content(
|
||||
conn: &PgConnection,
|
||||
comment_id: i32,
|
||||
new_content: &str,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::comment::dsl::*;
|
||||
diesel::update(comment.find(comment_id))
|
||||
.set((content.eq(new_content), updated.eq(naive_now())))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
|
||||
|
@ -226,7 +265,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_283687".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -246,7 +285,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_928738972".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -498,7 +498,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_92873982".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -518,7 +518,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_7625376".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
naive_now,
|
||||
schema::{community, community_follower, community_moderator, community_user_ban},
|
||||
Bannable,
|
||||
Crud,
|
||||
|
@ -29,7 +30,6 @@ pub struct Community {
|
|||
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// TODO add better delete, remove, lock actions here.
|
||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
||||
#[table_name = "community"]
|
||||
pub struct CommunityForm {
|
||||
|
@ -88,10 +88,10 @@ impl Community {
|
|||
.first::<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::*;
|
||||
community
|
||||
.filter(actor_id.eq(community_id))
|
||||
.filter(actor_id.eq(for_actor_id))
|
||||
.first::<Self>(conn)
|
||||
}
|
||||
|
||||
|
@ -99,6 +99,60 @@ impl Community {
|
|||
use crate::schema::community::dsl::*;
|
||||
community.filter(local.eq(true)).load::<Community>(conn)
|
||||
}
|
||||
|
||||
pub fn update_deleted(
|
||||
conn: &PgConnection,
|
||||
community_id: i32,
|
||||
new_deleted: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community::dsl::*;
|
||||
diesel::update(community.find(community_id))
|
||||
.set(deleted.eq(new_deleted))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_removed(
|
||||
conn: &PgConnection,
|
||||
community_id: i32,
|
||||
new_removed: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community::dsl::*;
|
||||
diesel::update(community.find(community_id))
|
||||
.set(removed.eq(new_removed))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_creator(
|
||||
conn: &PgConnection,
|
||||
community_id: i32,
|
||||
new_creator_id: i32,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community::dsl::*;
|
||||
diesel::update(community.find(community_id))
|
||||
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
fn community_mods_and_admins(
|
||||
conn: &PgConnection,
|
||||
community_id: i32,
|
||||
) -> Result<Vec<i32>, Error> {
|
||||
use crate::{community_view::CommunityModeratorView, user_view::UserView};
|
||||
let mut mods_and_admins: Vec<i32> = Vec::new();
|
||||
mods_and_admins.append(
|
||||
&mut CommunityModeratorView::for_community(conn, community_id)
|
||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())?,
|
||||
);
|
||||
mods_and_admins
|
||||
.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
|
||||
Ok(mods_and_admins)
|
||||
}
|
||||
|
||||
pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
|
||||
Self::community_mods_and_admins(conn, community_id)
|
||||
.unwrap_or_default()
|
||||
.contains(&user_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||
|
@ -258,7 +312,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_8266238".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -278,7 +332,7 @@ mod tests {
|
|||
removed: None,
|
||||
deleted: None,
|
||||
updated: None,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_7625376".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
@ -300,7 +354,7 @@ mod tests {
|
|||
deleted: false,
|
||||
published: inserted_community.published,
|
||||
updated: None,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: inserted_community.actor_id.to_owned(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -295,18 +295,18 @@ pub struct CommunityModeratorView {
|
|||
}
|
||||
|
||||
impl CommunityModeratorView {
|
||||
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
|
||||
pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
|
||||
use super::community_view::community_moderator_view::dsl::*;
|
||||
community_moderator_view
|
||||
.filter(community_id.eq(from_community_id))
|
||||
.filter(community_id.eq(for_community_id))
|
||||
.order_by(published)
|
||||
.load::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
|
||||
pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
|
||||
use super::community_view::community_moderator_view::dsl::*;
|
||||
community_moderator_view
|
||||
.filter(user_id.eq(from_user_id))
|
||||
.filter(user_id.eq(for_user_id))
|
||||
.order_by(published)
|
||||
.load::<Self>(conn)
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
pub extern crate diesel;
|
||||
#[macro_use]
|
||||
pub extern crate strum_macros;
|
||||
#[macro_use]
|
||||
pub extern crate lazy_static;
|
||||
pub extern crate bcrypt;
|
||||
pub extern crate chrono;
|
||||
pub extern crate log;
|
||||
pub extern crate regex;
|
||||
pub extern crate serde;
|
||||
pub extern crate serde_json;
|
||||
pub extern crate sha2;
|
||||
|
@ -12,6 +15,7 @@ pub extern crate strum;
|
|||
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, env::VarError};
|
||||
|
||||
|
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
|
|||
chrono::prelude::Utc::now().naive_utc()
|
||||
}
|
||||
|
||||
pub fn is_email_regex(test: &str) -> bool {
|
||||
EMAIL_REGEX.is_match(test)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_REGEX: Regex =
|
||||
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::fuzzy_search;
|
||||
use crate::get_database_url_from_env;
|
||||
use crate::{get_database_url_from_env, is_email_regex};
|
||||
use diesel::{Connection, PgConnection};
|
||||
|
||||
pub fn establish_unpooled_connection() -> PgConnection {
|
||||
|
@ -194,4 +207,10 @@ mod tests {
|
|||
let test = "This is a fuzzy search";
|
||||
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email() {
|
||||
assert!(is_email_regex("gush@gmail.com"));
|
||||
assert!(!is_email_regex("nada_neutho"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -470,7 +470,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_829398".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -497,7 +497,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_82982738".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -517,7 +517,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_283687".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -105,7 +105,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_8292378".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::{ParseError, Url};
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[table_name = "post"]
|
||||
|
@ -56,6 +57,12 @@ pub struct PostForm {
|
|||
pub local: bool,
|
||||
}
|
||||
|
||||
impl PostForm {
|
||||
pub fn get_ap_id(&self) -> Result<Url, ParseError> {
|
||||
Url::parse(&self.ap_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::post::dsl::*;
|
||||
|
@ -101,6 +108,50 @@ impl Post {
|
|||
))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_deleted(
|
||||
conn: &PgConnection,
|
||||
post_id: i32,
|
||||
new_deleted: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::post::dsl::*;
|
||||
diesel::update(post.find(post_id))
|
||||
.set(deleted.eq(new_deleted))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_removed(
|
||||
conn: &PgConnection,
|
||||
post_id: i32,
|
||||
new_removed: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::post::dsl::*;
|
||||
diesel::update(post.find(post_id))
|
||||
.set(removed.eq(new_removed))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
|
||||
use crate::schema::post::dsl::*;
|
||||
diesel::update(post.find(post_id))
|
||||
.set(locked.eq(new_locked))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_stickied(
|
||||
conn: &PgConnection,
|
||||
post_id: i32,
|
||||
new_stickied: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::post::dsl::*;
|
||||
diesel::update(post.find(post_id))
|
||||
.set(stickied.eq(new_stickied))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
|
||||
user_id == post_creator_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud<PostForm> for Post {
|
||||
|
@ -272,7 +323,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_8292683678".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -292,7 +343,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_8223262378".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -415,7 +415,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_8282738268".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -435,7 +435,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_2763".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{schema::private_message, Crud};
|
||||
use crate::{naive_now, schema::private_message, Crud};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -80,6 +80,50 @@ impl PrivateMessage {
|
|||
.filter(ap_id.eq(object_id))
|
||||
.first::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_content(
|
||||
conn: &PgConnection,
|
||||
private_message_id: i32,
|
||||
new_content: &str,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set((content.eq(new_content), updated.eq(naive_now())))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_deleted(
|
||||
conn: &PgConnection,
|
||||
private_message_id: i32,
|
||||
new_deleted: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set(deleted.eq(new_deleted))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn update_read(
|
||||
conn: &PgConnection,
|
||||
private_message_id: i32,
|
||||
new_read: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set(read.eq(new_read))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(
|
||||
private_message
|
||||
.filter(recipient_id.eq(for_recipient_id))
|
||||
.filter(read.eq(false)),
|
||||
)
|
||||
.set(read.eq(true))
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -113,7 +157,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_6723878".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -140,7 +184,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_287263876".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -180,6 +224,10 @@ mod tests {
|
|||
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
||||
let updated_private_message =
|
||||
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
||||
let deleted_private_message =
|
||||
PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
|
||||
let marked_read_private_message =
|
||||
PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap();
|
||||
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
||||
User_::delete(&conn, inserted_creator.id).unwrap();
|
||||
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||
|
@ -187,6 +235,8 @@ mod tests {
|
|||
assert_eq!(expected_private_message, read_private_message);
|
||||
assert_eq!(expected_private_message, updated_private_message);
|
||||
assert_eq!(expected_private_message, inserted_private_message);
|
||||
assert!(deleted_private_message.deleted);
|
||||
assert!(marked_read_private_message.read);
|
||||
assert_eq!(1, num_deleted);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{
|
||||
is_email_regex,
|
||||
naive_now,
|
||||
schema::{user_, user_::dsl::*},
|
||||
Crud,
|
||||
|
@ -125,9 +126,18 @@ impl User_ {
|
|||
use crate::schema::user_::dsl::*;
|
||||
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl User_ {
|
||||
pub fn find_by_email_or_username(
|
||||
conn: &PgConnection,
|
||||
username_or_email: &str,
|
||||
) -> Result<Self, Error> {
|
||||
if is_email_regex(username_or_email) {
|
||||
Self::find_by_email(conn, username_or_email)
|
||||
} else {
|
||||
Self::find_by_username(conn, username_or_email)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
|
||||
user_.filter(name.eq(username)).first::<User_>(conn)
|
||||
}
|
||||
|
@ -166,7 +176,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_9826382637".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -195,7 +205,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: inserted_user.actor_id.to_owned(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
|
|
@ -52,6 +52,30 @@ impl Crud<UserMentionForm> for UserMention {
|
|||
}
|
||||
}
|
||||
|
||||
impl UserMention {
|
||||
pub fn update_read(
|
||||
conn: &PgConnection,
|
||||
user_mention_id: i32,
|
||||
new_read: bool,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::user_mention::dsl::*;
|
||||
diesel::update(user_mention.find(user_mention_id))
|
||||
.set(read.eq(new_read))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
|
||||
use crate::schema::user_mention::dsl::*;
|
||||
diesel::update(
|
||||
user_mention
|
||||
.filter(recipient_id.eq(for_recipient_id))
|
||||
.filter(read.eq(false)),
|
||||
)
|
||||
.set(read.eq(true))
|
||||
.get_results::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
|
@ -86,7 +110,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_628763".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -113,7 +137,7 @@ mod tests {
|
|||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_927389278".into(),
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
|
@ -133,7 +157,7 @@ mod tests {
|
|||
deleted: None,
|
||||
updated: None,
|
||||
nsfw: false,
|
||||
actor_id: "http://fake.com".into(),
|
||||
actor_id: "changeme_876238".into(),
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
|
|
|
@ -157,7 +157,28 @@ impl UserView {
|
|||
|
||||
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||
use super::user_view::user_fast::dsl::*;
|
||||
use diesel::sql_types::{Nullable, Text};
|
||||
user_fast
|
||||
// The select is necessary here to not get back emails
|
||||
.select((
|
||||
id,
|
||||
actor_id,
|
||||
name,
|
||||
avatar,
|
||||
"".into_sql::<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))
|
||||
.order_by(published)
|
||||
.load::<Self>(conn)
|
||||
|
@ -165,6 +186,28 @@ impl UserView {
|
|||
|
||||
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
|
|||
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
|
||||
}
|
||||
|
||||
pub fn is_email_regex(test: &str) -> bool {
|
||||
EMAIL_REGEX.is_match(test)
|
||||
}
|
||||
|
||||
pub fn remove_slurs(test: &str) -> String {
|
||||
SLUR_REGEX.replace_all(test, "*removed*").to_string()
|
||||
}
|
||||
|
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
is_email_regex,
|
||||
is_valid_community_name,
|
||||
is_valid_post_title,
|
||||
is_valid_username,
|
||||
|
@ -185,12 +180,6 @@ mod tests {
|
|||
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email() {
|
||||
assert!(is_email_regex("gush@gmail.com"));
|
||||
assert!(!is_email_regex("nada_neutho"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_register_username() {
|
||||
assert!(is_valid_username("Hello_98"));
|
||||
|
|
20
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql
vendored
Normal file
20
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql
vendored
Normal 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;
|
50
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql
vendored
Normal file
50
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql
vendored
Normal 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));
|
|
@ -1,7 +1,7 @@
|
|||
use diesel::{result::Error, PgConnection};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
||||
use lemmy_db::{user::User_, Crud};
|
||||
use lemmy_utils::{is_email_regex, settings::Settings};
|
||||
use lemmy_utils::settings::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
type Jwt = String;
|
||||
|
@ -54,18 +54,6 @@ impl Claims {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
// TODO: move these into user?
|
||||
pub fn find_by_email_or_username(
|
||||
conn: &PgConnection,
|
||||
username_or_email: &str,
|
||||
) -> Result<User_, Error> {
|
||||
if is_email_regex(username_or_email) {
|
||||
User_::find_by_email(conn, username_or_email)
|
||||
} else {
|
||||
User_::find_by_username(conn, username_or_email)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
|
||||
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
|
||||
User_::read(&conn, claims.id)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
|
||||
apub::{ApubLikeableType, ApubObjectType},
|
||||
blocking,
|
||||
websocket::{
|
||||
|
@ -15,12 +15,10 @@ use lemmy_db::{
|
|||
comment_view::*,
|
||||
community_view::*,
|
||||
moderator::*,
|
||||
naive_now,
|
||||
post::*,
|
||||
site_view::*,
|
||||
user::*,
|
||||
user_mention::*,
|
||||
user_view::*,
|
||||
Crud,
|
||||
Likeable,
|
||||
ListingType,
|
||||
|
@ -44,22 +42,38 @@ use std::str::FromStr;
|
|||
pub struct CreateComment {
|
||||
content: String,
|
||||
parent_id: Option<i32>,
|
||||
edit_id: Option<i32>, // TODO this isn't used
|
||||
pub post_id: i32,
|
||||
form_id: Option<String>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditComment {
|
||||
content: String,
|
||||
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
|
||||
edit_id: i32,
|
||||
creator_id: i32,
|
||||
pub post_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
form_id: Option<String>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DeleteComment {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RemoveComment {
|
||||
edit_id: i32,
|
||||
removed: bool,
|
||||
reason: Option<String>,
|
||||
read: Option<bool>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MarkCommentAsRead {
|
||||
edit_id: i32,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
|
@ -74,12 +88,12 @@ pub struct SaveComment {
|
|||
pub struct CommentResponse {
|
||||
pub comment: CommentView,
|
||||
pub recipient_ids: Vec<i32>,
|
||||
pub form_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateCommentLike {
|
||||
comment_id: i32,
|
||||
pub post_id: i32,
|
||||
score: i16,
|
||||
auth: String,
|
||||
}
|
||||
|
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Check if post is locked, no new comments
|
||||
if post.locked {
|
||||
return Err(APIError::err("locked").into());
|
||||
}
|
||||
|
||||
// Create the comment
|
||||
let comment_form2 = comment_form.clone();
|
||||
let inserted_comment =
|
||||
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
|
||||
|
@ -157,6 +177,7 @@ impl Perform for Oper<CreateComment> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||
};
|
||||
|
||||
// Necessary to update the ap_id
|
||||
let inserted_comment_id = inserted_comment.id;
|
||||
let updated_comment: Comment = match blocking(pool, move |conn| {
|
||||
let apub_id =
|
||||
|
@ -175,8 +196,15 @@ impl Perform for Oper<CreateComment> {
|
|||
|
||||
// Scan the comment for user mentions, add those rows
|
||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
||||
let recipient_ids =
|
||||
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
|
||||
let recipient_ids = send_local_notifs(
|
||||
mentions,
|
||||
updated_comment.clone(),
|
||||
user.clone(),
|
||||
post,
|
||||
pool,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// You like your own comment by default
|
||||
let like_form = CommentLikeForm {
|
||||
|
@ -201,6 +229,7 @@ impl Perform for Oper<CreateComment> {
|
|||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
form_id: data.form_id.to_owned(),
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
|
@ -237,37 +266,14 @@ impl Perform for Oper<EditComment> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let orig_comment =
|
||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||
|
||||
let mut editors: Vec<i32> = vec![orig_comment.creator_id];
|
||||
let mut moderators: Vec<i32> = vec![];
|
||||
|
||||
let community_id = orig_comment.community_id;
|
||||
moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
CommunityModeratorView::for_community(&conn, community_id)
|
||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
|
||||
editors.extend(&moderators);
|
||||
// You are allowed to mark the comment as read even if you're banned.
|
||||
if data.read.is_none() {
|
||||
// Verify its the creator or a mod, or an admin
|
||||
|
||||
if !editors.contains(&user_id) {
|
||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||
// Check for a 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
|
||||
|
@ -278,12 +284,308 @@ impl Perform for Oper<EditComment> {
|
|||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Verify that only the creator can edit
|
||||
if user_id != orig_comment.creator_id {
|
||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Do the update
|
||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||
let edit_id = data.edit_id;
|
||||
let updated_comment = match blocking(pool, move |conn| {
|
||||
Comment::update_content(conn, edit_id, &content_slurs_removed)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
|
||||
// Send the apub update
|
||||
updated_comment
|
||||
.send_update(&user, &self.client, pool)
|
||||
.await?;
|
||||
|
||||
// Do the mentions / recipients
|
||||
let post_id = orig_comment.post_id;
|
||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||
|
||||
let updated_comment_content = updated_comment.content.to_owned();
|
||||
let mentions = scrape_text_for_mentions(&updated_comment_content);
|
||||
let recipient_ids =
|
||||
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let comment_view = blocking(pool, move |conn| {
|
||||
CommentView::read(conn, edit_id, Some(user_id))
|
||||
})
|
||||
.await??;
|
||||
|
||||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
form_id: data.form_id.to_owned(),
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::EditComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<DeleteComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &DeleteComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let orig_comment =
|
||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||
|
||||
// Check for a site ban
|
||||
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 {
|
||||
// check that user can mark as read
|
||||
updated_comment
|
||||
.send_undo_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Refetch it
|
||||
let edit_id = data.edit_id;
|
||||
let comment_view = blocking(pool, move |conn| {
|
||||
CommentView::read(conn, edit_id, Some(user_id))
|
||||
})
|
||||
.await??;
|
||||
|
||||
// Build the recipients
|
||||
let post_id = comment_view.post_id;
|
||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||
let mentions = vec![];
|
||||
let recipient_ids =
|
||||
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
|
||||
|
||||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
form_id: None,
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::DeleteComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<RemoveComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &RemoveComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let orig_comment =
|
||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||
|
||||
// Check for a site ban
|
||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||
if user.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
let community_id = orig_comment.community_id;
|
||||
let is_banned =
|
||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||
if blocking(pool, is_banned).await? {
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Verify that only a mod or admin can remove
|
||||
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||
|
||||
// Do the remove
|
||||
let removed = data.removed;
|
||||
let updated_comment = match blocking(pool, move |conn| {
|
||||
Comment::update_removed(conn, edit_id, removed)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
let form = ModRemoveCommentForm {
|
||||
mod_user_id: user_id,
|
||||
comment_id: data.edit_id,
|
||||
removed: Some(removed),
|
||||
reason: data.reason.to_owned(),
|
||||
};
|
||||
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
|
||||
|
||||
// Send the apub message
|
||||
if removed {
|
||||
updated_comment
|
||||
.send_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
} else {
|
||||
updated_comment
|
||||
.send_undo_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Refetch it
|
||||
let edit_id = data.edit_id;
|
||||
let comment_view = blocking(pool, move |conn| {
|
||||
CommentView::read(conn, edit_id, Some(user_id))
|
||||
})
|
||||
.await??;
|
||||
|
||||
// Build the recipients
|
||||
let post_id = comment_view.post_id;
|
||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||
let mentions = vec![];
|
||||
let recipient_ids =
|
||||
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
|
||||
|
||||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
form_id: None,
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::RemoveComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<MarkCommentAsRead> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &MarkCommentAsRead = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let orig_comment =
|
||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
||||
|
||||
// Check for a site ban
|
||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||
if user.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
let community_id = orig_comment.community_id;
|
||||
let is_banned =
|
||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||
if blocking(pool, is_banned).await? {
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Verify that only the recipient can mark as read
|
||||
// Needs to fetch the parent comment / post to get the recipient
|
||||
let parent_id = orig_comment.parent_id;
|
||||
match parent_id {
|
||||
Some(pid) => {
|
||||
|
@ -301,137 +603,27 @@ impl Perform for Oper<EditComment> {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
|
||||
|
||||
let comment_form = {
|
||||
if data.read.is_none() {
|
||||
// the ban etc checks should been made and have passed
|
||||
// the comment can be properly edited
|
||||
let post_removed = if moderators.contains(&user_id) {
|
||||
data.removed
|
||||
} else {
|
||||
Some(read_comment.removed)
|
||||
};
|
||||
|
||||
CommentForm {
|
||||
content: content_slurs_removed,
|
||||
parent_id: read_comment.parent_id,
|
||||
post_id: read_comment.post_id,
|
||||
creator_id: read_comment.creator_id,
|
||||
removed: post_removed.to_owned(),
|
||||
deleted: data.deleted.to_owned(),
|
||||
read: Some(read_comment.read),
|
||||
published: None,
|
||||
updated: Some(naive_now()),
|
||||
ap_id: read_comment.ap_id,
|
||||
local: read_comment.local,
|
||||
}
|
||||
} else {
|
||||
// the only field that can be updated it the read field
|
||||
CommentForm {
|
||||
content: read_comment.content,
|
||||
parent_id: read_comment.parent_id,
|
||||
post_id: read_comment.post_id,
|
||||
creator_id: read_comment.creator_id,
|
||||
removed: Some(read_comment.removed).to_owned(),
|
||||
deleted: Some(read_comment.deleted).to_owned(),
|
||||
read: data.read.to_owned(),
|
||||
published: None,
|
||||
updated: orig_comment.updated,
|
||||
ap_id: read_comment.ap_id,
|
||||
local: read_comment.local,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let comment_form2 = comment_form.clone();
|
||||
let updated_comment = match blocking(pool, move |conn| {
|
||||
Comment::update(conn, edit_id, &comment_form2)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
// 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()),
|
||||
};
|
||||
|
||||
if data.read.is_none() {
|
||||
if let Some(deleted) = data.deleted.to_owned() {
|
||||
if deleted {
|
||||
updated_comment
|
||||
.send_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
} else {
|
||||
updated_comment
|
||||
.send_undo_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(removed) = data.removed.to_owned() {
|
||||
if moderators.contains(&user_id) {
|
||||
if removed {
|
||||
updated_comment
|
||||
.send_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
} else {
|
||||
updated_comment
|
||||
.send_undo_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updated_comment
|
||||
.send_update(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Mod tables
|
||||
if moderators.contains(&user_id) {
|
||||
if let Some(removed) = data.removed.to_owned() {
|
||||
let form = ModRemoveCommentForm {
|
||||
mod_user_id: user_id,
|
||||
comment_id: data.edit_id,
|
||||
removed: Some(removed),
|
||||
reason: data.reason.to_owned(),
|
||||
};
|
||||
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let post_id = data.post_id;
|
||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||
|
||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
||||
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
|
||||
|
||||
// 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 mut res = CommentResponse {
|
||||
let res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
recipient_ids: Vec::new(),
|
||||
form_id: None,
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::EditComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
@ -480,6 +672,7 @@ impl Perform for Oper<SaveComment> {
|
|||
Ok(CommentResponse {
|
||||
comment: comment_view,
|
||||
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
|
||||
let post_id = data.post_id;
|
||||
let post_id = orig_comment.post_id;
|
||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
||||
let community_id = post.community_id;
|
||||
let is_banned =
|
||||
|
@ -550,7 +747,7 @@ impl Perform for Oper<CreateCommentLike> {
|
|||
|
||||
let like_form = CommentLikeForm {
|
||||
comment_id: data.comment_id,
|
||||
post_id: data.post_id,
|
||||
post_id,
|
||||
user_id,
|
||||
score: data.score,
|
||||
};
|
||||
|
@ -587,6 +784,7 @@ impl Perform for Oper<CreateCommentLike> {
|
|||
let mut res = CommentResponse {
|
||||
comment: liked_comment,
|
||||
recipient_ids,
|
||||
form_id: None,
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
|
@ -675,9 +873,10 @@ pub async fn send_local_notifs(
|
|||
user: User_,
|
||||
post: Post,
|
||||
pool: &DbPool,
|
||||
do_send_email: bool,
|
||||
) -> Result<Vec<i32>, LemmyError> {
|
||||
let ids = blocking(pool, move |conn| {
|
||||
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
|
||||
do_send_local_notifs(conn, &mentions, &comment, &user, &post, do_send_email)
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
@ -690,6 +889,7 @@ fn do_send_local_notifs(
|
|||
comment: &Comment,
|
||||
user: &User_,
|
||||
post: &Post,
|
||||
do_send_email: bool,
|
||||
) -> Vec<i32> {
|
||||
let mut recipient_ids = Vec::new();
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
@ -720,7 +920,7 @@ fn do_send_local_notifs(
|
|||
};
|
||||
|
||||
// Send an email to those users that have notifications on
|
||||
if mention_user.send_notifications_to_email {
|
||||
if do_send_email && mention_user.send_notifications_to_email {
|
||||
if let Some(mention_email) = mention_user.email {
|
||||
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
|
||||
let html = &format!(
|
||||
|
@ -744,7 +944,7 @@ fn do_send_local_notifs(
|
|||
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
|
||||
recipient_ids.push(parent_user.id);
|
||||
|
||||
if parent_user.send_notifications_to_email {
|
||||
if do_send_email && parent_user.send_notifications_to_email {
|
||||
if let Some(comment_reply_email) = parent_user.email {
|
||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
||||
let html = &format!(
|
||||
|
@ -767,7 +967,7 @@ fn do_send_local_notifs(
|
|||
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
|
||||
recipient_ids.push(parent_user.id);
|
||||
|
||||
if parent_user.send_notifications_to_email {
|
||||
if do_send_email && parent_user.send_notifications_to_email {
|
||||
if let Some(post_reply_email) = parent_user.email {
|
||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
||||
let html = &format!(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
|
||||
apub::ActorType,
|
||||
blocking,
|
||||
websocket::{
|
||||
|
@ -34,7 +34,6 @@ pub struct GetCommunity {
|
|||
pub struct GetCommunityResponse {
|
||||
pub community: CommunityView,
|
||||
pub moderators: Vec<CommunityModeratorView>,
|
||||
pub admins: Vec<UserView>,
|
||||
pub online: usize,
|
||||
}
|
||||
|
||||
|
@ -98,13 +97,24 @@ pub struct AddModToCommunityResponse {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditCommunity {
|
||||
pub edit_id: i32,
|
||||
name: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
category_id: i32,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<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>,
|
||||
expires: Option<i64>,
|
||||
auth: String,
|
||||
|
@ -185,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
};
|
||||
|
||||
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||
let site_creator_id = site.creator_id;
|
||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
let online = if let Some(ws) = websocket_info {
|
||||
if let Some(id) = ws.id {
|
||||
ws.chatserver.do_send(JoinCommunityRoom {
|
||||
|
@ -213,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
|
|||
let res = GetCommunityResponse {
|
||||
community: community_view,
|
||||
moderators,
|
||||
admins,
|
||||
online,
|
||||
};
|
||||
|
||||
|
@ -264,6 +266,17 @@ impl Perform for Oper<CreateCommunity> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Double check for duplicate community actor_ids
|
||||
let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
|
||||
let actor_id_cloned = actor_id.to_owned();
|
||||
let community_dupe = blocking(pool, move |conn| {
|
||||
Community::read_from_actor_id(conn, &actor_id_cloned)
|
||||
})
|
||||
.await?;
|
||||
if community_dupe.is_ok() {
|
||||
return Err(APIError::err("community_already_exists").into());
|
||||
}
|
||||
|
||||
// When you create a community, make sure the user becomes a moderator and a follower
|
||||
let keypair = generate_actor_keypair()?;
|
||||
|
||||
|
@ -277,7 +290,7 @@ impl Perform for Oper<CreateCommunity> {
|
|||
deleted: None,
|
||||
nsfw: data.nsfw,
|
||||
updated: None,
|
||||
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
|
||||
actor_id,
|
||||
local: true,
|
||||
private_key: Some(keypair.private_key),
|
||||
public_key: Some(keypair.public_key),
|
||||
|
@ -333,10 +346,6 @@ impl Perform for Oper<EditCommunity> {
|
|||
) -> Result<CommunityResponse, LemmyError> {
|
||||
let data: &EditCommunity = &self.data;
|
||||
|
||||
if let Err(slurs) = slur_check(&data.name) {
|
||||
return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
|
||||
}
|
||||
|
||||
if let Err(slurs) = slur_check(&data.title) {
|
||||
return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
|
||||
}
|
||||
|
@ -352,10 +361,6 @@ impl Perform for Oper<EditCommunity> {
|
|||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
if !is_valid_community_name(&data.name) {
|
||||
return Err(APIError::err("invalid_community_name").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a site ban
|
||||
|
@ -364,37 +369,28 @@ impl Perform for Oper<EditCommunity> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Verify its a mod
|
||||
// Verify its a mod (only mods can edit it)
|
||||
let edit_id = data.edit_id;
|
||||
let mut editors: Vec<i32> = Vec::new();
|
||||
editors.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
let mods: Vec<i32> = blocking(pool, move |conn| {
|
||||
CommunityModeratorView::for_community(conn, edit_id)
|
||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
editors.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
if !editors.contains(&user_id) {
|
||||
return Err(APIError::err("no_community_edit_allowed").into());
|
||||
.await??;
|
||||
if !mods.contains(&user_id) {
|
||||
return Err(APIError::err("not_a_moderator").into());
|
||||
}
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
||||
|
||||
let community_form = CommunityForm {
|
||||
name: data.name.to_owned(),
|
||||
name: read_community.name,
|
||||
title: data.title.to_owned(),
|
||||
description: data.description.to_owned(),
|
||||
category_id: data.category_id.to_owned(),
|
||||
creator_id: read_community.creator_id,
|
||||
removed: data.removed.to_owned(),
|
||||
deleted: data.deleted.to_owned(),
|
||||
removed: Some(read_community.removed),
|
||||
deleted: Some(read_community.deleted),
|
||||
nsfw: data.nsfw,
|
||||
updated: Some(naive_now()),
|
||||
actor_id: read_community.actor_id,
|
||||
|
@ -406,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
|
|||
};
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let updated_community = match blocking(pool, move |conn| {
|
||||
match blocking(pool, move |conn| {
|
||||
Community::update(conn, edit_id, &community_form)
|
||||
})
|
||||
.await?
|
||||
|
@ -415,23 +411,69 @@ impl Perform for Oper<EditCommunity> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
if let Some(removed) = data.removed.to_owned() {
|
||||
let expires = match data.expires {
|
||||
Some(time) => Some(naive_from_unix(time)),
|
||||
None => None,
|
||||
// TODO there needs to be some kind of an apub update
|
||||
// process for communities and users
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let community_view = blocking(pool, move |conn| {
|
||||
CommunityView::read(conn, edit_id, Some(user_id))
|
||||
})
|
||||
.await??;
|
||||
|
||||
let res = CommunityResponse {
|
||||
community: community_view,
|
||||
};
|
||||
let form = ModRemoveCommunityForm {
|
||||
mod_user_id: user_id,
|
||||
community_id: data.edit_id,
|
||||
removed: Some(removed),
|
||||
reason: data.reason.to_owned(),
|
||||
expires,
|
||||
|
||||
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()),
|
||||
};
|
||||
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
|
||||
|
||||
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)
|
||||
let edit_id = data.edit_id;
|
||||
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
||||
if read_community.creator_id != user_id {
|
||||
return Err(APIError::err("no_community_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Do the delete
|
||||
let edit_id = data.edit_id;
|
||||
let deleted = data.deleted;
|
||||
let updated_community = match blocking(pool, move |conn| {
|
||||
Community::update_deleted(conn, edit_id, deleted)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||
};
|
||||
|
||||
// Send apub messages
|
||||
if deleted {
|
||||
updated_community
|
||||
.send_delete(&user, &self.client, pool)
|
||||
|
@ -441,17 +483,6 @@ impl Perform for Oper<EditCommunity> {
|
|||
.send_undo_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(removed) = data.removed.to_owned() {
|
||||
if removed {
|
||||
updated_community
|
||||
.send_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
} else {
|
||||
updated_community
|
||||
.send_undo_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let community_view = blocking(pool, move |conn| {
|
||||
|
@ -463,19 +494,87 @@ impl Perform for Oper<EditCommunity> {
|
|||
community: community_view,
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
// Strip out the user id and subscribed when sending to others
|
||||
let mut res_sent = res.clone();
|
||||
res_sent.community.user_id = None;
|
||||
res_sent.community.subscribed = None;
|
||||
send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
|
||||
|
||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
||||
op: UserOperation::EditCommunity,
|
||||
response: res_sent,
|
||||
community_id: data.edit_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<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)
|
||||
}
|
||||
|
@ -652,27 +751,10 @@ impl Perform for Oper<BanFromCommunity> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let mut community_moderators: Vec<i32> = vec![];
|
||||
|
||||
let community_id = data.community_id;
|
||||
|
||||
community_moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
CommunityModeratorView::for_community(&conn, community_id)
|
||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
community_moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
|
||||
if !community_moderators.contains(&user_id) {
|
||||
return Err(APIError::err("couldnt_update_community").into());
|
||||
}
|
||||
// Verify that only mods or admins can ban
|
||||
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||
|
||||
let community_user_ban_form = CommunityUserBanForm {
|
||||
community_id: data.community_id,
|
||||
|
@ -692,6 +774,7 @@ impl Perform for Oper<BanFromCommunity> {
|
|||
}
|
||||
|
||||
// Mod tables
|
||||
// TODO eventually do correct expires
|
||||
let expires = match data.expires {
|
||||
Some(time) => Some(naive_from_unix(time)),
|
||||
None => None,
|
||||
|
@ -751,27 +834,10 @@ impl Perform for Oper<AddModToCommunity> {
|
|||
user_id: data.user_id,
|
||||
};
|
||||
|
||||
let mut community_moderators: Vec<i32> = vec![];
|
||||
|
||||
let community_id = data.community_id;
|
||||
|
||||
community_moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
CommunityModeratorView::for_community(&conn, community_id)
|
||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
community_moderators.append(
|
||||
&mut blocking(pool, move |conn| {
|
||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
||||
})
|
||||
.await??,
|
||||
);
|
||||
|
||||
if !community_moderators.contains(&user_id) {
|
||||
return Err(APIError::err("couldnt_update_community").into());
|
||||
}
|
||||
// Verify that only mods or admins can add mod
|
||||
is_mod_or_admin(pool, user_id, community_id).await?;
|
||||
|
||||
if data.added {
|
||||
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
||||
|
@ -850,26 +916,9 @@ impl Perform for Oper<TransferCommunity> {
|
|||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let community_form = CommunityForm {
|
||||
name: read_community.name,
|
||||
title: read_community.title,
|
||||
description: read_community.description,
|
||||
category_id: read_community.category_id,
|
||||
creator_id: data.user_id, // This makes the new user the community creator
|
||||
removed: None,
|
||||
deleted: None,
|
||||
nsfw: read_community.nsfw,
|
||||
updated: Some(naive_now()),
|
||||
actor_id: read_community.actor_id,
|
||||
local: read_community.local,
|
||||
private_key: read_community.private_key,
|
||||
public_key: read_community.public_key,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
};
|
||||
|
||||
let community_id = data.community_id;
|
||||
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
|
||||
let new_creator = data.user_id;
|
||||
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
|
||||
if blocking(pool, update).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_community").into());
|
||||
};
|
||||
|
@ -939,8 +988,27 @@ impl Perform for Oper<TransferCommunity> {
|
|||
Ok(GetCommunityResponse {
|
||||
community: community_view,
|
||||
moderators,
|
||||
admins,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
|
||||
use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError};
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
|
||||
use lemmy_db::{
|
||||
community::*,
|
||||
community_view::*,
|
||||
moderator::*,
|
||||
site::*,
|
||||
user::*,
|
||||
user_view::*,
|
||||
Crud,
|
||||
};
|
||||
|
||||
pub mod claims;
|
||||
pub mod comment;
|
||||
|
@ -44,3 +52,25 @@ pub trait Perform {
|
|||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> 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(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
|
||||
apub::{ApubLikeableType, ApubObjectType},
|
||||
blocking,
|
||||
fetch_iframely_and_pictrs_data,
|
||||
|
@ -18,10 +18,8 @@ use lemmy_db::{
|
|||
naive_now,
|
||||
post::*,
|
||||
post_view::*,
|
||||
site::*,
|
||||
site_view::*,
|
||||
user::*,
|
||||
user_view::*,
|
||||
Crud,
|
||||
Likeable,
|
||||
ListingType,
|
||||
|
@ -37,6 +35,7 @@ use lemmy_utils::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CreatePost {
|
||||
|
@ -65,7 +64,6 @@ pub struct GetPostResponse {
|
|||
comments: Vec<CommentView>,
|
||||
community: CommunityView,
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
admins: Vec<UserView>,
|
||||
pub online: usize,
|
||||
}
|
||||
|
||||
|
@ -95,20 +93,42 @@ pub struct CreatePostLike {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditPost {
|
||||
pub edit_id: i32,
|
||||
creator_id: i32,
|
||||
community_id: i32,
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
body: Option<String>,
|
||||
removed: Option<bool>,
|
||||
deleted: Option<bool>,
|
||||
nsfw: bool,
|
||||
locked: Option<bool>,
|
||||
stickied: Option<bool>,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DeletePost {
|
||||
pub edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RemovePost {
|
||||
pub edit_id: i32,
|
||||
removed: bool,
|
||||
reason: Option<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)]
|
||||
pub struct SavePost {
|
||||
post_id: i32,
|
||||
|
@ -162,6 +182,13 @@ impl Perform for Oper<CreatePost> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
if let Some(url) = data.url.as_ref() {
|
||||
match Url::parse(url) {
|
||||
Ok(_t) => (),
|
||||
Err(_e) => return Err(APIError::err("invalid_url").into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Iframely and pictrs cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
||||
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
||||
|
@ -303,14 +330,6 @@ impl Perform for Oper<GetPost> {
|
|||
})
|
||||
.await??;
|
||||
|
||||
let site_creator_id =
|
||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
||||
|
||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
let online = if let Some(ws) = websocket_info {
|
||||
if let Some(id) = ws.id {
|
||||
ws.chatserver.do_send(JoinPostRoom {
|
||||
|
@ -335,7 +354,6 @@ impl Perform for Oper<GetPost> {
|
|||
comments,
|
||||
community,
|
||||
moderators,
|
||||
admins,
|
||||
online,
|
||||
})
|
||||
}
|
||||
|
@ -541,35 +559,10 @@ impl Perform for Oper<EditPost> {
|
|||
let user_id = claims.id;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||
|
||||
// Verify its the creator or a mod or admin
|
||||
let community_id = read_post.community_id;
|
||||
let mut editors: Vec<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());
|
||||
}
|
||||
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
||||
|
||||
// Check for a community ban
|
||||
let community_id = read_post.community_id;
|
||||
let community_id = orig_post.community_id;
|
||||
let is_banned =
|
||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
||||
if blocking(pool, is_banned).await? {
|
||||
|
@ -582,55 +575,34 @@ impl Perform for Oper<EditPost> {
|
|||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Verify that only the creator can edit
|
||||
if !Post::is_post_creator(user_id, orig_post.creator_id) {
|
||||
return Err(APIError::err("no_post_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Fetch Iframely and Pictrs cached image
|
||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
||||
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
||||
|
||||
let post_form = {
|
||||
// only modify some properties if they are a moderator
|
||||
if moderators.contains(&user_id) {
|
||||
PostForm {
|
||||
let post_form = PostForm {
|
||||
name: data.name.trim().to_owned(),
|
||||
url: data.url.to_owned(),
|
||||
body: data.body.to_owned(),
|
||||
creator_id: read_post.creator_id.to_owned(),
|
||||
community_id: read_post.community_id,
|
||||
removed: data.removed.to_owned(),
|
||||
deleted: data.deleted.to_owned(),
|
||||
nsfw: data.nsfw,
|
||||
locked: data.locked.to_owned(),
|
||||
stickied: data.stickied.to_owned(),
|
||||
creator_id: orig_post.creator_id.to_owned(),
|
||||
community_id: orig_post.community_id,
|
||||
removed: Some(orig_post.removed),
|
||||
deleted: Some(orig_post.deleted),
|
||||
locked: Some(orig_post.locked),
|
||||
stickied: Some(orig_post.stickied),
|
||||
updated: Some(naive_now()),
|
||||
embed_title: iframely_title,
|
||||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictrs_thumbnail,
|
||||
ap_id: read_post.ap_id,
|
||||
local: read_post.local,
|
||||
ap_id: orig_post.ap_id,
|
||||
local: orig_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;
|
||||
|
@ -648,58 +620,8 @@ impl Perform for Oper<EditPost> {
|
|||
}
|
||||
};
|
||||
|
||||
if moderators.contains(&user_id) {
|
||||
// Mod tables
|
||||
if let Some(removed) = data.removed.to_owned() {
|
||||
let form = ModRemovePostForm {
|
||||
mod_user_id: user_id,
|
||||
post_id: data.edit_id,
|
||||
removed: Some(removed),
|
||||
reason: data.reason.to_owned(),
|
||||
};
|
||||
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
|
||||
}
|
||||
|
||||
if let Some(locked) = data.locked.to_owned() {
|
||||
let form = ModLockPostForm {
|
||||
mod_user_id: user_id,
|
||||
post_id: data.edit_id,
|
||||
locked: Some(locked),
|
||||
};
|
||||
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
|
||||
}
|
||||
|
||||
if let Some(stickied) = data.stickied.to_owned() {
|
||||
let form = ModStickyPostForm {
|
||||
mod_user_id: user_id,
|
||||
post_id: data.edit_id,
|
||||
stickied: Some(stickied),
|
||||
};
|
||||
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(deleted) = data.deleted.to_owned() {
|
||||
if deleted {
|
||||
updated_post.send_delete(&user, &self.client, pool).await?;
|
||||
} else {
|
||||
updated_post
|
||||
.send_undo_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
} else if let Some(removed) = data.removed.to_owned() {
|
||||
if moderators.contains(&user_id) {
|
||||
if removed {
|
||||
updated_post.send_remove(&user, &self.client, pool).await?;
|
||||
} else {
|
||||
updated_post
|
||||
.send_undo_remove(&user, &self.client, pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Send apub update
|
||||
updated_post.send_update(&user, &self.client, pool).await?;
|
||||
}
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let post_view = blocking(pool, move |conn| {
|
||||
|
@ -721,6 +643,324 @@ impl Perform for Oper<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)]
|
||||
impl Perform for Oper<SavePost> {
|
||||
type Response = PostResponse;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use super::user::Register;
|
||||
use crate::{
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||
apub::fetcher::search_by_apub_id,
|
||||
blocking,
|
||||
version,
|
||||
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
|
||||
DbPool,
|
||||
LemmyError,
|
||||
|
@ -110,6 +111,7 @@ pub struct GetSiteResponse {
|
|||
admins: Vec<UserView>,
|
||||
banned: Vec<UserView>,
|
||||
pub online: usize,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -255,10 +257,7 @@ impl Perform for Oper<CreateSite> {
|
|||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
||||
if !user.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
is_admin(pool, user_id).await?;
|
||||
|
||||
let site_form = SiteForm {
|
||||
name: data.name.to_owned(),
|
||||
|
@ -309,10 +308,7 @@ impl Perform for Oper<EditSite> {
|
|||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
||||
if !user.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
is_admin(pool, user_id).await?;
|
||||
|
||||
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||
|
||||
|
@ -424,6 +420,7 @@ impl Perform for Oper<GetSite> {
|
|||
admins,
|
||||
banned,
|
||||
online,
|
||||
version: version::VERSION.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -666,6 +663,7 @@ impl Perform for Oper<TransferSite> {
|
|||
admins,
|
||||
banned,
|
||||
online: 0,
|
||||
version: version::VERSION.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -689,12 +687,7 @@ impl Perform for Oper<GetSiteConfig> {
|
|||
let user_id = claims.id;
|
||||
|
||||
// Only let admins read this
|
||||
let admins = blocking(pool, move |conn| UserView::admins(conn)).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());
|
||||
}
|
||||
is_admin(pool, user_id).await?;
|
||||
|
||||
let config_hjson = Settings::read_config_file()?;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
api::{claims::Claims, APIError, Oper, Perform},
|
||||
api::{claims::Claims, is_admin, APIError, Oper, Perform},
|
||||
apub::ApubObjectType,
|
||||
blocking,
|
||||
websocket::{
|
||||
|
@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse {
|
|||
moderates: Vec<CommunityModeratorView>,
|
||||
comments: Vec<CommentView>,
|
||||
posts: Vec<PostView>,
|
||||
admins: Vec<UserView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -174,9 +173,9 @@ pub struct GetUserMentions {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditUserMention {
|
||||
pub struct MarkUserMentionAsRead {
|
||||
user_mention_id: i32,
|
||||
read: Option<bool>,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
|
@ -216,9 +215,21 @@ pub struct CreatePrivateMessage {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditPrivateMessage {
|
||||
edit_id: i32,
|
||||
content: Option<String>,
|
||||
deleted: Option<bool>,
|
||||
read: Option<bool>,
|
||||
content: String,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DeletePrivateMessage {
|
||||
edit_id: i32,
|
||||
deleted: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MarkPrivateMessageAsRead {
|
||||
edit_id: i32,
|
||||
read: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
|
@ -264,7 +275,7 @@ impl Perform for Oper<Login> {
|
|||
// Fetch that username / email
|
||||
let username_or_email = data.username_or_email.clone();
|
||||
let user = match blocking(pool, move |conn| {
|
||||
Claims::find_by_email_or_username(conn, &username_or_email)
|
||||
User_::find_by_email_or_username(conn, &username_or_email)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
|
@ -631,20 +642,10 @@ impl Perform for Oper<GetUserDetails> {
|
|||
})
|
||||
.await??;
|
||||
|
||||
let site_creator_id =
|
||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
||||
|
||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
// If its not the same user, remove the email
|
||||
if let Some(user_id) = user_id {
|
||||
if user_details_id != user_id {
|
||||
user_view.email = None;
|
||||
}
|
||||
} else {
|
||||
// If its not the same user, remove the email, and settings
|
||||
// TODO an if let chain would be better here, but can't figure it out
|
||||
// TODO separate out settings into its own thing
|
||||
if user_id.is_none() || user_details_id != user_id.unwrap_or(0) {
|
||||
user_view.email = None;
|
||||
}
|
||||
|
||||
|
@ -655,7 +656,6 @@ impl Perform for Oper<GetUserDetails> {
|
|||
moderates,
|
||||
comments,
|
||||
posts,
|
||||
admins,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -679,10 +679,7 @@ impl Perform for Oper<AddAdmin> {
|
|||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
|
||||
if !blocking(pool, is_admin).await?? {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
is_admin(pool, user_id).await?;
|
||||
|
||||
let added = data.added;
|
||||
let added_user_id = data.user_id;
|
||||
|
@ -741,10 +738,7 @@ impl Perform for Oper<BanUser> {
|
|||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
|
||||
if !blocking(pool, is_admin).await?? {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
is_admin(pool, user_id).await?;
|
||||
|
||||
let ban = data.ban;
|
||||
let banned_user_id = data.user_id;
|
||||
|
@ -864,7 +858,7 @@ impl Perform for Oper<GetUserMentions> {
|
|||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<EditUserMention> {
|
||||
impl Perform for Oper<MarkUserMentionAsRead> {
|
||||
type Response = UserMentionResponse;
|
||||
|
||||
async fn perform(
|
||||
|
@ -872,7 +866,7 @@ impl Perform for Oper<EditUserMention> {
|
|||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<UserMentionResponse, LemmyError> {
|
||||
let data: &EditUserMention = &self.data;
|
||||
let data: &MarkUserMentionAsRead = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
|
@ -880,28 +874,23 @@ impl Perform for Oper<EditUserMention> {
|
|||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
if user_id != data.user_mention_id {
|
||||
|
||||
let user_mention_id = data.user_mention_id;
|
||||
let read_user_mention =
|
||||
blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
|
||||
|
||||
if user_id != read_user_mention.recipient_id {
|
||||
return Err(APIError::err("couldnt_update_comment").into());
|
||||
}
|
||||
|
||||
let user_mention_id = data.user_mention_id;
|
||||
let user_mention =
|
||||
blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
|
||||
|
||||
let user_mention_form = UserMentionForm {
|
||||
recipient_id: user_id,
|
||||
comment_id: user_mention.comment_id,
|
||||
read: data.read.to_owned(),
|
||||
};
|
||||
|
||||
let user_mention_id = user_mention.id;
|
||||
let update_mention =
|
||||
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
|
||||
let user_mention_id = read_user_mention.id;
|
||||
let read = data.read;
|
||||
let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
|
||||
if blocking(pool, update_mention).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_comment").into());
|
||||
};
|
||||
|
||||
let user_mention_id = user_mention.id;
|
||||
let user_mention_id = read_user_mention.id;
|
||||
let user_mention_view = blocking(pool, move |conn| {
|
||||
UserMentionView::read(conn, user_mention_id, user_id)
|
||||
})
|
||||
|
@ -941,71 +930,27 @@ impl Perform for Oper<MarkAllAsRead> {
|
|||
.await??;
|
||||
|
||||
// TODO: this should probably be a bulk operation
|
||||
// Not easy to do as a bulk operation,
|
||||
// because recipient_id isn't in the comment table
|
||||
for reply in &replies {
|
||||
let reply_id = reply.id;
|
||||
let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id);
|
||||
let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
|
||||
if blocking(pool, mark_as_read).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_comment").into());
|
||||
}
|
||||
}
|
||||
|
||||
// Mentions
|
||||
let mentions = blocking(pool, move |conn| {
|
||||
UserMentionQueryBuilder::create(conn, user_id)
|
||||
.unread_only(true)
|
||||
.page(1)
|
||||
.limit(999)
|
||||
.list()
|
||||
})
|
||||
.await??;
|
||||
|
||||
// TODO: this should probably be a bulk operation
|
||||
for mention in &mentions {
|
||||
let mention_form = UserMentionForm {
|
||||
recipient_id: mention.to_owned().recipient_id,
|
||||
comment_id: mention.to_owned().id,
|
||||
read: Some(true),
|
||||
};
|
||||
|
||||
let user_mention_id = mention.user_mention_id;
|
||||
let update_mention =
|
||||
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form);
|
||||
if blocking(pool, update_mention).await?.is_err() {
|
||||
// Mark all user mentions as read
|
||||
let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
|
||||
if blocking(pool, update_user_mentions).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_comment").into());
|
||||
}
|
||||
}
|
||||
|
||||
// messages
|
||||
let messages = blocking(pool, move |conn| {
|
||||
PrivateMessageQueryBuilder::create(conn, user_id)
|
||||
.page(1)
|
||||
.limit(999)
|
||||
.unread_only(true)
|
||||
.list()
|
||||
})
|
||||
.await??;
|
||||
|
||||
// TODO: this should probably be a bulk operation
|
||||
for message in &messages {
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: message.to_owned().content,
|
||||
creator_id: message.to_owned().creator_id,
|
||||
recipient_id: message.to_owned().recipient_id,
|
||||
deleted: None,
|
||||
read: Some(true),
|
||||
updated: None,
|
||||
ap_id: message.to_owned().ap_id,
|
||||
local: message.local,
|
||||
published: None,
|
||||
};
|
||||
|
||||
let message_id = message.id;
|
||||
let update_pm =
|
||||
move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
|
||||
// Mark all private_messages as read
|
||||
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
|
||||
if blocking(pool, update_pm).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_private_message").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetRepliesResponse { replies: vec![] })
|
||||
}
|
||||
|
@ -1294,59 +1239,25 @@ impl Perform for Oper<EditPrivateMessage> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let orig_private_message =
|
||||
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||
|
||||
// Check for a site ban
|
||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||
if user.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Check to make sure they are the creator (or the recipient marking as read
|
||||
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|
||||
|| orig_private_message.creator_id.eq(&user_id))
|
||||
{
|
||||
// Checking permissions
|
||||
let edit_id = data.edit_id;
|
||||
let orig_private_message =
|
||||
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||
if user_id != orig_private_message.creator_id {
|
||||
return Err(APIError::err("no_private_message_edit_allowed").into());
|
||||
}
|
||||
|
||||
let content_slurs_removed = match &data.content {
|
||||
Some(content) => remove_slurs(content),
|
||||
None => orig_private_message.content.clone(),
|
||||
};
|
||||
|
||||
let private_message_form = {
|
||||
if data.read.is_some() {
|
||||
PrivateMessageForm {
|
||||
content: orig_private_message.content.to_owned(),
|
||||
creator_id: orig_private_message.creator_id,
|
||||
recipient_id: orig_private_message.recipient_id,
|
||||
read: data.read.to_owned(),
|
||||
updated: orig_private_message.updated,
|
||||
deleted: Some(orig_private_message.deleted),
|
||||
ap_id: orig_private_message.ap_id,
|
||||
local: orig_private_message.local,
|
||||
published: None,
|
||||
}
|
||||
} else {
|
||||
PrivateMessageForm {
|
||||
content: content_slurs_removed,
|
||||
creator_id: orig_private_message.creator_id,
|
||||
recipient_id: orig_private_message.recipient_id,
|
||||
deleted: data.deleted.to_owned(),
|
||||
read: Some(orig_private_message.read),
|
||||
updated: Some(naive_now()),
|
||||
ap_id: orig_private_message.ap_id,
|
||||
local: orig_private_message.local,
|
||||
published: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Doing the update
|
||||
let content_slurs_removed = remove_slurs(&data.content);
|
||||
let edit_id = data.edit_id;
|
||||
let updated_private_message = match blocking(pool, move |conn| {
|
||||
PrivateMessage::update(conn, edit_id, &private_message_form)
|
||||
PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
|
@ -1354,9 +1265,76 @@ impl Perform for Oper<EditPrivateMessage> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||
};
|
||||
|
||||
if data.read.is_none() {
|
||||
if let Some(deleted) = data.deleted.to_owned() {
|
||||
if deleted {
|
||||
// Send the apub update
|
||||
updated_private_message
|
||||
.send_update(&user, &self.client, pool)
|
||||
.await?;
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||
let recipient_id = message.recipient_id;
|
||||
|
||||
let res = PrivateMessageResponse { message };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendUserRoomMessage {
|
||||
op: UserOperation::EditPrivateMessage,
|
||||
response: res.clone(),
|
||||
recipient_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<DeletePrivateMessage> {
|
||||
type Response = PrivateMessageResponse;
|
||||
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PrivateMessageResponse, LemmyError> {
|
||||
let data: &DeletePrivateMessage = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a site ban
|
||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||
if user.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Checking permissions
|
||||
let edit_id = data.edit_id;
|
||||
let orig_private_message =
|
||||
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||
if user_id != orig_private_message.creator_id {
|
||||
return Err(APIError::err("no_private_message_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Doing the update
|
||||
let edit_id = data.edit_id;
|
||||
let deleted = data.deleted;
|
||||
let updated_private_message = match blocking(pool, move |conn| {
|
||||
PrivateMessage::update_deleted(conn, edit_id, deleted)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
Ok(private_message) => private_message,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||
};
|
||||
|
||||
// Send the apub update
|
||||
if data.deleted {
|
||||
updated_private_message
|
||||
.send_delete(&user, &self.client, pool)
|
||||
.await?;
|
||||
|
@ -1365,27 +1343,83 @@ impl Perform for Oper<EditPrivateMessage> {
|
|||
.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 message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||
let recipient_id = message.recipient_id;
|
||||
|
||||
let res = PrivateMessageResponse { message };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendUserRoomMessage {
|
||||
op: UserOperation::EditPrivateMessage,
|
||||
op: UserOperation::DeletePrivateMessage,
|
||||
response: res.clone(),
|
||||
recipient_id: orig_private_message.recipient_id,
|
||||
recipient_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<MarkPrivateMessageAsRead> {
|
||||
type Response = PrivateMessageResponse;
|
||||
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PrivateMessageResponse, LemmyError> {
|
||||
let data: &MarkPrivateMessageAsRead = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a site ban
|
||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
||||
if user.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Checking permissions
|
||||
let edit_id = data.edit_id;
|
||||
let orig_private_message =
|
||||
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
|
||||
if user_id != orig_private_message.recipient_id {
|
||||
return Err(APIError::err("couldnt_update_private_message").into());
|
||||
}
|
||||
|
||||
// Doing the update
|
||||
let edit_id = data.edit_id;
|
||||
let read = data.read;
|
||||
match blocking(pool, move |conn| {
|
||||
PrivateMessage::update_read(conn, edit_id, read)
|
||||
})
|
||||
.await?
|
||||
{
|
||||
Ok(private_message) => private_message,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||
};
|
||||
|
||||
// No need to send an apub update
|
||||
|
||||
let edit_id = data.edit_id;
|
||||
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
|
||||
let recipient_id = message.recipient_id;
|
||||
|
||||
let res = PrivateMessageResponse { message };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendUserRoomMessage {
|
||||
op: UserOperation::MarkPrivateMessageAsRead,
|
||||
response: res.clone(),
|
||||
recipient_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,41 +10,20 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
|
||||
use activitystreams_new::base::AnyBase;
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::{community::Community, user::User_};
|
||||
use log::debug;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
use url::Url;
|
||||
|
||||
pub fn populate_object_props(
|
||||
props: &mut ObjectProperties,
|
||||
addressed_ccs: Vec<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>(
|
||||
pub async fn send_activity_to_community(
|
||||
creator: &User_,
|
||||
community: &Community,
|
||||
to: Vec<String>,
|
||||
activity: A,
|
||||
activity: AnyBase,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
|
||||
{
|
||||
) -> Result<(), LemmyError> {
|
||||
insert_activity(creator.id, activity.clone(), true, pool).await?;
|
||||
|
||||
// if this is a local community, we need to do an announce from the community instead
|
||||
|
@ -58,15 +37,12 @@ where
|
|||
}
|
||||
|
||||
/// Send an activity to a list of recipients, using the correct headers etc.
|
||||
pub async fn send_activity<A>(
|
||||
pub async fn send_activity(
|
||||
client: &Client,
|
||||
activity: &A,
|
||||
activity: &AnyBase,
|
||||
actor: &dyn ActorType,
|
||||
to: Vec<String>,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
A: Serialize,
|
||||
{
|
||||
) -> Result<(), LemmyError> {
|
||||
let activity = serde_json::to_string(&activity)?;
|
||||
debug!("Sending activitypub activity {} to {:?}", activity, to);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity_to_community},
|
||||
activities::send_activity_to_community,
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
|
@ -21,13 +21,15 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
use activitystreams_new::{
|
||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
||||
base::AnyBase,
|
||||
context,
|
||||
link::Mention,
|
||||
object::{kind::NoteType, properties::ObjectProperties, Note},
|
||||
object::{kind::NoteType, Note, Tombstone},
|
||||
prelude::*,
|
||||
public,
|
||||
};
|
||||
use activitystreams_new::object::Tombstone;
|
||||
use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
|
||||
use itertools::Itertools;
|
||||
use lemmy_db::{
|
||||
|
@ -40,6 +42,8 @@ use lemmy_db::{
|
|||
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Error;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommentQuery {
|
||||
|
@ -66,8 +70,7 @@ impl ToApub for Comment {
|
|||
type Response = Note;
|
||||
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
||||
let mut comment = Note::default();
|
||||
let oprops: &mut ObjectProperties = comment.as_mut();
|
||||
let mut comment = Note::new();
|
||||
|
||||
let creator_id = self.creator_id;
|
||||
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
|
||||
|
@ -88,18 +91,18 @@ impl ToApub for Comment {
|
|||
in_reply_to_vec.push(parent_comment.ap_id);
|
||||
}
|
||||
|
||||
oprops
|
||||
comment
|
||||
// Not needed when the Post is embedded in a collection (like for community outbox)
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(self.ap_id.to_owned())?
|
||||
.set_published(convert_datetime(self.published))?
|
||||
.set_to_xsd_any_uri(community.actor_id)?
|
||||
.set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
|
||||
.set_content_xsd_string(self.content.to_owned())?
|
||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&self.ap_id)?)
|
||||
.set_published(convert_datetime(self.published))
|
||||
.set_to(community.actor_id)
|
||||
.set_many_in_reply_tos(in_reply_to_vec)
|
||||
.set_content(self.content.to_owned())
|
||||
.set_attributed_to(creator.actor_id);
|
||||
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
comment.set_updated(convert_datetime(u));
|
||||
}
|
||||
|
||||
Ok(comment)
|
||||
|
@ -110,7 +113,7 @@ impl ToApub for Comment {
|
|||
self.deleted,
|
||||
&self.ap_id,
|
||||
self.updated,
|
||||
NoteType.to_string(),
|
||||
NoteType::Note.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -124,14 +127,25 @@ impl FromApub for CommentForm {
|
|||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
actor_id: &Url,
|
||||
) -> Result<CommentForm, LemmyError> {
|
||||
let oprops = ¬e.object_props;
|
||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
||||
let creator_actor_id = ¬e
|
||||
.attributed_to()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.unwrap();
|
||||
|
||||
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
|
||||
let creator = get_or_fetch_and_upsert_remote_user(creator_actor_id, client, pool).await?;
|
||||
|
||||
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
|
||||
let post_ap_id = in_reply_tos.next().unwrap().to_string();
|
||||
let mut in_reply_tos = note
|
||||
.in_reply_to()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_many()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|i| i.as_xsd_any_uri().unwrap());
|
||||
let post_ap_id = in_reply_tos.next().unwrap();
|
||||
|
||||
// This post, or the parent comment might not yet exist on this server yet, fetch them.
|
||||
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
|
||||
|
@ -140,7 +154,7 @@ impl FromApub for CommentForm {
|
|||
// For deeply nested comments, FromApub automatically gets called recursively
|
||||
let parent_id: Option<i32> = match in_reply_tos.next() {
|
||||
Some(parent_comment_uri) => {
|
||||
let parent_comment_ap_id = &parent_comment_uri.to_string();
|
||||
let parent_comment_ap_id = &parent_comment_uri;
|
||||
let parent_comment =
|
||||
get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?;
|
||||
|
||||
|
@ -153,20 +167,18 @@ impl FromApub for CommentForm {
|
|||
creator_id: creator.id,
|
||||
post_id: post.id,
|
||||
parent_id,
|
||||
content: oprops
|
||||
.get_content_xsd_string()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap(),
|
||||
content: note
|
||||
.content()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
removed: None,
|
||||
read: None,
|
||||
published: oprops
|
||||
.get_published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: oprops
|
||||
.get_updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
published: note.published().map(|u| u.to_owned().naive_local()),
|
||||
updated: note.updated().map(|u| u.to_owned().naive_local()),
|
||||
deleted: None,
|
||||
ap_id: oprops.get_id().unwrap().to_string(),
|
||||
ap_id: note.id(actor_id.domain().unwrap())?.unwrap().to_string(),
|
||||
local: false,
|
||||
})
|
||||
}
|
||||
|
@ -193,18 +205,24 @@ impl ApubObjectType for Comment {
|
|||
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
|
||||
|
||||
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut create = Create::new();
|
||||
populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
|
||||
|
||||
// Set the mention tags
|
||||
create.object_props.set_many_tag_base_boxes(maa.tags)?;
|
||||
|
||||
let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(maa.addressed_ccs.to_owned())
|
||||
// Set the mention tags
|
||||
.set_many_tags(maa.get_tags()?);
|
||||
|
||||
send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?;
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
maa.inboxes,
|
||||
create.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -227,18 +245,24 @@ impl ApubObjectType for Comment {
|
|||
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
|
||||
|
||||
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut update = Update::new();
|
||||
populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
|
||||
|
||||
// Set the mention tags
|
||||
update.object_props.set_many_tag_base_boxes(maa.tags)?;
|
||||
|
||||
let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(maa.addressed_ccs.to_owned())
|
||||
// Set the mention tags
|
||||
.set_many_tags(maa.get_tags()?);
|
||||
|
||||
send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?;
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
maa.inboxes,
|
||||
update.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -257,24 +281,18 @@ impl ApubObjectType for Comment {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut delete = Delete::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
delete,
|
||||
delete.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -298,40 +316,28 @@ impl ApubObjectType for Comment {
|
|||
|
||||
// Generate a fake delete activity, with the correct object
|
||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut delete = Delete::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -354,24 +360,18 @@ impl ApubObjectType for Comment {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut remove = Remove::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&mod_,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
remove,
|
||||
remove.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -395,39 +395,27 @@ impl ApubObjectType for Comment {
|
|||
|
||||
// Generate a fake delete activity, with the correct object
|
||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut remove = Remove::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(remove)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&mod_,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -454,22 +442,18 @@ impl ApubLikeableType for Comment {
|
|||
|
||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut like = Like::new();
|
||||
populate_object_props(
|
||||
&mut like.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
like,
|
||||
like.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -493,22 +477,18 @@ impl ApubLikeableType for Comment {
|
|||
|
||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut dislike = Dislike::new();
|
||||
populate_object_props(
|
||||
&mut dislike.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
dislike
|
||||
.dislike_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
dislike,
|
||||
dislike.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -532,38 +512,28 @@ impl ApubLikeableType for Comment {
|
|||
|
||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut like = Like::new();
|
||||
populate_object_props(
|
||||
&mut like.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(like)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -578,6 +548,16 @@ struct MentionsAndAddresses {
|
|||
tags: Vec<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,
|
||||
/// and mention tags, so they know where to be sent to.
|
||||
/// Addresses are the users / addresses that go in the cc field.
|
||||
|
@ -604,17 +584,14 @@ async fn collect_non_local_mentions_and_addresses(
|
|||
// TODO should it be fetching it every time?
|
||||
if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
|
||||
debug!("mention actor_id: {}", actor_id);
|
||||
addressed_ccs.push(actor_id.to_owned());
|
||||
addressed_ccs.push(actor_id.to_owned().to_string());
|
||||
|
||||
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
|
||||
let shared_inbox = mention_user.get_shared_inbox_url();
|
||||
|
||||
mention_inboxes.push(shared_inbox);
|
||||
let mut mention_tag = Mention::new();
|
||||
mention_tag
|
||||
.link_props
|
||||
.set_href(actor_id)?
|
||||
.set_name_xsd_string(mention.full_name())?;
|
||||
mention_tag.set_href(actor_id).set_name(mention.full_name());
|
||||
tags.push(mention_tag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity},
|
||||
activities::send_activity,
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
|
@ -18,22 +18,16 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
activity::{Accept, Announce, Delete, Remove, Undo},
|
||||
Activity,
|
||||
Base,
|
||||
BaseBox,
|
||||
};
|
||||
use activitystreams_ext::Ext2;
|
||||
use activitystreams_new::{
|
||||
activity::Follow,
|
||||
activity::{Accept, Announce, Delete, Follow, Remove, Undo},
|
||||
actor::{kind::GroupType, ApActor, Endpoints, Group},
|
||||
base::BaseExt,
|
||||
base::{AnyBase, BaseExt},
|
||||
collection::UnorderedCollection,
|
||||
context,
|
||||
object::Tombstone,
|
||||
prelude::*,
|
||||
primitives::{XsdAnyUri, XsdDateTime},
|
||||
public,
|
||||
};
|
||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
||||
use itertools::Itertools;
|
||||
|
@ -44,8 +38,8 @@ use lemmy_db::{
|
|||
user::User_,
|
||||
};
|
||||
use lemmy_utils::convert_datetime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Debug, str::FromStr};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommunityQuery {
|
||||
|
@ -72,13 +66,13 @@ impl ToApub for Community {
|
|||
let mut group = Group::new();
|
||||
group
|
||||
.set_context(context())
|
||||
.set_id(XsdAnyUri::from_str(&self.actor_id)?)
|
||||
.set_id(Url::parse(&self.actor_id)?)
|
||||
.set_name(self.name.to_owned())
|
||||
.set_published(XsdDateTime::from(convert_datetime(self.published)))
|
||||
.set_published(convert_datetime(self.published))
|
||||
.set_many_attributed_tos(moderators);
|
||||
|
||||
if let Some(u) = self.updated.to_owned() {
|
||||
group.set_updated(XsdDateTime::from(convert_datetime(u)));
|
||||
group.set_updated(convert_datetime(u));
|
||||
}
|
||||
if let Some(d) = self.description.to_owned() {
|
||||
// TODO: this should be html, also add source field with raw markdown
|
||||
|
@ -117,14 +111,14 @@ impl ToApub for Community {
|
|||
self.deleted,
|
||||
&self.actor_id,
|
||||
self.updated,
|
||||
GroupType.to_string(),
|
||||
GroupType::Group.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ActorType for Community {
|
||||
fn actor_id(&self) -> String {
|
||||
fn actor_id_str(&self) -> String {
|
||||
self.actor_id.to_owned()
|
||||
}
|
||||
|
||||
|
@ -138,27 +132,23 @@ impl ActorType for Community {
|
|||
/// As a local community, accept the follow request from a remote user.
|
||||
async fn send_accept_follow(
|
||||
&self,
|
||||
follow: &Follow,
|
||||
follow: Follow,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<(), LemmyError> {
|
||||
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
|
||||
let actor_uri = follow.actor()?.as_single_xsd_any_uri().unwrap().to_string();
|
||||
let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut accept = Accept::new();
|
||||
accept
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
|
||||
let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
|
||||
let to = format!("{}/inbox", actor_uri);
|
||||
accept
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
insert_activity(self.creator_id, accept.clone(), true, pool).await?;
|
||||
|
||||
send_activity(client, &accept, self, vec![to]).await?;
|
||||
send_activity(client, &accept.into_any_base()?, self, vec![to]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -172,17 +162,12 @@ impl ActorType for Community {
|
|||
|
||||
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut delete = Delete::default();
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
insert_activity(self.creator_id, delete.clone(), true, pool).await?;
|
||||
|
||||
|
@ -191,7 +176,7 @@ impl ActorType for Community {
|
|||
// Note: For an accept, since it was automatic, no one pushed a button,
|
||||
// the community was the actor.
|
||||
// But for delete, the creator is the actor, and does the signing
|
||||
send_activity(client, &delete, creator, inboxes).await?;
|
||||
send_activity(client, &delete.into_any_base()?, creator, inboxes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -205,33 +190,22 @@ impl ActorType for Community {
|
|||
|
||||
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut delete = Delete::default();
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
insert_activity(self.creator_id, undo.clone(), true, pool).await?;
|
||||
|
||||
|
@ -240,7 +214,7 @@ impl ActorType for Community {
|
|||
// Note: For an accept, since it was automatic, no one pushed a button,
|
||||
// the community was the actor.
|
||||
// But for delete, the creator is the actor, and does the signing
|
||||
send_activity(client, &undo, creator, inboxes).await?;
|
||||
send_activity(client, &undo.into_any_base()?, creator, inboxes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -254,17 +228,12 @@ impl ActorType for Community {
|
|||
|
||||
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut remove = Remove::default();
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
insert_activity(mod_.id, remove.clone(), true, pool).await?;
|
||||
|
||||
|
@ -273,7 +242,7 @@ impl ActorType for Community {
|
|||
// Note: For an accept, since it was automatic, no one pushed a button,
|
||||
// the community was the actor.
|
||||
// But for delete, the creator is the actor, and does the signing
|
||||
send_activity(client, &remove, mod_, inboxes).await?;
|
||||
send_activity(client, &remove.into_any_base()?, mod_, inboxes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -287,32 +256,21 @@ impl ActorType for Community {
|
|||
|
||||
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut remove = Remove::default();
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![self.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(remove)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![self.get_followers_url()]);
|
||||
|
||||
insert_activity(mod_.id, undo.clone(), true, pool).await?;
|
||||
|
||||
|
@ -321,7 +279,7 @@ impl ActorType for Community {
|
|||
// Note: For an accept, since it was automatic, no one pushed a button,
|
||||
// the community was the actor.
|
||||
// But for remove , the creator is the actor, and does the signing
|
||||
send_activity(client, &undo, mod_, inboxes).await?;
|
||||
send_activity(client, &undo.into_any_base()?, mod_, inboxes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -335,7 +293,7 @@ impl ActorType for Community {
|
|||
.await??;
|
||||
let inboxes = inboxes
|
||||
.into_iter()
|
||||
.map(|c| get_shared_inbox(&c.user_actor_id))
|
||||
.map(|c| get_shared_inbox(&Url::parse(&c.user_actor_id).unwrap()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.unique()
|
||||
.collect();
|
||||
|
@ -367,8 +325,13 @@ impl FromApub for CommunityForm {
|
|||
type ApubType = GroupExt;
|
||||
|
||||
/// Parse an ActivityPub group received from another instance into a Lemmy community.
|
||||
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
|
||||
let creator_and_moderator_uris = group.attributed_to().unwrap();
|
||||
async fn from_apub(
|
||||
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
|
||||
.as_many()
|
||||
.unwrap()
|
||||
|
@ -378,26 +341,37 @@ impl FromApub for CommunityForm {
|
|||
.as_xsd_any_uri()
|
||||
.unwrap();
|
||||
|
||||
let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
|
||||
let creator = get_or_fetch_and_upsert_remote_user(creator_uri, client, pool).await?;
|
||||
|
||||
Ok(CommunityForm {
|
||||
name: group.name().unwrap().as_single_xsd_string().unwrap().into(),
|
||||
name: group
|
||||
.inner
|
||||
.name()
|
||||
.unwrap()
|
||||
.as_one()
|
||||
.unwrap()
|
||||
.as_xsd_string()
|
||||
.unwrap()
|
||||
.into(),
|
||||
title: group.inner.preferred_username().unwrap().to_string(),
|
||||
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
|
||||
// -> same for post.content etc
|
||||
description: group
|
||||
.inner
|
||||
.content()
|
||||
.map(|s| s.as_single_xsd_string().unwrap().into()),
|
||||
category_id: group.ext_one.category.identifier.parse::<i32>()?,
|
||||
creator_id: creator.id,
|
||||
removed: None,
|
||||
published: group
|
||||
.published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
|
||||
published: group.inner.published().map(|u| u.to_owned().naive_local()),
|
||||
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
|
||||
deleted: None,
|
||||
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,
|
||||
private_key: None,
|
||||
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))
|
||||
}
|
||||
|
||||
pub async fn do_announce<A>(
|
||||
activity: A,
|
||||
pub async fn do_announce(
|
||||
activity: AnyBase,
|
||||
community: &Community,
|
||||
sender: &dyn ActorType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError>
|
||||
where
|
||||
A: Activity + Base + Serialize + Debug,
|
||||
{
|
||||
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()),
|
||||
)?;
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let id = format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4());
|
||||
let mut announce = Announce::new(community.actor_id.to_owned(), activity);
|
||||
announce
|
||||
.announce_props
|
||||
.set_actor_xsd_any_uri(community.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
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()
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ use crate::{
|
|||
routes::{ChatServerParam, DbPoolParam},
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::activity::Undo;
|
||||
use activitystreams_new::activity::Follow;
|
||||
use activitystreams_new::{
|
||||
activity::{Follow, Undo},
|
||||
prelude::*,
|
||||
};
|
||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
||||
use lemmy_db::{
|
||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
||||
|
@ -32,14 +34,9 @@ impl CommunityAcceptedObjects {
|
|||
fn follow(&self) -> Result<Follow, LemmyError> {
|
||||
match self {
|
||||
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
|
||||
CommunityAcceptedObjects::Undo(u) => Ok(
|
||||
u.undo_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.into_concrete::<Follow>()?,
|
||||
),
|
||||
CommunityAcceptedObjects::Undo(u) => {
|
||||
Ok(Follow::from_any_base(u.object().as_one().unwrap().to_owned())?.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,11 +69,11 @@ pub async fn community_inbox(
|
|||
&community.name, &input
|
||||
);
|
||||
let follow = input.follow()?;
|
||||
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
|
||||
let community_uri = follow.object.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();
|
||||
|
||||
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)?;
|
||||
|
||||
|
@ -108,7 +105,7 @@ async fn handle_follow(
|
|||
})
|
||||
.await?;
|
||||
|
||||
community.send_accept_follow(&follow, &client, &db).await?;
|
||||
community.send_accept_follow(follow, &client, &db).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::LemmyError;
|
||||
use activitystreams::{ext::Extension, Actor};
|
||||
use activitystreams_ext::UnparsedExtension;
|
||||
use activitystreams_new::unparsed::UnparsedMutExt;
|
||||
use diesel::PgConnection;
|
||||
use lemmy_db::{category::Category, Crud};
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use activitystreams::{ext::Extension, Base};
|
||||
use activitystreams_ext::UnparsedExtension;
|
||||
use activitystreams_new::unparsed::UnparsedMutExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
|
@ -8,4 +9,22 @@ pub struct PageExtension {
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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 http_signature_normalization_actix::{
|
||||
digest::{DigestClient, SignExt},
|
||||
|
@ -24,7 +25,7 @@ pub async fn sign(
|
|||
actor: &dyn ActorType,
|
||||
activity: String,
|
||||
) -> 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 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,7 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::object::Note;
|
||||
use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
|
||||
use activitystreams_new::{base::BaseExt, object::Note, prelude::*};
|
||||
use actix_web::client::Client;
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{result::Error::NotFound, PgConnection};
|
||||
|
@ -137,9 +136,10 @@ pub async fn search_by_apub_id(
|
|||
users: vec![],
|
||||
};
|
||||
|
||||
let domain = query_url.domain().unwrap();
|
||||
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
|
||||
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?;
|
||||
|
||||
|
@ -148,10 +148,9 @@ pub async fn search_by_apub_id(
|
|||
response
|
||||
}
|
||||
SearchAcceptedObjects::Group(g) => {
|
||||
let community_uri = g.inner.id().unwrap().to_string();
|
||||
let community_uri = g.inner.id(domain)?.unwrap();
|
||||
|
||||
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?;
|
||||
|
||||
// TODO Maybe at some point in the future, fetch all the history of a community
|
||||
// fetch_community_outbox(&c, conn)?;
|
||||
|
@ -165,7 +164,7 @@ pub async fn search_by_apub_id(
|
|||
response
|
||||
}
|
||||
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??;
|
||||
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
|
||||
}
|
||||
SearchAcceptedObjects::Comment(c) => {
|
||||
let post_url = c
|
||||
.object_props
|
||||
.get_many_in_reply_to_xsd_any_uris()
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let post_url = c.in_reply_to().as_ref().unwrap().as_many().unwrap();
|
||||
|
||||
// TODO: also fetch parent comments if any
|
||||
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
|
||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
||||
let comment_form = CommentForm::from_apub(&c, client, pool).await?;
|
||||
let x = post_url.first().unwrap().as_xsd_any_uri().unwrap();
|
||||
let post = fetch_remote_object(client, x).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??;
|
||||
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.
|
||||
pub async fn get_or_fetch_and_upsert_remote_user(
|
||||
apub_id: &str,
|
||||
apub_id: &Url,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<User_, LemmyError> {
|
||||
let apub_id_owned = apub_id.to_owned();
|
||||
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?;
|
||||
|
||||
|
@ -214,9 +208,9 @@ pub async fn get_or_fetch_and_upsert_remote_user(
|
|||
// If its older than a day, re-fetch it
|
||||
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
|
||||
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());
|
||||
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),
|
||||
Err(NotFound {}) => {
|
||||
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??;
|
||||
|
||||
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.
|
||||
pub async fn get_or_fetch_and_upsert_remote_community(
|
||||
apub_id: &str,
|
||||
apub_id: &Url,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Community, LemmyError> {
|
||||
let apub_id_owned = apub_id.to_owned();
|
||||
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?;
|
||||
|
||||
match community {
|
||||
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
|
||||
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());
|
||||
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),
|
||||
Err(NotFound {}) => {
|
||||
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??;
|
||||
|
||||
// Also add the community moderators too
|
||||
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()
|
||||
.unwrap()
|
||||
.iter()
|
||||
|
@ -293,7 +287,7 @@ pub async fn get_or_fetch_and_upsert_remote_community(
|
|||
let mut creator_and_moderators = Vec::new();
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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(
|
||||
post_ap_id: &str,
|
||||
post_ap_id: &Url,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Post, LemmyError> {
|
||||
let post_ap_id_owned = post_ap_id.to_owned();
|
||||
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?;
|
||||
|
||||
|
@ -342,8 +336,8 @@ pub async fn get_or_fetch_and_insert_remote_post(
|
|||
Ok(p) => Ok(p),
|
||||
Err(NotFound {}) => {
|
||||
debug!("Fetching and creating remote post: {}", post_ap_id);
|
||||
let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
|
||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
||||
let post = fetch_remote_object::<PageExt>(client, post_ap_id).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??;
|
||||
|
||||
|
@ -363,13 +357,13 @@ fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Com
|
|||
}
|
||||
|
||||
pub async fn get_or_fetch_and_insert_remote_comment(
|
||||
comment_ap_id: &str,
|
||||
comment_ap_id: &Url,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Comment, LemmyError> {
|
||||
let comment_ap_id_owned = comment_ap_id.to_owned();
|
||||
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?;
|
||||
|
||||
|
@ -380,8 +374,8 @@ pub async fn get_or_fetch_and_insert_remote_comment(
|
|||
"Fetching and creating remote comment and its parents: {}",
|
||||
comment_ap_id
|
||||
);
|
||||
let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
|
||||
let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
|
||||
let comment = fetch_remote_object::<Note>(client, comment_ap_id).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??;
|
||||
|
||||
|
|
|
@ -22,12 +22,11 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::object::Page;
|
||||
use activitystreams_ext::{Ext1, Ext2};
|
||||
use activitystreams_new::{
|
||||
activity::Follow,
|
||||
actor::{ApActor, Group, Person},
|
||||
object::Tombstone,
|
||||
object::{Page, Tombstone},
|
||||
prelude::*,
|
||||
};
|
||||
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 log::debug;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
|
||||
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
|
||||
|
@ -115,7 +114,7 @@ fn create_tombstone(
|
|||
let mut tombstone = Tombstone::new();
|
||||
tombstone.set_id(object_id.parse()?);
|
||||
tombstone.set_former_type(former_type);
|
||||
tombstone.set_deleted(convert_datetime(updated).into());
|
||||
tombstone.set_deleted(convert_datetime(updated));
|
||||
Ok(tombstone)
|
||||
} else {
|
||||
Err(format_err!("Cant convert to tombstone because updated time was None.").into())
|
||||
|
@ -132,6 +131,7 @@ pub trait FromApub {
|
|||
apub: &Self::ApubType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
actor_id: &Url,
|
||||
) -> Result<Self, LemmyError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
@ -199,13 +199,12 @@ pub trait ApubLikeableType {
|
|||
) -> Result<(), LemmyError>;
|
||||
}
|
||||
|
||||
pub fn get_shared_inbox(actor_id: &str) -> String {
|
||||
let url = Url::parse(actor_id).unwrap();
|
||||
pub fn get_shared_inbox(actor_id: &Url) -> String {
|
||||
format!(
|
||||
"{}://{}{}/inbox",
|
||||
&url.scheme(),
|
||||
&url.host_str().unwrap(),
|
||||
if let Some(port) = url.port() {
|
||||
&actor_id.scheme(),
|
||||
&actor_id.host_str().unwrap(),
|
||||
if let Some(port) = actor_id.port() {
|
||||
format!(":{}", port)
|
||||
} else {
|
||||
"".to_string()
|
||||
|
@ -215,7 +214,7 @@ pub fn get_shared_inbox(actor_id: &str) -> String {
|
|||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait ActorType {
|
||||
fn actor_id(&self) -> String;
|
||||
fn actor_id_str(&self) -> String;
|
||||
|
||||
fn public_key(&self) -> String;
|
||||
fn private_key(&self) -> String;
|
||||
|
@ -239,7 +238,7 @@ pub trait ActorType {
|
|||
#[allow(unused_variables)]
|
||||
async fn send_accept_follow(
|
||||
&self,
|
||||
follow: &Follow,
|
||||
follow: Follow,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<(), LemmyError>;
|
||||
|
@ -273,35 +272,40 @@ pub trait ActorType {
|
|||
/// For a given community, returns the inboxes of all followers.
|
||||
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
|
||||
|
||||
// TODO move these to the db rows
|
||||
fn get_inbox_url(&self) -> String {
|
||||
format!("{}/inbox", &self.actor_id())
|
||||
fn actor_id(&self) -> Result<Url, ParseError> {
|
||||
Url::parse(&self.actor_id_str())
|
||||
}
|
||||
|
||||
// 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 {
|
||||
get_shared_inbox(&self.actor_id())
|
||||
get_shared_inbox(&self.actor_id().unwrap())
|
||||
}
|
||||
|
||||
fn get_outbox_url(&self) -> String {
|
||||
format!("{}/outbox", &self.actor_id())
|
||||
format!("{}/outbox", &self.actor_id_str())
|
||||
}
|
||||
|
||||
fn get_followers_url(&self) -> String {
|
||||
format!("{}/followers", &self.actor_id())
|
||||
format!("{}/followers", &self.actor_id_str())
|
||||
}
|
||||
|
||||
fn get_following_url(&self) -> String {
|
||||
format!("{}/following", &self.actor_id())
|
||||
format!("{}/following", &self.actor_id_str())
|
||||
}
|
||||
|
||||
fn get_liked_url(&self) -> String {
|
||||
format!("{}/liked", &self.actor_id())
|
||||
format!("{}/liked", &self.actor_id_str())
|
||||
}
|
||||
|
||||
fn get_public_key_ext(&self) -> PublicKeyExtension {
|
||||
PublicKey {
|
||||
id: format!("{}#main-key", self.actor_id()),
|
||||
owner: self.actor_id(),
|
||||
id: format!("{}#main-key", self.actor_id_str()),
|
||||
owner: self.actor_id_str(),
|
||||
public_key_pem: self.public_key(),
|
||||
}
|
||||
.to_ext()
|
||||
|
@ -311,7 +315,7 @@ pub trait ActorType {
|
|||
pub async fn fetch_webfinger_url(
|
||||
mention: &MentionData,
|
||||
client: &Client,
|
||||
) -> Result<String, LemmyError> {
|
||||
) -> Result<Url, LemmyError> {
|
||||
let fetch_url = format!(
|
||||
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
|
||||
get_apub_protocol_string(),
|
||||
|
@ -336,6 +340,8 @@ pub async fn fetch_webfinger_url(
|
|||
link
|
||||
.href
|
||||
.to_owned()
|
||||
.map(|u| Url::parse(&u))
|
||||
.transpose()?
|
||||
.ok_or_else(|| format_err!("No href found.").into())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
apub::{
|
||||
activities::{populate_object_props, send_activity_to_community},
|
||||
activities::send_activity_to_community,
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
create_tombstone,
|
||||
|
@ -18,14 +18,14 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
use activitystreams_ext::Ext1;
|
||||
use activitystreams_new::{
|
||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
||||
context,
|
||||
object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page},
|
||||
BaseBox,
|
||||
object::{kind::PageType, Image, Page, Tombstone},
|
||||
prelude::*,
|
||||
public,
|
||||
};
|
||||
use activitystreams_ext::Ext1;
|
||||
use activitystreams_new::object::Tombstone;
|
||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
||||
use lemmy_db::{
|
||||
community::Community,
|
||||
|
@ -35,6 +35,7 @@ use lemmy_db::{
|
|||
};
|
||||
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
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.
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
|
||||
let mut page = Page::default();
|
||||
let oprops: &mut ObjectProperties = page.as_mut();
|
||||
let mut page = Page::new();
|
||||
|
||||
let creator_id = self.creator_id;
|
||||
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 = 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)
|
||||
// TODO: need to set proper context defining sensitive/commentsEnabled fields
|
||||
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(self.ap_id.to_owned())?
|
||||
.set_context(context())
|
||||
.set_id(self.ap_id.parse::<Url>()?)
|
||||
// Use summary field to be consistent with mastodon content warning.
|
||||
// https://mastodon.xyz/@Louisa/103987265222901387.json
|
||||
.set_summary_xsd_string(self.name.to_owned())?
|
||||
.set_published(convert_datetime(self.published))?
|
||||
.set_to_xsd_any_uri(community.actor_id)?
|
||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
||||
.set_summary(self.name.to_owned())
|
||||
.set_published(convert_datetime(self.published))
|
||||
.set_to(community.actor_id)
|
||||
.set_attributed_to(creator.actor_id);
|
||||
|
||||
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("")
|
||||
// https://github.com/LemmyNet/lemmy/issues/602
|
||||
let url = self.url.as_ref().filter(|u| !u.is_empty());
|
||||
if let Some(u) = url {
|
||||
oprops.set_url_xsd_any_uri(u.to_owned())?;
|
||||
page.set_url(u.to_owned());
|
||||
|
||||
// Embeds
|
||||
let mut page_preview = Page::new();
|
||||
page_preview
|
||||
.object_props
|
||||
.set_url_xsd_any_uri(u.to_owned())?;
|
||||
page_preview.set_url(u.to_owned());
|
||||
|
||||
if let Some(embed_title) = &self.embed_title {
|
||||
page_preview
|
||||
.object_props
|
||||
.set_name_xsd_string(embed_title.to_owned())?;
|
||||
page_preview.set_name(embed_title.to_owned());
|
||||
}
|
||||
|
||||
if let Some(embed_description) = &self.embed_description {
|
||||
page_preview
|
||||
.object_props
|
||||
.set_summary_xsd_string(embed_description.to_owned())?;
|
||||
page_preview.set_summary(embed_description.to_owned());
|
||||
}
|
||||
|
||||
if let Some(embed_html) = &self.embed_html {
|
||||
page_preview
|
||||
.object_props
|
||||
.set_content_xsd_string(embed_html.to_owned())?;
|
||||
page_preview.set_content(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 {
|
||||
|
@ -130,13 +122,12 @@ impl ToApub for Post {
|
|||
);
|
||||
|
||||
let mut image = Image::new();
|
||||
image.object_props.set_url_xsd_any_uri(full_url)?;
|
||||
let any_image = AnyImage::from_concrete(image)?;
|
||||
oprops.set_image_any_image(any_image)?;
|
||||
image.set_url(full_url);
|
||||
page.set_image(image.into_any_base()?);
|
||||
}
|
||||
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
page.set_updated(convert_datetime(u));
|
||||
}
|
||||
|
||||
let ext = PageExtension {
|
||||
|
@ -151,7 +142,7 @@ impl ToApub for Post {
|
|||
self.deleted,
|
||||
&self.ap_id,
|
||||
self.updated,
|
||||
PageType.to_string(),
|
||||
PageType::Page.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -165,63 +156,92 @@ impl FromApub for PostForm {
|
|||
page: &PageExt,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
actor_id: &Url,
|
||||
) -> Result<PostForm, LemmyError> {
|
||||
let ext = &page.ext_one;
|
||||
let oprops = &page.inner.object_props;
|
||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
||||
let creator_actor_id = page
|
||||
.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 =
|
||||
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() {
|
||||
Some(any_image) => any_image
|
||||
.to_owned()
|
||||
.into_concrete::<Image>()?
|
||||
.object_props
|
||||
.get_url_xsd_any_uri()
|
||||
let thumbnail_url = match &page.inner.image() {
|
||||
Some(any_image) => Image::from_any_base(any_image.to_owned().as_one().unwrap().to_owned())?
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let url = oprops.get_url_xsd_any_uri().map(|u| u.to_string());
|
||||
let (embed_title, embed_description, embed_html) = match oprops.get_preview_base_box() {
|
||||
let (embed_title, embed_description, embed_html) = match page.inner.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
|
||||
.object_props
|
||||
.get_name_xsd_string()
|
||||
.map(|n| n.to_string());
|
||||
.name()
|
||||
.map(|n| n.as_one().unwrap().as_xsd_string().unwrap().to_string());
|
||||
let summary = preview_page
|
||||
.object_props
|
||||
.get_summary_xsd_string()
|
||||
.map(|s| s.to_string());
|
||||
.summary()
|
||||
.map(|s| s.as_single_xsd_string().unwrap().to_string());
|
||||
let content = preview_page
|
||||
.object_props
|
||||
.get_content_xsd_string()
|
||||
.map(|c| c.to_string());
|
||||
.content()
|
||||
.map(|c| c.as_single_xsd_string().unwrap().to_string());
|
||||
(name, summary, content)
|
||||
}
|
||||
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 {
|
||||
name: oprops.get_summary_xsd_string().unwrap().to_string(),
|
||||
name: page
|
||||
.inner
|
||||
.summary()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
url,
|
||||
body: oprops.get_content_xsd_string().map(|c| c.to_string()),
|
||||
body,
|
||||
creator_id: creator.id,
|
||||
community_id: community.id,
|
||||
removed: None,
|
||||
locked: Some(!ext.comments_enabled),
|
||||
published: oprops
|
||||
.get_published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: oprops
|
||||
.get_updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
published: page
|
||||
.inner
|
||||
.published()
|
||||
.as_ref()
|
||||
.map(|u| u.to_owned().naive_local()),
|
||||
updated: page
|
||||
.inner
|
||||
.updated()
|
||||
.as_ref()
|
||||
.map(|u| u.to_owned().naive_local()),
|
||||
deleted: None,
|
||||
nsfw: ext.sensitive,
|
||||
stickied: None, // -> put it in "featured" collection of the community
|
||||
|
@ -229,7 +249,11 @@ impl FromApub for PostForm {
|
|||
embed_description,
|
||||
embed_html,
|
||||
thumbnail_url,
|
||||
ap_id: oprops.get_id().unwrap().to_string(),
|
||||
ap_id: page
|
||||
.inner
|
||||
.id(actor_id.domain().unwrap())?
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
local: false,
|
||||
})
|
||||
}
|
||||
|
@ -251,22 +275,18 @@ impl ApubObjectType for Post {
|
|||
|
||||
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut create = Create::new();
|
||||
populate_object_props(
|
||||
&mut create.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
create,
|
||||
create.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -288,22 +308,18 @@ impl ApubObjectType for Post {
|
|||
|
||||
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut update = Update::new();
|
||||
populate_object_props(
|
||||
&mut update.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
update,
|
||||
update.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -323,24 +339,18 @@ impl ApubObjectType for Post {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut delete = Delete::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
delete,
|
||||
delete.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -360,40 +370,28 @@ impl ApubObjectType for Post {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut delete = Delete::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut delete.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -413,24 +411,18 @@ impl ApubObjectType for Post {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut remove = Remove::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
mod_,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
remove,
|
||||
remove.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -450,39 +442,27 @@ impl ApubObjectType for Post {
|
|||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
||||
|
||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut remove = Remove::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut remove.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
|
||||
let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(remove)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
mod_,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -506,22 +486,18 @@ impl ApubLikeableType for Post {
|
|||
|
||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut like = Like::new();
|
||||
populate_object_props(
|
||||
&mut like.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
like,
|
||||
like.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -542,22 +518,18 @@ impl ApubLikeableType for Post {
|
|||
|
||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut dislike = Dislike::new();
|
||||
populate_object_props(
|
||||
&mut dislike.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
dislike
|
||||
.dislike_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
dislike,
|
||||
dislike.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
@ -578,38 +550,28 @@ impl ApubLikeableType for Post {
|
|||
|
||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
|
||||
let mut like = Like::new();
|
||||
populate_object_props(
|
||||
&mut like.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&id,
|
||||
)?;
|
||||
let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
||||
let mut undo = Undo::default();
|
||||
|
||||
populate_object_props(
|
||||
&mut undo.object_props,
|
||||
vec![community.get_followers_url()],
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(like)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(public())
|
||||
.set_many_ccs(vec![community.get_followers_url()]);
|
||||
|
||||
send_activity_to_community(
|
||||
&creator,
|
||||
&community,
|
||||
vec![community.get_shared_inbox_url()],
|
||||
undo,
|
||||
undo.into_any_base()?,
|
||||
client,
|
||||
pool,
|
||||
)
|
||||
|
|
|
@ -12,12 +12,12 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
use activitystreams_new::{
|
||||
activity::{Create, Delete, Undo, Update},
|
||||
context,
|
||||
object::{kind::NoteType, properties::ObjectProperties, Note},
|
||||
object::{kind::NoteType, Note, Tombstone},
|
||||
prelude::*,
|
||||
};
|
||||
use activitystreams_new::object::Tombstone;
|
||||
use actix_web::client::Client;
|
||||
use lemmy_db::{
|
||||
private_message::{PrivateMessage, PrivateMessageForm},
|
||||
|
@ -25,14 +25,14 @@ use lemmy_db::{
|
|||
Crud,
|
||||
};
|
||||
use lemmy_utils::convert_datetime;
|
||||
use url::Url;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ToApub for PrivateMessage {
|
||||
type Response = Note;
|
||||
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
||||
let mut private_message = Note::default();
|
||||
let oprops: &mut ObjectProperties = private_message.as_mut();
|
||||
let mut private_message = Note::new();
|
||||
|
||||
let creator_id = self.creator_id;
|
||||
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 = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
||||
|
||||
oprops
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(self.ap_id.to_owned())?
|
||||
.set_published(convert_datetime(self.published))?
|
||||
.set_content_xsd_string(self.content.to_owned())?
|
||||
.set_to_xsd_any_uri(recipient.actor_id)?
|
||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
||||
private_message
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&self.ap_id.to_owned())?)
|
||||
.set_published(convert_datetime(self.published))
|
||||
.set_content(self.content.to_owned())
|
||||
.set_to(recipient.actor_id)
|
||||
.set_attributed_to(creator.actor_id);
|
||||
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
private_message.set_updated(convert_datetime(u));
|
||||
}
|
||||
|
||||
Ok(private_message)
|
||||
|
@ -60,7 +60,7 @@ impl ToApub for PrivateMessage {
|
|||
self.deleted,
|
||||
&self.ap_id,
|
||||
self.updated,
|
||||
NoteType.to_string(),
|
||||
NoteType::Note.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -74,32 +74,35 @@ impl FromApub for PrivateMessageForm {
|
|||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
actor_id: &Url,
|
||||
) -> Result<PrivateMessageForm, LemmyError> {
|
||||
let oprops = ¬e.object_props;
|
||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
||||
let creator_actor_id = note
|
||||
.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 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?;
|
||||
|
||||
Ok(PrivateMessageForm {
|
||||
creator_id: creator.id,
|
||||
recipient_id: recipient.id,
|
||||
content: oprops
|
||||
.get_content_xsd_string()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap(),
|
||||
published: oprops
|
||||
.get_published()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: oprops
|
||||
.get_updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
content: note
|
||||
.content()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
published: note.published().map(|u| u.to_owned().naive_local()),
|
||||
updated: note.updated().map(|u| u.to_owned().naive_local()),
|
||||
deleted: None,
|
||||
read: None,
|
||||
ap_id: oprops.get_id().unwrap().to_string(),
|
||||
ap_id: note.id(actor_id.domain().unwrap())?.unwrap().to_string(),
|
||||
local: false,
|
||||
})
|
||||
}
|
||||
|
@ -120,21 +123,16 @@ impl ApubObjectType for PrivateMessage {
|
|||
let recipient_id = self.recipient_id;
|
||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
||||
|
||||
let mut create = Create::new();
|
||||
create
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
let to = format!("{}/inbox", recipient.actor_id);
|
||||
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -151,21 +149,16 @@ impl ApubObjectType for PrivateMessage {
|
|||
let recipient_id = self.recipient_id;
|
||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
||||
|
||||
let mut update = Update::new();
|
||||
update
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
let to = format!("{}/inbox", recipient.actor_id);
|
||||
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -181,21 +174,16 @@ impl ApubObjectType for PrivateMessage {
|
|||
let recipient_id = self.recipient_id;
|
||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
||||
|
||||
let mut delete = Delete::new();
|
||||
delete
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
let to = format!("{}/inbox", recipient.actor_id);
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -211,36 +199,25 @@ impl ApubObjectType for PrivateMessage {
|
|||
let recipient_id = self.recipient_id;
|
||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
||||
|
||||
let mut delete = Delete::new();
|
||||
delete
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
|
||||
let to = format!("{}/inbox", recipient.actor_id);
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
// TODO
|
||||
// Undo that fake activity
|
||||
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
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(undo_id)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
.set_context(context())
|
||||
.set_id(Url::parse(&undo_id)?)
|
||||
.set_to(to.clone());
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,4 @@
|
|||
use crate::{
|
||||
api::claims::Claims,
|
||||
apub::{
|
||||
activities::send_activity,
|
||||
create_apub_response,
|
||||
|
@ -21,16 +20,15 @@ use activitystreams_new::{
|
|||
context,
|
||||
object::{Image, Tombstone},
|
||||
prelude::*,
|
||||
primitives::{XsdAnyUri, XsdDateTime},
|
||||
};
|
||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
||||
use failure::_core::str::FromStr;
|
||||
use lemmy_db::{
|
||||
naive_now,
|
||||
user::{UserForm, User_},
|
||||
};
|
||||
use lemmy_utils::convert_datetime;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserQuery {
|
||||
|
@ -47,12 +45,12 @@ impl ToApub for User_ {
|
|||
let mut person = Person::new();
|
||||
person
|
||||
.set_context(context())
|
||||
.set_id(XsdAnyUri::from_str(&self.actor_id)?)
|
||||
.set_id(Url::parse(&self.actor_id)?)
|
||||
.set_name(self.name.to_owned())
|
||||
.set_published(XsdDateTime::from(convert_datetime(self.published)));
|
||||
.set_published(convert_datetime(self.published));
|
||||
|
||||
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 {
|
||||
|
@ -85,7 +83,7 @@ impl ToApub for User_ {
|
|||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ActorType for User_ {
|
||||
fn actor_id(&self) -> String {
|
||||
fn actor_id_str(&self) -> String {
|
||||
self.actor_id.to_owned()
|
||||
}
|
||||
|
||||
|
@ -111,7 +109,7 @@ impl ActorType for User_ {
|
|||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -130,12 +128,12 @@ impl ActorType for User_ {
|
|||
// TODO
|
||||
// Undo that fake activity
|
||||
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()?);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -177,7 +175,7 @@ impl ActorType for User_ {
|
|||
|
||||
async fn send_accept_follow(
|
||||
&self,
|
||||
_follow: &Follow,
|
||||
_follow: Follow,
|
||||
_client: &Client,
|
||||
_pool: &DbPool,
|
||||
) -> Result<(), LemmyError> {
|
||||
|
@ -193,12 +191,17 @@ impl ActorType for User_ {
|
|||
impl FromApub for UserForm {
|
||||
type ApubType = PersonExt;
|
||||
/// 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() {
|
||||
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
|
@ -209,18 +212,18 @@ impl FromApub for UserForm {
|
|||
name: person
|
||||
.name()
|
||||
.unwrap()
|
||||
.as_single_xsd_string()
|
||||
.one()
|
||||
.unwrap()
|
||||
.into(),
|
||||
.as_xsd_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
|
||||
password_encrypted: "".to_string(),
|
||||
admin: false,
|
||||
banned: false,
|
||||
email: None,
|
||||
avatar,
|
||||
updated: person
|
||||
.updated()
|
||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||
updated: person.updated().map(|u| u.to_owned().naive_local()),
|
||||
show_nsfw: false,
|
||||
theme: "".to_string(),
|
||||
default_sort_type: 0,
|
||||
|
@ -229,8 +232,9 @@ impl FromApub for UserForm {
|
|||
show_avatars: false,
|
||||
send_notifications_to_email: false,
|
||||
matrix_user_id: None,
|
||||
actor_id: person.id().unwrap().to_string(),
|
||||
actor_id: person.id(actor_id.domain().unwrap())?.unwrap().to_string(),
|
||||
bio: person
|
||||
.inner
|
||||
.summary()
|
||||
.map(|s| s.as_single_xsd_string().unwrap().into()),
|
||||
local: false,
|
||||
|
@ -248,7 +252,7 @@ pub async fn get_apub_user_http(
|
|||
) -> Result<HttpResponse<Body>, LemmyError> {
|
||||
let user_name = info.into_inner().user_name;
|
||||
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??;
|
||||
let u = user.to_apub(&db).await?;
|
||||
|
|
|
@ -12,9 +12,10 @@ use crate::{
|
|||
DbPool,
|
||||
LemmyError,
|
||||
};
|
||||
use activitystreams::{
|
||||
use activitystreams_new::{
|
||||
activity::{Accept, Create, Delete, Undo, Update},
|
||||
object::Note,
|
||||
prelude::*,
|
||||
};
|
||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
||||
use lemmy_db::{
|
||||
|
@ -79,11 +80,7 @@ async fn receive_accept(
|
|||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let community_uri = accept
|
||||
.accept_props
|
||||
.get_actor_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let community_uri = accept.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
|
||||
let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
|
||||
verify(request, &community)?;
|
||||
|
@ -116,26 +113,15 @@ async fn receive_create_private_message(
|
|||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let note = create
|
||||
.create_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.into_concrete::<Note>()?;
|
||||
let user_uri = &create.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
let note = Note::from_any_base(create.object().as_one().unwrap().to_owned())?.unwrap();
|
||||
|
||||
let user_uri = create
|
||||
.create_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)?;
|
||||
|
||||
insert_activity(user.id, create, false, pool).await?;
|
||||
|
||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool, user_uri).await?;
|
||||
|
||||
let inserted_private_message = blocking(pool, move |conn| {
|
||||
PrivateMessage::create(conn, &private_message)
|
||||
|
@ -168,26 +154,15 @@ async fn receive_update_private_message(
|
|||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let note = update
|
||||
.update_props
|
||||
.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_uri = &update.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
let note = Note::from_any_base(update.object().as_one().unwrap().to_owned())?.unwrap();
|
||||
|
||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
||||
verify(request, &user)?;
|
||||
|
||||
insert_activity(user.id, update, false, pool).await?;
|
||||
|
||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool, user_uri).await?;
|
||||
|
||||
let private_message_ap_id = private_message_form.ap_id.clone();
|
||||
let private_message = blocking(pool, move |conn| {
|
||||
|
@ -228,26 +203,15 @@ async fn receive_delete_private_message(
|
|||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
|
||||
|
||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
||||
verify(request, &user)?;
|
||||
|
||||
insert_activity(user.id, delete, false, pool).await?;
|
||||
|
||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool, user_uri).await?;
|
||||
|
||||
let private_message_ap_id = private_message_form.ap_id;
|
||||
let private_message = blocking(pool, move |conn| {
|
||||
|
@ -300,34 +264,16 @@ async fn receive_undo_delete_private_message(
|
|||
pool: &DbPool,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let delete = undo
|
||||
.undo_props
|
||||
.get_object_base_box()
|
||||
.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 delete = Delete::from_any_base(undo.object().as_one().unwrap().to_owned())?.unwrap();
|
||||
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
|
||||
let user_uri = &delete.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
|
||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
||||
verify(request, &user)?;
|
||||
|
||||
insert_activity(user.id, delete, false, pool).await?;
|
||||
|
||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool, user_uri).await?;
|
||||
|
||||
let private_message_ap_id = private_message.ap_id.clone();
|
||||
let private_message_id = blocking(pool, move |conn| {
|
||||
|
|
|
@ -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
|
||||
let incorrect_users = user_
|
||||
.filter(actor_id.eq("http://fake.com"))
|
||||
.filter(actor_id.like("changeme_%"))
|
||||
.filter(local.eq(true))
|
||||
.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
|
||||
let incorrect_communities = community
|
||||
.filter(actor_id.eq("http://fake.com"))
|
||||
.filter(actor_id.like("changeme_%"))
|
||||
.filter(local.eq(true))
|
||||
.load::<Community>(conn)?;
|
||||
|
||||
|
|
|
@ -53,7 +53,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
|||
.route("", web::put().to(route_post::<EditCommunity>))
|
||||
.route("/list", web::get().to(route_get::<ListCommunities>))
|
||||
.route("/follow", web::post().to(route_post::<FollowCommunity>))
|
||||
.route("/delete", web::post().to(route_post::<DeleteCommunity>))
|
||||
// Mod Actions
|
||||
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
|
||||
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
|
||||
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
||||
.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())
|
||||
.route("", web::get().to(route_get::<GetPost>))
|
||||
.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("/like", web::post().to(route_post::<CreatePostLike>))
|
||||
.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())
|
||||
.route("", web::post().to(route_post::<CreateComment>))
|
||||
.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("/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())
|
||||
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
|
||||
.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
|
||||
.service(
|
||||
|
@ -107,7 +127,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
|||
.wrap(rate_limit.message())
|
||||
.route("", web::get().to(route_get::<GetUserDetails>))
|
||||
.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(
|
||||
"/followed_communities",
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &str = "v0.7.20";
|
||||
pub const VERSION: &str = "v0.7.26";
|
||||
|
|
|
@ -28,19 +28,28 @@ pub enum UserOperation {
|
|||
GetCommunity,
|
||||
CreateComment,
|
||||
EditComment,
|
||||
DeleteComment,
|
||||
RemoveComment,
|
||||
MarkCommentAsRead,
|
||||
SaveComment,
|
||||
CreateCommentLike,
|
||||
GetPosts,
|
||||
CreatePostLike,
|
||||
EditPost,
|
||||
DeletePost,
|
||||
RemovePost,
|
||||
LockPost,
|
||||
StickyPost,
|
||||
SavePost,
|
||||
EditCommunity,
|
||||
DeleteCommunity,
|
||||
RemoveCommunity,
|
||||
FollowCommunity,
|
||||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
MarkUserMentionAsRead,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
|
@ -59,6 +68,8 @@ pub enum UserOperation {
|
|||
PasswordChange,
|
||||
CreatePrivateMessage,
|
||||
EditPrivateMessage,
|
||||
DeletePrivateMessage,
|
||||
MarkPrivateMessageAsRead,
|
||||
GetPrivateMessages,
|
||||
UserJoin,
|
||||
GetComments,
|
||||
|
|
|
@ -212,6 +212,9 @@ impl ChatServer {
|
|||
|
||||
// Also leave all communities
|
||||
// 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() {
|
||||
sessions.remove(&id);
|
||||
}
|
||||
|
@ -443,18 +446,28 @@ impl ChatServer {
|
|||
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
||||
UserOperation::BanUser => do_user_operation::<BanUser>(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::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
|
||||
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(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 => {
|
||||
do_user_operation::<CreatePrivateMessage>(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::UserJoin => do_user_operation::<UserJoin>(args).await,
|
||||
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
|
||||
|
||||
// Site ops
|
||||
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
|
||||
|
@ -473,6 +486,8 @@ impl ChatServer {
|
|||
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
|
||||
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(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::GetFollowedCommunities => {
|
||||
do_user_operation::<GetFollowedCommunities>(args).await
|
||||
|
@ -485,12 +500,19 @@ impl ChatServer {
|
|||
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
|
||||
UserOperation::GetPosts => do_user_operation::<GetPosts>(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::SavePost => do_user_operation::<SavePost>(args).await,
|
||||
|
||||
// Comment ops
|
||||
UserOperation::CreateComment => do_user_operation::<CreateComment>(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::GetComments => do_user_operation::<GetComments>(args).await,
|
||||
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
|
||||
|
|
2
ui/package.json
vendored
2
ui/package.json
vendored
|
@ -37,6 +37,8 @@
|
|||
"markdown-it": "^10.0.0",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"prettier": "^2.0.4",
|
||||
|
|
168
ui/src/api_tests/api.spec.ts
vendored
168
ui/src/api_tests/api.spec.ts
vendored
|
@ -4,22 +4,28 @@ import {
|
|||
LoginForm,
|
||||
LoginResponse,
|
||||
PostForm,
|
||||
DeletePostForm,
|
||||
RemovePostForm,
|
||||
// TODO need to test LockPost and StickyPost federated
|
||||
PostResponse,
|
||||
SearchResponse,
|
||||
FollowCommunityForm,
|
||||
CommunityResponse,
|
||||
GetFollowedCommunitiesResponse,
|
||||
GetPostForm,
|
||||
GetPostResponse,
|
||||
CommentForm,
|
||||
DeleteCommentForm,
|
||||
RemoveCommentForm,
|
||||
CommentResponse,
|
||||
CommunityForm,
|
||||
GetCommunityForm,
|
||||
DeleteCommunityForm,
|
||||
RemoveCommunityForm,
|
||||
GetCommunityResponse,
|
||||
CommentLikeForm,
|
||||
CreatePostLikeForm,
|
||||
PrivateMessageForm,
|
||||
EditPrivateMessageForm,
|
||||
DeletePrivateMessageForm,
|
||||
PrivateMessageResponse,
|
||||
PrivateMessagesResponse,
|
||||
GetUserMentionsResponse,
|
||||
|
@ -97,7 +103,6 @@ describe('main', () => {
|
|||
name,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 2,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -266,7 +271,6 @@ describe('main', () => {
|
|||
name,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 3,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -323,7 +327,6 @@ describe('main', () => {
|
|||
edit_id: 2,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 3,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -382,7 +385,6 @@ describe('main', () => {
|
|||
let unlikeCommentForm: CommentLikeForm = {
|
||||
comment_id: createResponse.comment.id,
|
||||
score: 0,
|
||||
post_id: 2,
|
||||
auth: lemmyAlphaAuth,
|
||||
};
|
||||
|
||||
|
@ -585,7 +587,6 @@ describe('main', () => {
|
|||
name: postName,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createCommunityRes.community.id,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -620,19 +621,16 @@ describe('main', () => {
|
|||
expect(createCommentRes.comment.content).toBe(commentContent);
|
||||
|
||||
// lemmy_beta deletes the comment
|
||||
let deleteCommentForm: CommentForm = {
|
||||
content: commentContent,
|
||||
let deleteCommentForm: DeleteCommentForm = {
|
||||
edit_id: createCommentRes.comment.id,
|
||||
post_id: createPostRes.post.id,
|
||||
deleted: true,
|
||||
auth: lemmyBetaAuth,
|
||||
creator_id: createCommentRes.comment.creator_id,
|
||||
};
|
||||
|
||||
let deleteCommentRes: CommentResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/comment`,
|
||||
`${lemmyBetaApiUrl}/comment/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -649,19 +647,16 @@ describe('main', () => {
|
|||
expect(getPostRes.comments[0].deleted).toBe(true);
|
||||
|
||||
// lemmy_beta undeletes the comment
|
||||
let undeleteCommentForm: CommentForm = {
|
||||
content: commentContent,
|
||||
let undeleteCommentForm: DeleteCommentForm = {
|
||||
edit_id: createCommentRes.comment.id,
|
||||
post_id: createPostRes.post.id,
|
||||
deleted: false,
|
||||
auth: lemmyBetaAuth,
|
||||
creator_id: createCommentRes.comment.creator_id,
|
||||
};
|
||||
|
||||
let undeleteCommentRes: CommentResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/comment`,
|
||||
`${lemmyBetaApiUrl}/comment/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -677,23 +672,22 @@ describe('main', () => {
|
|||
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
|
||||
|
||||
// lemmy_beta deletes the post
|
||||
let deletePostForm: PostForm = {
|
||||
name: postName,
|
||||
let deletePostForm: DeletePostForm = {
|
||||
edit_id: createPostRes.post.id,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createPostRes.post.community_id,
|
||||
creator_id: createPostRes.post.creator_id,
|
||||
nsfw: false,
|
||||
deleted: true,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
|
||||
method: 'PUT',
|
||||
let deletePostRes: PostResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/post/delete`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(deletePostForm),
|
||||
}).then(d => d.json());
|
||||
}
|
||||
).then(d => d.json());
|
||||
expect(deletePostRes.post.deleted).toBe(true);
|
||||
|
||||
// Make sure lemmy_alpha sees the post is deleted
|
||||
|
@ -703,20 +697,16 @@ describe('main', () => {
|
|||
expect(getPostResAgain.post.deleted).toBe(true);
|
||||
|
||||
// lemmy_beta undeletes the post
|
||||
let undeletePostForm: PostForm = {
|
||||
name: postName,
|
||||
let undeletePostForm: DeletePostForm = {
|
||||
edit_id: createPostRes.post.id,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createPostRes.post.community_id,
|
||||
creator_id: createPostRes.post.creator_id,
|
||||
nsfw: false,
|
||||
deleted: false,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let undeletePostRes: PostResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/post`,
|
||||
`${lemmyBetaApiUrl}/post/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -732,20 +722,16 @@ describe('main', () => {
|
|||
expect(getPostResAgainTwo.post.deleted).toBe(false);
|
||||
|
||||
// lemmy_beta deletes the community
|
||||
let deleteCommunityForm: CommunityForm = {
|
||||
name: communityName,
|
||||
title: communityName,
|
||||
category_id: 1,
|
||||
let deleteCommunityForm: DeleteCommunityForm = {
|
||||
edit_id: createCommunityRes.community.id,
|
||||
nsfw: false,
|
||||
deleted: true,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let deleteResponse: CommunityResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/community`,
|
||||
`${lemmyBetaApiUrl}/community/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -765,20 +751,16 @@ describe('main', () => {
|
|||
expect(getCommunityRes.community.deleted).toBe(true);
|
||||
|
||||
// lemmy_beta undeletes the community
|
||||
let undeleteCommunityForm: CommunityForm = {
|
||||
name: communityName,
|
||||
title: communityName,
|
||||
category_id: 1,
|
||||
let undeleteCommunityForm: DeleteCommunityForm = {
|
||||
edit_id: createCommunityRes.community.id,
|
||||
nsfw: false,
|
||||
deleted: false,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let undeleteCommunityRes: CommunityResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/community`,
|
||||
`${lemmyBetaApiUrl}/community/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -861,7 +843,6 @@ describe('main', () => {
|
|||
name: postName,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createCommunityRes.community.id,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -896,19 +877,16 @@ describe('main', () => {
|
|||
expect(createCommentRes.comment.content).toBe(commentContent);
|
||||
|
||||
// lemmy_beta removes the comment
|
||||
let removeCommentForm: CommentForm = {
|
||||
content: commentContent,
|
||||
let removeCommentForm: RemoveCommentForm = {
|
||||
edit_id: createCommentRes.comment.id,
|
||||
post_id: createPostRes.post.id,
|
||||
removed: true,
|
||||
auth: lemmyBetaAuth,
|
||||
creator_id: createCommentRes.comment.creator_id,
|
||||
};
|
||||
|
||||
let removeCommentRes: CommentResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/comment`,
|
||||
`${lemmyBetaApiUrl}/comment/remove`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -925,19 +903,16 @@ describe('main', () => {
|
|||
expect(getPostRes.comments[0].removed).toBe(true);
|
||||
|
||||
// lemmy_beta undeletes the comment
|
||||
let unremoveCommentForm: CommentForm = {
|
||||
content: commentContent,
|
||||
let unremoveCommentForm: RemoveCommentForm = {
|
||||
edit_id: createCommentRes.comment.id,
|
||||
post_id: createPostRes.post.id,
|
||||
removed: false,
|
||||
auth: lemmyBetaAuth,
|
||||
creator_id: createCommentRes.comment.creator_id,
|
||||
};
|
||||
|
||||
let unremoveCommentRes: CommentResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/comment`,
|
||||
`${lemmyBetaApiUrl}/comment/remove`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -953,23 +928,22 @@ describe('main', () => {
|
|||
expect(getPostUnremoveRes.comments[0].removed).toBe(false);
|
||||
|
||||
// lemmy_beta deletes the post
|
||||
let removePostForm: PostForm = {
|
||||
name: postName,
|
||||
let removePostForm: RemovePostForm = {
|
||||
edit_id: createPostRes.post.id,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createPostRes.post.community_id,
|
||||
creator_id: createPostRes.post.creator_id,
|
||||
nsfw: false,
|
||||
removed: true,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
|
||||
method: 'PUT',
|
||||
let removePostRes: PostResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/post/remove`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(removePostForm),
|
||||
}).then(d => d.json());
|
||||
}
|
||||
).then(d => d.json());
|
||||
expect(removePostRes.post.removed).toBe(true);
|
||||
|
||||
// Make sure lemmy_alpha sees the post is deleted
|
||||
|
@ -979,20 +953,16 @@ describe('main', () => {
|
|||
expect(getPostResAgain.post.removed).toBe(true);
|
||||
|
||||
// lemmy_beta unremoves the post
|
||||
let unremovePostForm: PostForm = {
|
||||
name: postName,
|
||||
let unremovePostForm: RemovePostForm = {
|
||||
edit_id: createPostRes.post.id,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: createPostRes.post.community_id,
|
||||
creator_id: createPostRes.post.creator_id,
|
||||
nsfw: false,
|
||||
removed: false,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let unremovePostRes: PostResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/post`,
|
||||
`${lemmyBetaApiUrl}/post/remove`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1007,21 +977,17 @@ describe('main', () => {
|
|||
}).then(d => d.json());
|
||||
expect(getPostResAgainTwo.post.removed).toBe(false);
|
||||
|
||||
// lemmy_beta deletes the community
|
||||
let removeCommunityForm: CommunityForm = {
|
||||
name: communityName,
|
||||
title: communityName,
|
||||
category_id: 1,
|
||||
// lemmy_beta removes the community
|
||||
let removeCommunityForm: RemoveCommunityForm = {
|
||||
edit_id: createCommunityRes.community.id,
|
||||
nsfw: false,
|
||||
removed: true,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let removeCommunityRes: CommunityResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/community`,
|
||||
`${lemmyBetaApiUrl}/community/remove`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1029,7 +995,7 @@ describe('main', () => {
|
|||
}
|
||||
).then(d => d.json());
|
||||
|
||||
// Make sure the delete went through
|
||||
// Make sure the remove went through
|
||||
expect(removeCommunityRes.community.removed).toBe(true);
|
||||
|
||||
// Re-get it from alpha, make sure its removed there too
|
||||
|
@ -1041,20 +1007,16 @@ describe('main', () => {
|
|||
expect(getCommunityRes.community.removed).toBe(true);
|
||||
|
||||
// lemmy_beta unremoves the community
|
||||
let unremoveCommunityForm: CommunityForm = {
|
||||
name: communityName,
|
||||
title: communityName,
|
||||
category_id: 1,
|
||||
let unremoveCommunityForm: RemoveCommunityForm = {
|
||||
edit_id: createCommunityRes.community.id,
|
||||
nsfw: false,
|
||||
removed: false,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let unremoveCommunityRes: CommunityResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/community`,
|
||||
`${lemmyBetaApiUrl}/community/remove`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1149,16 +1111,16 @@ describe('main', () => {
|
|||
);
|
||||
|
||||
// lemmy alpha deletes the private message
|
||||
let deletePrivateMessageForm: EditPrivateMessageForm = {
|
||||
let deletePrivateMessageForm: DeletePrivateMessageForm = {
|
||||
deleted: true,
|
||||
edit_id: createRes.message.id,
|
||||
auth: lemmyAlphaAuth,
|
||||
};
|
||||
|
||||
let deleteRes: PrivateMessageResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/private_message`,
|
||||
`${lemmyAlphaApiUrl}/private_message/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1182,16 +1144,16 @@ describe('main', () => {
|
|||
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
|
||||
|
||||
// lemmy alpha undeletes the private message
|
||||
let undeletePrivateMessageForm: EditPrivateMessageForm = {
|
||||
let undeletePrivateMessageForm: DeletePrivateMessageForm = {
|
||||
deleted: false,
|
||||
edit_id: createRes.message.id,
|
||||
auth: lemmyAlphaAuth,
|
||||
};
|
||||
|
||||
let undeleteRes: PrivateMessageResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/private_message`,
|
||||
`${lemmyAlphaApiUrl}/private_message/delete`,
|
||||
{
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
@ -1252,7 +1214,6 @@ describe('main', () => {
|
|||
name: postName,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 2,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
@ -1363,7 +1324,6 @@ describe('main', () => {
|
|||
name: betaPostName,
|
||||
auth: lemmyBetaAuth,
|
||||
community_id: 2,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
|
|
1
ui/src/components/admin-settings.tsx
vendored
1
ui/src/components/admin-settings.tsx
vendored
|
@ -46,6 +46,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
admins: [],
|
||||
banned: [],
|
||||
online: null,
|
||||
version: null,
|
||||
},
|
||||
siteConfigForm: {
|
||||
config_hjson: null,
|
||||
|
|
336
ui/src/components/comment-form.tsx
vendored
336
ui/src/components/comment-form.tsx
vendored
|
@ -1,8 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import {
|
||||
CommentNode as CommentNodeI,
|
||||
CommentForm as CommentFormI,
|
||||
|
@ -10,22 +9,11 @@ import {
|
|||
UserOperation,
|
||||
CommentResponse,
|
||||
} from '../interfaces';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
mdToHtml,
|
||||
randomStr,
|
||||
markdownHelpUrl,
|
||||
toast,
|
||||
setupTribute,
|
||||
wsJsonToRes,
|
||||
pictrsDeleteToast,
|
||||
} from '../utils';
|
||||
import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
|
||||
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 { T } from 'inferno-i18next';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
|
||||
interface CommentFormProps {
|
||||
postId?: number;
|
||||
|
@ -39,15 +27,10 @@ interface CommentFormProps {
|
|||
interface CommentFormState {
|
||||
commentForm: CommentFormI;
|
||||
buttonTitle: string;
|
||||
previewMode: boolean;
|
||||
loading: boolean;
|
||||
imageLoading: boolean;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
private id = `comment-textarea-${randomStr()}`;
|
||||
private formId = `comment-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommentFormState = {
|
||||
commentForm: {
|
||||
|
@ -65,15 +48,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
: this.props.edit
|
||||
? capitalizeFirstLetter(i18n.t('save'))
|
||||
: capitalizeFirstLetter(i18n.t('reply')),
|
||||
previewMode: false,
|
||||
loading: false,
|
||||
imageLoading: false,
|
||||
finished: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
|
||||
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() {
|
||||
this.subscription.unsubscribe();
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="mb-3">
|
||||
<Prompt
|
||||
when={this.state.commentForm.content}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
{UserService.Instance.user ? (
|
||||
<form
|
||||
id={this.formId}
|
||||
onSubmit={linkEvent(this, this.handleCommentSubmit)}
|
||||
>
|
||||
<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.commentForm.content}
|
||||
onInput={linkEvent(this, this.handleCommentContentChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
required
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.commentForm.content}
|
||||
buttonTitle={this.state.buttonTitle}
|
||||
finished={this.state.finished}
|
||||
replyType={!!this.props.node}
|
||||
focus={this.props.focus}
|
||||
disabled={this.props.disabled}
|
||||
rows={2}
|
||||
maxLength={10000}
|
||||
onSubmit={this.handleCommentSubmit}
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
/>
|
||||
{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">
|
||||
<use xlinkHref="#icon-alert-triangle"></use>
|
||||
</svg>
|
||||
<T i18nKey="must_login" class="d-inline">
|
||||
#<Link to="/login">#</Link>
|
||||
#
|
||||
<Link class="alert-link" to="/login">
|
||||
#
|
||||
</Link>
|
||||
</T>
|
||||
</div>
|
||||
)}
|
||||
|
@ -266,141 +115,36 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
);
|
||||
}
|
||||
|
||||
handleFinished(op: UserOperation, data: CommentResponse) {
|
||||
let isReply =
|
||||
this.props.node !== undefined && data.comment.parent_id !== null;
|
||||
let xor =
|
||||
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
|
||||
|
||||
if (
|
||||
(data.comment.creator_id == UserService.Instance.user.id &&
|
||||
((op == UserOperation.CreateComment &&
|
||||
// If its a reply, make sure parent child match
|
||||
isReply &&
|
||||
data.comment.parent_id == this.props.node.comment.id) ||
|
||||
// Otherwise, check the XOR of the two
|
||||
(!isReply && xor))) ||
|
||||
// If its a comment edit, only check that its from your user, and that its a
|
||||
// text edit only
|
||||
|
||||
(data.comment.creator_id == UserService.Instance.user.id &&
|
||||
op == UserOperation.EditComment &&
|
||||
data.comment.content)
|
||||
) {
|
||||
this.state.previewMode = false;
|
||||
this.state.loading = false;
|
||||
this.state.commentForm.content = '';
|
||||
handleCommentSubmit(msg: { val: string; formId: string }) {
|
||||
this.state.commentForm.content = msg.val;
|
||||
this.state.commentForm.form_id = msg.formId;
|
||||
if (this.props.edit) {
|
||||
WebSocketService.Instance.editComment(this.state.commentForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createComment(this.state.commentForm);
|
||||
}
|
||||
this.setState(this.state);
|
||||
let form: any = document.getElementById(this.formId);
|
||||
form.reset();
|
||||
if (this.props.node) {
|
||||
}
|
||||
|
||||
handleReplyCancel() {
|
||||
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 {
|
||||
WebSocketService.Instance.createComment(i.state.commentForm);
|
||||
}
|
||||
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleCommentContentChange(i: CommentForm, event: any) {
|
||||
i.state.commentForm.content = event.target.value;
|
||||
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) {
|
||||
let res = wsJsonToRes(msg);
|
||||
|
||||
// Only do the showing and hiding if logged in
|
||||
if (UserService.Instance.user) {
|
||||
if (res.op == UserOperation.CreateComment) {
|
||||
if (
|
||||
res.op == UserOperation.CreateComment ||
|
||||
res.op == UserOperation.EditComment
|
||||
) {
|
||||
let data = res.data as CommentResponse;
|
||||
this.handleFinished(res.op, data);
|
||||
} else if (res.op == UserOperation.EditComment) {
|
||||
let data = res.data as CommentResponse;
|
||||
this.handleFinished(res.op, data);
|
||||
|
||||
// This only finishes this form, if the randomly generated form_id matches the one received
|
||||
if (this.state.commentForm.form_id == data.form_id) {
|
||||
this.setState({ finished: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
36
ui/src/components/comment-node.tsx
vendored
36
ui/src/components/comment-node.tsx
vendored
|
@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
|
|||
import {
|
||||
CommentNode as CommentNodeI,
|
||||
CommentLikeForm,
|
||||
CommentForm as CommentFormI,
|
||||
EditUserMentionForm,
|
||||
DeleteCommentForm,
|
||||
RemoveCommentForm,
|
||||
MarkCommentAsReadForm,
|
||||
MarkUserMentionAsReadForm,
|
||||
SaveCommentForm,
|
||||
BanFromCommunityForm,
|
||||
BanUserForm,
|
||||
|
@ -848,16 +850,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
|
||||
handleDeleteClick(i: CommentNode) {
|
||||
let deleteForm: CommentFormI = {
|
||||
content: i.props.node.comment.content,
|
||||
let deleteForm: DeleteCommentForm = {
|
||||
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,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(deleteForm);
|
||||
WebSocketService.Instance.deleteComment(deleteForm);
|
||||
}
|
||||
|
||||
handleSaveCommentClick(i: CommentNode) {
|
||||
|
@ -901,7 +899,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.id,
|
||||
post_id: i.comment.post_id,
|
||||
score: this.state.my_vote,
|
||||
};
|
||||
|
||||
|
@ -929,7 +926,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
let form: CommentLikeForm = {
|
||||
comment_id: i.comment.id,
|
||||
post_id: i.comment.post_id,
|
||||
score: this.state.my_vote,
|
||||
};
|
||||
|
||||
|
@ -950,17 +946,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
|
||||
handleModRemoveSubmit(i: CommentNode) {
|
||||
event.preventDefault();
|
||||
let form: CommentFormI = {
|
||||
content: i.props.node.comment.content,
|
||||
let form: RemoveCommentForm = {
|
||||
edit_id: i.props.node.comment.id,
|
||||
creator_id: i.props.node.comment.creator_id,
|
||||
post_id: i.props.node.comment.post_id,
|
||||
parent_id: i.props.node.comment.parent_id,
|
||||
removed: !i.props.node.comment.removed,
|
||||
reason: i.state.removeReason,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
WebSocketService.Instance.removeComment(form);
|
||||
|
||||
i.state.showRemoveDialog = false;
|
||||
i.setState(i.state);
|
||||
|
@ -969,22 +961,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
handleMarkRead(i: CommentNode) {
|
||||
// if it has a user_mention_id field, then its a mention
|
||||
if (i.props.node.comment.user_mention_id) {
|
||||
let form: EditUserMentionForm = {
|
||||
let form: MarkUserMentionAsReadForm = {
|
||||
user_mention_id: i.props.node.comment.user_mention_id,
|
||||
read: !i.props.node.comment.read,
|
||||
};
|
||||
WebSocketService.Instance.editUserMention(form);
|
||||
WebSocketService.Instance.markUserMentionAsRead(form);
|
||||
} else {
|
||||
let form: CommentFormI = {
|
||||
content: i.props.node.comment.content,
|
||||
let form: MarkCommentAsReadForm = {
|
||||
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,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
WebSocketService.Instance.markCommentAsRead(form);
|
||||
}
|
||||
|
||||
i.state.readLoading = true;
|
||||
|
|
41
ui/src/components/communities.tsx
vendored
41
ui/src/components/communities.tsx
vendored
|
@ -13,7 +13,7 @@ import {
|
|||
GetSiteResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { wsJsonToRes, toast } from '../utils';
|
||||
import { wsJsonToRes, toast, getPageFromProps } from '../utils';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -27,12 +27,16 @@ interface CommunitiesState {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
interface CommunitiesProps {
|
||||
page: number;
|
||||
}
|
||||
|
||||
export class Communities extends Component<any, CommunitiesState> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommunitiesState = {
|
||||
communities: [],
|
||||
loading: true,
|
||||
page: this.getPageFromProps(this.props),
|
||||
page: getPageFromProps(this.props),
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -50,19 +54,19 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
if (nextProps.history.action == 'POP') {
|
||||
this.state = this.emptyState;
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
static getDerivedStateFromProps(props: any): CommunitiesProps {
|
||||
return {
|
||||
page: getPageFromProps(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_: any, lastState: CommunitiesState) {
|
||||
if (lastState.page !== this.state.page) {
|
||||
this.setState({ loading: true });
|
||||
this.refetch();
|
||||
}
|
||||
}
|
||||
|
@ -172,22 +176,17 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
this.props.history.push(`/communities/page/${this.state.page}`);
|
||||
updateUrl(paramUpdates: CommunitiesProps) {
|
||||
const page = paramUpdates.page || this.state.page;
|
||||
this.props.history.push(`/communities/page/${page}`);
|
||||
}
|
||||
|
||||
nextPage(i: Communities) {
|
||||
i.state.page++;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
i.updateUrl({ page: i.state.page + 1 });
|
||||
}
|
||||
|
||||
prevPage(i: Communities) {
|
||||
i.state.page--;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
i.updateUrl({ page: i.state.page - 1 });
|
||||
}
|
||||
|
||||
handleUnsubscribe(communityId: number) {
|
||||
|
|
47
ui/src/components/community-form.tsx
vendored
47
ui/src/components/community-form.tsx
vendored
|
@ -11,18 +11,11 @@ import {
|
|||
WebSocketJsonResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
wsJsonToRes,
|
||||
capitalizeFirstLetter,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
} from '../utils';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import autosize from 'autosize';
|
||||
import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
import { Community } from '../interfaces';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
|
||||
interface CommunityFormProps {
|
||||
community?: Community; // If a community is given, that means this is an edit
|
||||
|
@ -43,7 +36,6 @@ export class CommunityForm extends Component<
|
|||
CommunityFormState
|
||||
> {
|
||||
private id = `community-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
|
||||
private emptyState: CommunityFormState = {
|
||||
|
@ -60,9 +52,12 @@ export class CommunityForm extends Component<
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
|
||||
this
|
||||
);
|
||||
|
||||
if (this.props.community) {
|
||||
this.state.communityForm = {
|
||||
name: this.props.community.name,
|
||||
|
@ -86,17 +81,6 @@ export class CommunityForm extends Component<
|
|||
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() {
|
||||
if (
|
||||
!this.state.loading &&
|
||||
|
@ -128,6 +112,7 @@ export class CommunityForm extends Component<
|
|||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
<form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
|
||||
{!this.props.community && (
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label" htmlFor="community-name">
|
||||
{i18n.t('name')}
|
||||
|
@ -147,7 +132,7 @@ export class CommunityForm extends Component<
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label" htmlFor="community-title">
|
||||
{i18n.t('title')}
|
||||
|
@ -170,13 +155,9 @@ export class CommunityForm extends Component<
|
|||
{i18n.t('sidebar')}
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.communityForm.description}
|
||||
onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
|
||||
class="form-control"
|
||||
rows={3}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.communityForm.description}
|
||||
onContentChange={this.handleCommunityDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -270,9 +251,9 @@ export class CommunityForm extends Component<
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleCommunityDescriptionChange(i: CommunityForm, event: any) {
|
||||
i.state.communityForm.description = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleCommunityDescriptionChange(val: string) {
|
||||
this.state.communityForm.description = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCommunityCategoryChange(i: CommunityForm, event: any) {
|
||||
|
|
90
ui/src/components/community.tsx
vendored
90
ui/src/components/community.tsx
vendored
|
@ -65,6 +65,18 @@ interface State {
|
|||
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> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: State = {
|
||||
|
@ -143,16 +155,21 @@ export class Community extends Component<any, State> {
|
|||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
static getDerivedStateFromProps(props: any): CommunityProps {
|
||||
return {
|
||||
dataType: getDataTypeFromProps(props),
|
||||
sort: getSortTypeFromProps(props),
|
||||
page: getPageFromProps(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_: any, lastState: State) {
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
lastState.dataType !== this.state.dataType ||
|
||||
lastState.sort !== this.state.sort ||
|
||||
lastState.page !== this.state.page
|
||||
) {
|
||||
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||
this.state.sort = getSortTypeFromProps(nextProps);
|
||||
this.state.page = getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.setState({ loading: true });
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
|
@ -273,46 +290,33 @@ export class Community extends Component<any, State> {
|
|||
}
|
||||
|
||||
nextPage(i: Community) {
|
||||
i.state.page++;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchData();
|
||||
i.updateUrl({ page: i.state.page + 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
prevPage(i: Community) {
|
||||
i.state.page--;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchData();
|
||||
i.updateUrl({ page: i.state.page - 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchData();
|
||||
this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleDataTypeChange(val: DataType) {
|
||||
this.state.dataType = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchData();
|
||||
this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
|
||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||
updateUrl(paramUpdates: UrlParams) {
|
||||
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(
|
||||
`/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;
|
||||
this.state.community = data.community;
|
||||
this.state.moderators = data.moderators;
|
||||
this.state.admins = data.admins;
|
||||
this.state.online = data.online;
|
||||
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
|
||||
this.setState(this.state);
|
||||
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;
|
||||
this.state.community = data.community;
|
||||
this.setState(this.state);
|
||||
|
@ -372,7 +379,13 @@ export class Community extends Component<any, State> {
|
|||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
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;
|
||||
editPostFindRes(data, this.state.posts);
|
||||
this.setState(this.state);
|
||||
|
@ -401,7 +414,11 @@ export class Community extends Component<any, State> {
|
|||
this.state.comments = data.comments;
|
||||
this.state.loading = false;
|
||||
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;
|
||||
editCommentRes(data, this.state.comments);
|
||||
this.setState(this.state);
|
||||
|
@ -424,6 +441,7 @@ export class Community extends Component<any, State> {
|
|||
} else if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
this.state.site = data.site;
|
||||
this.state.admins = data.admins;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
|
2
ui/src/components/create-community.tsx
vendored
2
ui/src/components/create-community.tsx
vendored
|
@ -70,7 +70,7 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
|
|||
console.log(msg);
|
||||
let res = wsJsonToRes(msg);
|
||||
if (msg.error) {
|
||||
toast(i18n.t(msg.error), 'danger');
|
||||
// Toast errors are already handled by community-form
|
||||
return;
|
||||
} else if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
|
|
15
ui/src/components/data-type-select.tsx
vendored
15
ui/src/components/data-type-select.tsx
vendored
|
@ -25,6 +25,12 @@ export class DataTypeSelect extends Component<
|
|||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: any): DataTypeSelectProps {
|
||||
return {
|
||||
type_: props.type_,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
|
@ -42,8 +48,9 @@ export class DataTypeSelect extends Component<
|
|||
{i18n.t('posts')}
|
||||
</label>
|
||||
<label
|
||||
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
|
||||
DataType.Comment && 'active'}`}
|
||||
className={`pointer btn btn-sm btn-secondary ${
|
||||
this.state.type_ == DataType.Comment && 'active'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -58,8 +65,6 @@ export class DataTypeSelect extends Component<
|
|||
}
|
||||
|
||||
handleTypeChange(i: DataTypeSelect, event: any) {
|
||||
i.state.type_ = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
i.props.onChange(i.state.type_);
|
||||
i.props.onChange(Number(event.target.value));
|
||||
}
|
||||
}
|
||||
|
|
45
ui/src/components/footer.tsx
vendored
45
ui/src/components/footer.tsx
vendored
|
@ -1,12 +1,41 @@
|
|||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { repoUrl } from '../utils';
|
||||
import { version } from '../version';
|
||||
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) {
|
||||
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() {
|
||||
|
@ -15,7 +44,7 @@ export class Footer extends Component<any, any> {
|
|||
<div className="navbar-collapse">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<span class="navbar-text">{version}</span>
|
||||
<span class="navbar-text">{this.state.version}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Link class="nav-link" to="/modlog">
|
||||
|
@ -42,4 +71,12 @@ export class Footer extends Component<any, any> {
|
|||
</nav>
|
||||
);
|
||||
}
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
let res = wsJsonToRes(msg);
|
||||
|
||||
if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
this.setState({ version: data.version });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
ui/src/components/inbox.tsx
vendored
35
ui/src/components/inbox.tsx
vendored
|
@ -446,9 +446,30 @@ export class Inbox extends Component<any, InboxState> {
|
|||
let found: PrivateMessageI = this.state.messages.find(
|
||||
m => m.id === data.message.id
|
||||
);
|
||||
if (found) {
|
||||
found.content = data.message.content;
|
||||
found.updated = data.message.updated;
|
||||
}
|
||||
this.setState(this.state);
|
||||
} else if (res.op == UserOperation.DeletePrivateMessage) {
|
||||
let data = res.data as PrivateMessageResponse;
|
||||
let found: PrivateMessageI = this.state.messages.find(
|
||||
m => m.id === data.message.id
|
||||
);
|
||||
if (found) {
|
||||
found.deleted = data.message.deleted;
|
||||
found.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(
|
||||
|
@ -458,15 +479,21 @@ export class Inbox extends Component<any, InboxState> {
|
|||
let found = this.state.messages.find(c => c.id == data.message.id);
|
||||
found.read = data.message.read;
|
||||
}
|
||||
}
|
||||
this.sendUnreadCount();
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.MarkAllAsRead) {
|
||||
// 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;
|
||||
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 (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
|
||||
|
@ -480,7 +507,7 @@ export class Inbox extends Component<any, InboxState> {
|
|||
this.sendUnreadCount();
|
||||
this.setState(this.state);
|
||||
setupTippy();
|
||||
} else if (res.op == UserOperation.EditUserMention) {
|
||||
} else if (res.op == UserOperation.MarkUserMentionAsRead) {
|
||||
let data = res.data as UserMentionResponse;
|
||||
|
||||
let found = this.state.mentions.find(c => c.id == data.mention.id);
|
||||
|
|
15
ui/src/components/listing-type-select.tsx
vendored
15
ui/src/components/listing-type-select.tsx
vendored
|
@ -26,6 +26,12 @@ export class ListingTypeSelect extends Component<
|
|||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
|
||||
return {
|
||||
type_: props.type_,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="btn-group btn-group-toggle">
|
||||
|
@ -45,8 +51,9 @@ export class ListingTypeSelect extends Component<
|
|||
{i18n.t('subscribed')}
|
||||
</label>
|
||||
<label
|
||||
className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
|
||||
ListingType.All && 'active'}`}
|
||||
className={`pointer btn btn-sm btn-secondary ${
|
||||
this.state.type_ == ListingType.All && 'active'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -61,8 +68,6 @@ export class ListingTypeSelect extends Component<
|
|||
}
|
||||
|
||||
handleTypeChange(i: ListingTypeSelect, event: any) {
|
||||
i.state.type_ = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
i.props.onChange(i.state.type_);
|
||||
i.props.onChange(Number(event.target.value));
|
||||
}
|
||||
}
|
||||
|
|
11
ui/src/components/login.tsx
vendored
11
ui/src/components/login.tsx
vendored
|
@ -120,14 +120,15 @@ export class Login extends Component<any, State> {
|
|||
class="form-control"
|
||||
required
|
||||
/>
|
||||
{validEmail(this.state.loginForm.username_or_email) && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={!validEmail(this.state.loginForm.username_or_email)}
|
||||
onClick={linkEvent(this, this.handlePasswordReset)}
|
||||
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
|
||||
>
|
||||
{i18n.t('forgot_password')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
@ -186,6 +187,14 @@ export class Login extends Component<any, State> {
|
|||
onInput={linkEvent(this, this.handleRegisterEmailChange)}
|
||||
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>
|
||||
|
||||
|
|
93
ui/src/components/main.tsx
vendored
93
ui/src/components/main.tsx
vendored
|
@ -70,6 +70,20 @@ interface MainState {
|
|||
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> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: MainState = {
|
||||
|
@ -93,6 +107,7 @@ export class Main extends Component<any, MainState> {
|
|||
admins: [],
|
||||
banned: [],
|
||||
online: null,
|
||||
version: null,
|
||||
},
|
||||
showEditSite: false,
|
||||
loading: true,
|
||||
|
@ -141,17 +156,23 @@ export class Main extends Component<any, MainState> {
|
|||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
static getDerivedStateFromProps(props: any): MainProps {
|
||||
return {
|
||||
listingType: getListingTypeFromProps(props),
|
||||
dataType: getDataTypeFromProps(props),
|
||||
sort: getSortTypeFromProps(props),
|
||||
page: getPageFromProps(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_: any, lastState: MainState) {
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
lastState.listingType !== this.state.listingType ||
|
||||
lastState.dataType !== this.state.dataType ||
|
||||
lastState.sort !== this.state.sort ||
|
||||
lastState.page !== this.state.page
|
||||
) {
|
||||
this.state.listingType = getListingTypeFromProps(nextProps);
|
||||
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||
this.state.sort = getSortTypeFromProps(nextProps);
|
||||
this.state.page = getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.setState({ loading: true });
|
||||
this.fetchData();
|
||||
}
|
||||
}
|
||||
|
@ -257,12 +278,17 @@ export class Main extends Component<any, MainState> {
|
|||
);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
|
||||
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
|
||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||
updateUrl(paramUpdates: UrlParams) {
|
||||
const listingTypeStr =
|
||||
paramUpdates.listingType ||
|
||||
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(
|
||||
`/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) {
|
||||
i.state.page++;
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchData();
|
||||
i.updateUrl({ page: i.state.page + 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
prevPage(i: Main) {
|
||||
i.state.page--;
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.fetchData();
|
||||
i.updateUrl({ page: i.state.page - 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchData();
|
||||
this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleListingTypeChange(val: ListingType) {
|
||||
this.state.listingType = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchData();
|
||||
this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
handleDataTypeChange(val: DataType) {
|
||||
this.state.dataType = val;
|
||||
this.state.page = 1;
|
||||
this.state.loading = true;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.fetchData();
|
||||
this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
|
@ -699,7 +702,11 @@ export class Main extends Component<any, MainState> {
|
|||
this.state.comments = data.comments;
|
||||
this.state.loading = false;
|
||||
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;
|
||||
editCommentRes(data, this.state.comments);
|
||||
this.setState(this.state);
|
||||
|
|
538
ui/src/components/markdown-textarea.tsx
vendored
Normal file
538
ui/src/components/markdown-textarea.tsx
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
6
ui/src/components/navbar.tsx
vendored
6
ui/src/components/navbar.tsx
vendored
|
@ -30,7 +30,6 @@ import {
|
|||
messageToastify,
|
||||
md,
|
||||
} from '../utils';
|
||||
import { version } from '../version';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface NavbarState {
|
||||
|
@ -41,6 +40,7 @@ interface NavbarState {
|
|||
messages: Array<PrivateMessage>;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
version: string;
|
||||
admins: Array<UserView>;
|
||||
searchParam: string;
|
||||
toggleSearch: boolean;
|
||||
|
@ -58,6 +58,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
messages: [],
|
||||
expanded: false,
|
||||
siteName: undefined,
|
||||
version: undefined,
|
||||
admins: [],
|
||||
searchParam: '',
|
||||
toggleSearch: false,
|
||||
|
@ -150,7 +151,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
navbar() {
|
||||
return (
|
||||
<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}
|
||||
</Link>
|
||||
{this.state.isLoggedIn && (
|
||||
|
@ -395,6 +396,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
if (data.site && !this.state.siteName) {
|
||||
this.state.siteName = data.site.name;
|
||||
this.state.version = data.version;
|
||||
this.state.admins = data.admins;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
|
68
ui/src/components/post-form.tsx
vendored
68
ui/src/components/post-form.tsx
vendored
|
@ -1,6 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { PostListings } from './post-listings';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
|
@ -24,22 +25,16 @@ import {
|
|||
getPageTitle,
|
||||
validURL,
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
archiveUrl,
|
||||
mdToHtml,
|
||||
debounce,
|
||||
isImage,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
hostname,
|
||||
pictrsDeleteToast,
|
||||
validTitle,
|
||||
} 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 { i18n } from '../i18next';
|
||||
|
||||
|
@ -68,7 +63,6 @@ interface PostFormState {
|
|||
|
||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
private id = `post-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private choices: Choices;
|
||||
private emptyState: PostFormState = {
|
||||
|
@ -77,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
nsfw: false,
|
||||
auth: null,
|
||||
community_id: null,
|
||||
creator_id: UserService.Instance.user
|
||||
? UserService.Instance.user.id
|
||||
: null,
|
||||
},
|
||||
communities: [],
|
||||
loading: false,
|
||||
|
@ -94,8 +85,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
super(props, context);
|
||||
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
|
||||
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
|
@ -106,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
name: this.props.post.name,
|
||||
community_id: this.props.post.community_id,
|
||||
edit_id: this.props.post.id,
|
||||
creator_id: this.props.post.creator_id,
|
||||
url: this.props.post.url,
|
||||
nsfw: this.props.post.nsfw,
|
||||
auth: null,
|
||||
|
@ -140,14 +129,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -166,7 +147,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
this.choices && this.choices.destroy();
|
||||
/* this.choices && this.choices.destroy(); */
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
|
@ -305,41 +286,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
{i18n.t('body')}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.postForm.body}
|
||||
onInput={linkEvent(this, this.handlePostBodyChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.postForm.body}
|
||||
onContentChange={this.handlePostBodyChange}
|
||||
/>
|
||||
{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>
|
||||
{!this.props.post && (
|
||||
|
@ -499,9 +449,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePostBodyChange(i: PostForm, event: any) {
|
||||
i.state.postForm.body = event.target.value;
|
||||
i.setState(i.state);
|
||||
handlePostBodyChange(val: string) {
|
||||
this.state.postForm.body = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePostCommunityChange(i: PostForm, event: any) {
|
||||
|
|
43
ui/src/components/post-listing.tsx
vendored
43
ui/src/components/post-listing.tsx
vendored
|
@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services';
|
|||
import {
|
||||
Post,
|
||||
CreatePostLikeForm,
|
||||
PostForm as PostFormI,
|
||||
DeletePostForm,
|
||||
RemovePostForm,
|
||||
LockPostForm,
|
||||
StickyPostForm,
|
||||
SavePostForm,
|
||||
CommunityUser,
|
||||
UserView,
|
||||
|
@ -33,7 +36,6 @@ import {
|
|||
setupTippy,
|
||||
hostname,
|
||||
previewLines,
|
||||
toast,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -101,6 +103,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
this.state.upvotes = nextProps.post.upvotes;
|
||||
this.state.downvotes = nextProps.post.downvotes;
|
||||
this.state.score = nextProps.post.score;
|
||||
if (this.props.post.id !== nextProps.post.id) {
|
||||
this.state.imageExpanded = false;
|
||||
}
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
|
@ -1111,18 +1116,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
|
||||
handleDeleteClick(i: PostListing) {
|
||||
let deleteForm: PostFormI = {
|
||||
body: i.props.post.body,
|
||||
community_id: i.props.post.community_id,
|
||||
name: i.props.post.name,
|
||||
url: i.props.post.url,
|
||||
let deleteForm: DeletePostForm = {
|
||||
edit_id: i.props.post.id,
|
||||
creator_id: i.props.post.creator_id,
|
||||
deleted: !i.props.post.deleted,
|
||||
nsfw: i.props.post.nsfw,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editPost(deleteForm);
|
||||
WebSocketService.Instance.deletePost(deleteForm);
|
||||
}
|
||||
|
||||
handleSavePostClick(i: PostListing) {
|
||||
|
@ -1160,46 +1159,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
|
||||
handleModRemoveSubmit(i: PostListing) {
|
||||
event.preventDefault();
|
||||
let form: PostFormI = {
|
||||
name: i.props.post.name,
|
||||
community_id: i.props.post.community_id,
|
||||
let form: RemovePostForm = {
|
||||
edit_id: i.props.post.id,
|
||||
creator_id: i.props.post.creator_id,
|
||||
removed: !i.props.post.removed,
|
||||
reason: i.state.removeReason,
|
||||
nsfw: i.props.post.nsfw,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editPost(form);
|
||||
WebSocketService.Instance.removePost(form);
|
||||
|
||||
i.state.showRemoveDialog = false;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleModLock(i: PostListing) {
|
||||
let form: PostFormI = {
|
||||
name: i.props.post.name,
|
||||
community_id: i.props.post.community_id,
|
||||
let form: LockPostForm = {
|
||||
edit_id: i.props.post.id,
|
||||
creator_id: i.props.post.creator_id,
|
||||
nsfw: i.props.post.nsfw,
|
||||
locked: !i.props.post.locked,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editPost(form);
|
||||
WebSocketService.Instance.lockPost(form);
|
||||
}
|
||||
|
||||
handleModSticky(i: PostListing) {
|
||||
let form: PostFormI = {
|
||||
name: i.props.post.name,
|
||||
community_id: i.props.post.community_id,
|
||||
let form: StickyPostForm = {
|
||||
edit_id: i.props.post.id,
|
||||
creator_id: i.props.post.creator_id,
|
||||
nsfw: i.props.post.nsfw,
|
||||
stickied: !i.props.post.stickied,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editPost(form);
|
||||
WebSocketService.Instance.stickyPost(form);
|
||||
}
|
||||
|
||||
handleModBanFromCommunityShow(i: PostListing) {
|
||||
|
|
33
ui/src/components/post.tsx
vendored
33
ui/src/components/post.tsx
vendored
|
@ -8,7 +8,7 @@ import {
|
|||
GetPostResponse,
|
||||
PostResponse,
|
||||
Comment,
|
||||
CommentForm as CommentFormI,
|
||||
MarkCommentAsReadForm,
|
||||
CommentResponse,
|
||||
CommentSortType,
|
||||
CommentViewType,
|
||||
|
@ -92,6 +92,7 @@ export class Post extends Component<any, PostState> {
|
|||
enable_nsfw: undefined,
|
||||
},
|
||||
online: null,
|
||||
version: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -167,16 +168,12 @@ export class Post extends Component<any, PostState> {
|
|||
UserService.Instance.user &&
|
||||
UserService.Instance.user.id == parent_user_id
|
||||
) {
|
||||
let form: CommentFormI = {
|
||||
content: found.content,
|
||||
let form: MarkCommentAsReadForm = {
|
||||
edit_id: found.id,
|
||||
creator_id: found.creator_id,
|
||||
post_id: found.post_id,
|
||||
parent_id: found.parent_id,
|
||||
read: true,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editComment(form);
|
||||
WebSocketService.Instance.markCommentAsRead(form);
|
||||
UserService.Instance.user.unreadCount--;
|
||||
UserService.Instance.sub.next({
|
||||
user: UserService.Instance.user,
|
||||
|
@ -408,7 +405,6 @@ export class Post extends Component<any, PostState> {
|
|||
this.state.comments = data.comments;
|
||||
this.state.community = data.community;
|
||||
this.state.moderators = data.moderators;
|
||||
this.state.siteRes.admins = data.admins;
|
||||
this.state.online = data.online;
|
||||
this.state.loading = false;
|
||||
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.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;
|
||||
editCommentRes(data, this.state.comments);
|
||||
this.setState(this.state);
|
||||
|
@ -452,7 +452,13 @@ export class Post extends Component<any, PostState> {
|
|||
let data = res.data as PostResponse;
|
||||
createPostLikeRes(data, this.state.post);
|
||||
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;
|
||||
this.state.post = data.post;
|
||||
this.setState(this.state);
|
||||
|
@ -462,7 +468,11 @@ export class Post extends Component<any, PostState> {
|
|||
this.state.post = data.post;
|
||||
this.setState(this.state);
|
||||
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;
|
||||
this.state.community = data.community;
|
||||
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;
|
||||
this.state.community = data.community;
|
||||
this.state.moderators = data.moderators;
|
||||
this.state.siteRes.admins = data.admins;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
|
100
ui/src/components/private-message-form.tsx
vendored
100
ui/src/components/private-message-form.tsx
vendored
|
@ -18,17 +18,12 @@ import {
|
|||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
mdToHtml,
|
||||
wsJsonToRes,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { UserListing } from './user-listing';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import autosize from 'autosize';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -52,8 +47,6 @@ export class PrivateMessageForm extends Component<
|
|||
PrivateMessageFormProps,
|
||||
PrivateMessageFormState
|
||||
> {
|
||||
private id = `message-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private emptyState: PrivateMessageFormState = {
|
||||
privateMessageForm: {
|
||||
|
@ -69,9 +62,10 @@ export class PrivateMessageForm extends Component<
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.handleContentChange = this.handleContentChange.bind(this);
|
||||
|
||||
if (this.props.privateMessage) {
|
||||
this.state.privateMessageForm = {
|
||||
content: this.props.privateMessage.content,
|
||||
|
@ -99,14 +93,6 @@ export class PrivateMessageForm extends Component<
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -153,24 +139,23 @@ export class PrivateMessageForm extends Component<
|
|||
</div>
|
||||
)}
|
||||
<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">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.privateMessageForm.content}
|
||||
onInput={linkEvent(this, this.handleContentChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.privateMessageForm.content}
|
||||
onContentChange={this.handleContentChange}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.privateMessageForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -184,7 +169,7 @@ export class PrivateMessageForm extends Component<
|
|||
class="alert-link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://about.riot.im/"
|
||||
href="https://element.io/get-started"
|
||||
>
|
||||
#
|
||||
</a>
|
||||
|
@ -210,16 +195,6 @@ export class PrivateMessageForm extends Component<
|
|||
capitalizeFirstLetter(i18n.t('send_message'))
|
||||
)}
|
||||
</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 && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -230,30 +205,7 @@ export class PrivateMessageForm extends Component<
|
|||
</button>
|
||||
)}
|
||||
<ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
|
||||
<li class="list-inline-item">
|
||||
<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>
|
||||
<li class="list-inline-item"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -284,9 +236,9 @@ export class PrivateMessageForm extends Component<
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleContentChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.privateMessageForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleContentChange(val: string) {
|
||||
this.state.privateMessageForm.content = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCancel(i: PrivateMessageForm) {
|
||||
|
@ -311,7 +263,11 @@ export class PrivateMessageForm extends Component<
|
|||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
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;
|
||||
this.state.loading = false;
|
||||
this.props.onEdit(data.message);
|
||||
|
|
11
ui/src/components/private-message.tsx
vendored
11
ui/src/components/private-message.tsx
vendored
|
@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
|
|||
import { Link } from 'inferno-router';
|
||||
import {
|
||||
PrivateMessage as PrivateMessageI,
|
||||
EditPrivateMessageForm,
|
||||
DeletePrivateMessageForm,
|
||||
MarkPrivateMessageAsReadForm,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
||||
|
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
|
||||
handleDeleteClick(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
let form: DeletePrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
deleted: !i.props.privateMessage.deleted,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
WebSocketService.Instance.deletePrivateMessage(form);
|
||||
}
|
||||
|
||||
handleReplyCancel() {
|
||||
|
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
|
||||
handleMarkRead(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
let form: MarkPrivateMessageAsReadForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
read: !i.props.privateMessage.read,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
WebSocketService.Instance.markPrivateMessageAsRead(form);
|
||||
}
|
||||
|
||||
handleMessageCollapse(i: PrivateMessage) {
|
||||
|
|
109
ui/src/components/search.tsx
vendored
109
ui/src/components/search.tsx
vendored
|
@ -28,6 +28,7 @@ import {
|
|||
createCommentLikeRes,
|
||||
createPostLikeFindRes,
|
||||
commentsToFlatNodes,
|
||||
getPageFromProps,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { UserListing } from './user-listing';
|
||||
|
@ -44,15 +45,31 @@ interface SearchState {
|
|||
searchResponse: SearchResponse;
|
||||
loading: boolean;
|
||||
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> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: SearchState = {
|
||||
q: this.getSearchQueryFromProps(this.props),
|
||||
type_: this.getSearchTypeFromProps(this.props),
|
||||
sort: this.getSortTypeFromProps(this.props),
|
||||
page: this.getPageFromProps(this.props),
|
||||
q: Search.getSearchQueryFromProps(this.props),
|
||||
type_: Search.getSearchTypeFromProps(this.props),
|
||||
sort: Search.getSortTypeFromProps(this.props),
|
||||
page: getPageFromProps(this.props),
|
||||
searchText: Search.getSearchQueryFromProps(this.props),
|
||||
searchResponse: {
|
||||
type_: null,
|
||||
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 : '';
|
||||
}
|
||||
|
||||
getSearchTypeFromProps(props: any): SearchType {
|
||||
static getSearchTypeFromProps(props: any): SearchType {
|
||||
return props.match.params.type
|
||||
? routeSearchTypeToEnum(props.match.params.type)
|
||||
: SearchType.All;
|
||||
}
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
static getSortTypeFromProps(props: any): SortType {
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: SortType.TopAll;
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||
}
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
|
@ -122,17 +135,23 @@ export class Search extends Component<any, SearchState> {
|
|||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
static getDerivedStateFromProps(props: any): SearchProps {
|
||||
return {
|
||||
q: Search.getSearchQueryFromProps(props),
|
||||
type_: Search.getSearchTypeFromProps(props),
|
||||
sort: Search.getSortTypeFromProps(props),
|
||||
page: getPageFromProps(props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_: any, lastState: SearchState) {
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
lastState.q !== this.state.q ||
|
||||
lastState.type_ !== this.state.type_ ||
|
||||
lastState.sort !== this.state.sort ||
|
||||
lastState.page !== this.state.page
|
||||
) {
|
||||
this.state.q = this.getSearchQueryFromProps(nextProps);
|
||||
this.state.type_ = this.getSearchTypeFromProps(nextProps);
|
||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.setState({ loading: true, searchText: this.state.q });
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +182,7 @@ export class Search extends Component<any, SearchState> {
|
|||
<input
|
||||
type="text"
|
||||
class="form-control mr-2"
|
||||
value={this.state.q}
|
||||
value={this.state.searchText}
|
||||
placeholder={`${i18n.t('search')}...`}
|
||||
onInput={linkEvent(this, this.handleQChange)}
|
||||
required
|
||||
|
@ -413,17 +432,11 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
|
||||
nextPage(i: Search) {
|
||||
i.state.page++;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.search();
|
||||
i.updateUrl({ page: i.state.page + 1 });
|
||||
}
|
||||
|
||||
prevPage(i: Search) {
|
||||
i.state.page--;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.search();
|
||||
i.updateUrl({ page: i.state.page - 1 });
|
||||
}
|
||||
|
||||
search() {
|
||||
|
@ -441,37 +454,39 @@ export class Search extends Component<any, SearchState> {
|
|||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
|
||||
}
|
||||
|
||||
handleTypeChange(i: Search, event: any) {
|
||||
i.state.type_ = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.updateUrl({
|
||||
type_: SearchType[Number(event.target.value)].toLowerCase(),
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
handleSearchSubmit(i: Search, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.loading = true;
|
||||
i.search();
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.updateUrl({
|
||||
q: i.state.searchText,
|
||||
type_: SearchType[i.state.type_].toLowerCase(),
|
||||
sort: SortType[i.state.sort].toLowerCase(),
|
||||
page: i.state.page,
|
||||
});
|
||||
}
|
||||
|
||||
handleQChange(i: Search, event: any) {
|
||||
i.state.q = event.target.value;
|
||||
i.setState(i.state);
|
||||
i.setState({ searchText: event.target.value });
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
let typeStr = SearchType[this.state.type_].toLowerCase();
|
||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||
updateUrl(paramUpdates: UrlParams) {
|
||||
const qStr = paramUpdates.q || this.state.q;
|
||||
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(
|
||||
`/search/q/${this.state.q}/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
|
||||
`/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
21
ui/src/components/sidebar.tsx
vendored
21
ui/src/components/sidebar.tsx
vendored
|
@ -4,7 +4,8 @@ import {
|
|||
Community,
|
||||
CommunityUser,
|
||||
FollowCommunityForm,
|
||||
CommunityForm as CommunityFormI,
|
||||
DeleteCommunityForm,
|
||||
RemoveCommunityForm,
|
||||
UserView,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
|
@ -284,16 +285,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
|
||||
handleDeleteClick(i: Sidebar) {
|
||||
event.preventDefault();
|
||||
let deleteForm: CommunityFormI = {
|
||||
name: i.props.community.name,
|
||||
title: i.props.community.title,
|
||||
category_id: i.props.community.category_id,
|
||||
let deleteForm: DeleteCommunityForm = {
|
||||
edit_id: i.props.community.id,
|
||||
deleted: !i.props.community.deleted,
|
||||
nsfw: i.props.community.nsfw,
|
||||
auth: null,
|
||||
};
|
||||
WebSocketService.Instance.editCommunity(deleteForm);
|
||||
WebSocketService.Instance.deleteCommunity(deleteForm);
|
||||
}
|
||||
|
||||
handleUnsubscribe(communityId: number) {
|
||||
|
@ -350,18 +346,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
|
||||
handleModRemoveSubmit(i: Sidebar) {
|
||||
event.preventDefault();
|
||||
let deleteForm: CommunityFormI = {
|
||||
name: i.props.community.name,
|
||||
title: i.props.community.title,
|
||||
category_id: i.props.community.category_id,
|
||||
let removeForm: RemoveCommunityForm = {
|
||||
edit_id: i.props.community.id,
|
||||
removed: !i.props.community.removed,
|
||||
reason: i.state.removeReason,
|
||||
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.setState(i.state);
|
||||
|
|
37
ui/src/components/site-form.tsx
vendored
37
ui/src/components/site-form.tsx
vendored
|
@ -1,10 +1,9 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import { capitalizeFirstLetter, randomStr } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface SiteFormProps {
|
||||
|
@ -19,7 +18,6 @@ interface SiteFormState {
|
|||
|
||||
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||
private id = `site-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private emptyState: SiteFormState = {
|
||||
siteForm: {
|
||||
enable_downvotes: true,
|
||||
|
@ -33,8 +31,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
|
||||
this
|
||||
);
|
||||
|
||||
if (this.props.site) {
|
||||
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
|
||||
componentWillReceiveProps() {
|
||||
this.state.loading = false;
|
||||
|
@ -119,13 +108,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
{i18n.t('sidebar')}
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.siteForm.description}
|
||||
onInput={linkEvent(this, this.handleSiteDescriptionChange)}
|
||||
class="form-control"
|
||||
rows={3}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.siteForm.description}
|
||||
onContentChange={this.handleSiteDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -238,9 +223,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleSiteDescriptionChange(i: SiteForm, event: any) {
|
||||
i.state.siteForm.description = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleSiteDescriptionChange(val: string) {
|
||||
this.state.siteForm.description = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleSiteEnableNsfwChange(i: SiteForm, event: any) {
|
||||
|
|
10
ui/src/components/sort-select.tsx
vendored
10
ui/src/components/sort-select.tsx
vendored
|
@ -23,6 +23,12 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
|||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: any): SortSelectState {
|
||||
return {
|
||||
sort: props.sort,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
|
@ -59,8 +65,6 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
|||
}
|
||||
|
||||
handleSortChange(i: SortSelect, event: any) {
|
||||
i.state.sort = Number(event.target.value);
|
||||
i.setState(i.state);
|
||||
i.props.onChange(i.state.sort);
|
||||
i.props.onChange(event.target.value);
|
||||
}
|
||||
}
|
||||
|
|
2
ui/src/components/sponsors.tsx
vendored
2
ui/src/components/sponsors.tsx
vendored
|
@ -17,6 +17,8 @@ interface SilverUser {
|
|||
}
|
||||
|
||||
let general = [
|
||||
'Rachel Schmitz',
|
||||
'comradeda',
|
||||
'ybaumy',
|
||||
'dude in phx',
|
||||
'twilight loki',
|
||||
|
|
27
ui/src/components/symbols.tsx
vendored
27
ui/src/components/symbols.tsx
vendored
File diff suppressed because one or more lines are too long
304
ui/src/components/user-details.tsx
vendored
Normal file
304
ui/src/components/user-details.tsx
vendored
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
413
ui/src/components/user.tsx
vendored
413
ui/src/components/user.tsx
vendored
|
@ -4,24 +4,18 @@ import { Subscription } from 'rxjs';
|
|||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
UserOperation,
|
||||
Post,
|
||||
Comment,
|
||||
CommunityUser,
|
||||
GetUserDetailsForm,
|
||||
SortType,
|
||||
ListingType,
|
||||
UserDetailsResponse,
|
||||
UserView,
|
||||
CommentResponse,
|
||||
UserSettingsForm,
|
||||
LoginResponse,
|
||||
BanUserResponse,
|
||||
AddAdminResponse,
|
||||
DeleteAccountForm,
|
||||
PostResponse,
|
||||
WebSocketJsonResponse,
|
||||
GetSiteResponse,
|
||||
Site,
|
||||
UserDetailsView,
|
||||
UserDetailsResponse,
|
||||
AddAdminResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
|
@ -34,28 +28,15 @@ import {
|
|||
languages,
|
||||
showAvatars,
|
||||
toast,
|
||||
editCommentRes,
|
||||
saveCommentRes,
|
||||
createCommentLikeRes,
|
||||
createPostLikeFindRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { UserListing } from './user-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { ListingTypeSelect } from './listing-type-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { i18n } from '../i18next';
|
||||
import moment from 'moment';
|
||||
|
||||
enum View {
|
||||
Overview,
|
||||
Comments,
|
||||
Posts,
|
||||
Saved,
|
||||
}
|
||||
import { UserDetails } from './user-details';
|
||||
|
||||
interface UserState {
|
||||
user: UserView;
|
||||
|
@ -63,11 +44,7 @@ interface UserState {
|
|||
username: string;
|
||||
follows: Array<CommunityUser>;
|
||||
moderates: Array<CommunityUser>;
|
||||
comments: Array<Comment>;
|
||||
posts: Array<Post>;
|
||||
saved?: Array<Post>;
|
||||
admins: Array<UserView>;
|
||||
view: View;
|
||||
view: UserDetailsView;
|
||||
sort: SortType;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
|
@ -77,7 +54,21 @@ interface UserState {
|
|||
deleteAccountLoading: boolean;
|
||||
deleteAccountShowConfirm: boolean;
|
||||
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> {
|
||||
|
@ -102,14 +93,11 @@ export class User extends Component<any, UserState> {
|
|||
username: null,
|
||||
follows: [],
|
||||
moderates: [],
|
||||
comments: [],
|
||||
posts: [],
|
||||
admins: [],
|
||||
loading: true,
|
||||
avatarLoading: false,
|
||||
view: this.getViewFromProps(this.props),
|
||||
sort: this.getSortTypeFromProps(this.props),
|
||||
page: this.getPageFromProps(this.props),
|
||||
view: User.getViewFromProps(this.props.match.view),
|
||||
sort: User.getSortTypeFromProps(this.props.match.sort),
|
||||
page: User.getPageFromProps(this.props.match.page),
|
||||
userSettingsForm: {
|
||||
show_nsfw: null,
|
||||
theme: null,
|
||||
|
@ -126,6 +114,10 @@ export class User extends Component<any, UserState> {
|
|||
deleteAccountForm: {
|
||||
password: null,
|
||||
},
|
||||
siteRes: {
|
||||
admins: [],
|
||||
banned: [],
|
||||
online: undefined,
|
||||
site: {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
|
@ -140,6 +132,7 @@ export class User extends Component<any, UserState> {
|
|||
open_registration: undefined,
|
||||
enable_nsfw: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -153,8 +146,9 @@ export class User extends Component<any, UserState> {
|
|||
this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind(
|
||||
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.subscription = WebSocketService.Instance.subject
|
||||
|
@ -165,7 +159,6 @@ export class User extends Component<any, UserState> {
|
|||
() => console.log('complete')
|
||||
);
|
||||
|
||||
this.refetch();
|
||||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
|
@ -176,38 +169,32 @@ export class User extends Component<any, UserState> {
|
|||
);
|
||||
}
|
||||
|
||||
getViewFromProps(props: any): View {
|
||||
return props.match.params.view
|
||||
? View[capitalizeFirstLetter(props.match.params.view)]
|
||||
: View.Overview;
|
||||
static getViewFromProps(view: any): UserDetailsView {
|
||||
return view
|
||||
? UserDetailsView[capitalizeFirstLetter(view)]
|
||||
: UserDetailsView.Overview;
|
||||
}
|
||||
|
||||
getSortTypeFromProps(props: any): SortType {
|
||||
return props.match.params.sort
|
||||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: SortType.New;
|
||||
static getSortTypeFromProps(sort: any): SortType {
|
||||
return sort ? routeSortTypeToEnum(sort) : SortType.New;
|
||||
}
|
||||
|
||||
getPageFromProps(props: any): number {
|
||||
return props.match.params.page ? Number(props.match.params.page) : 1;
|
||||
static getPageFromProps(page: any): number {
|
||||
return page ? Number(page) : 1;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
// Necessary for back button for some reason
|
||||
componentWillReceiveProps(nextProps: any) {
|
||||
if (
|
||||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
) {
|
||||
this.state.view = this.getViewFromProps(nextProps);
|
||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||
this.state.page = this.getPageFromProps(nextProps);
|
||||
this.setState(this.state);
|
||||
this.refetch();
|
||||
}
|
||||
static getDerivedStateFromProps(props: any): UserProps {
|
||||
return {
|
||||
view: this.getViewFromProps(props.match.params.view),
|
||||
sort: this.getSortTypeFromProps(props.match.params.sort),
|
||||
page: this.getPageFromProps(props.match.params.page),
|
||||
user_id: Number(props.match.params.id) || null,
|
||||
username: props.match.params.username,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) {
|
||||
|
@ -219,18 +206,13 @@ export class User extends Component<any, UserState> {
|
|||
// Couldnt get a refresh working. This does for now.
|
||||
location.reload();
|
||||
}
|
||||
document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<h5>
|
||||
|
@ -242,24 +224,41 @@ export class User extends Component<any, UserState> {
|
|||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>/u/{this.state.user.name}</span>
|
||||
<span>/u/{this.state.username}</span>
|
||||
</h5>
|
||||
{this.selects()}
|
||||
{this.state.view == View.Overview && this.overview()}
|
||||
{this.state.view == View.Comments && this.comments()}
|
||||
{this.state.view == View.Posts && this.posts()}
|
||||
{this.state.view == View.Saved && this.overview()}
|
||||
{this.paginator()}
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
this.selects()
|
||||
)}
|
||||
<UserDetails
|
||||
user_id={this.state.user_id}
|
||||
username={this.state.username}
|
||||
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">
|
||||
{this.userInfo()}
|
||||
{this.isCurrentUser && this.userSettings()}
|
||||
{this.moderates()}
|
||||
{this.follows()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -268,52 +267,52 @@ export class User extends Component<any, UserState> {
|
|||
<div class="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Overview && 'active'}
|
||||
${this.state.view == UserDetailsView.Overview && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Overview}
|
||||
checked={this.state.view == View.Overview}
|
||||
value={UserDetailsView.Overview}
|
||||
checked={this.state.view === UserDetailsView.Overview}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('overview')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Comments && 'active'}
|
||||
${this.state.view == UserDetailsView.Comments && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Comments}
|
||||
checked={this.state.view == View.Comments}
|
||||
value={UserDetailsView.Comments}
|
||||
checked={this.state.view == UserDetailsView.Comments}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('comments')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Posts && 'active'}
|
||||
${this.state.view == UserDetailsView.Posts && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Posts}
|
||||
checked={this.state.view == View.Posts}
|
||||
value={UserDetailsView.Posts}
|
||||
checked={this.state.view == UserDetailsView.Posts}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{i18n.t('posts')}
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer btn-outline-light
|
||||
${this.state.view == View.Saved && 'active'}
|
||||
${this.state.view == UserDetailsView.Saved && 'active'}
|
||||
`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value={View.Saved}
|
||||
checked={this.state.view == View.Saved}
|
||||
value={UserDetailsView.Saved}
|
||||
checked={this.state.view == UserDetailsView.Saved}
|
||||
onChange={linkEvent(this, this.handleViewChange)}
|
||||
/>
|
||||
{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() {
|
||||
let user = this.state.user;
|
||||
return (
|
||||
|
@ -722,7 +643,7 @@ export class User extends Component<any, UserState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.site.enable_nsfw && (
|
||||
{this.state.siteRes.site.enable_nsfw && (
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
|
@ -896,77 +817,30 @@ export class User extends Component<any, UserState> {
|
|||
);
|
||||
}
|
||||
|
||||
paginator() {
|
||||
return (
|
||||
<div class="my-2">
|
||||
{this.state.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>
|
||||
);
|
||||
}
|
||||
|
||||
updateUrl() {
|
||||
let viewStr = View[this.state.view].toLowerCase();
|
||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||
updateUrl(paramUpdates: UrlParams) {
|
||||
const page = paramUpdates.page || this.state.page;
|
||||
const viewStr =
|
||||
paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
|
||||
const sortStr =
|
||||
paramUpdates.sort || SortType[this.state.sort].toLowerCase();
|
||||
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) {
|
||||
i.state.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);
|
||||
handlePageChange(page: number) {
|
||||
this.updateUrl({ page });
|
||||
}
|
||||
|
||||
handleSortChange(val: SortType) {
|
||||
this.state.sort = val;
|
||||
this.state.page = 1;
|
||||
this.setState(this.state);
|
||||
this.updateUrl();
|
||||
this.refetch();
|
||||
this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
|
||||
}
|
||||
|
||||
handleViewChange(i: User, event: any) {
|
||||
i.state.view = Number(event.target.value);
|
||||
i.state.page = 1;
|
||||
i.setState(i.state);
|
||||
i.updateUrl();
|
||||
i.refetch();
|
||||
i.updateUrl({
|
||||
view: UserDetailsView[Number(event.target.value)].toLowerCase(),
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
|
||||
handleUserSettingsShowNsfwChange(i: User, event: any) {
|
||||
|
@ -1137,28 +1011,28 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
console.log(msg);
|
||||
let res = wsJsonToRes(msg);
|
||||
const res = wsJsonToRes(msg);
|
||||
if (msg.error) {
|
||||
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') {
|
||||
this.context.router.history.push('/');
|
||||
}
|
||||
this.setState(this.state);
|
||||
this.setState({
|
||||
deleteAccountLoading: false,
|
||||
avatarLoading: false,
|
||||
userSettingsLoading: false,
|
||||
});
|
||||
return;
|
||||
} else if (msg.reconnect) {
|
||||
this.refetch();
|
||||
} 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
|
||||
// and set the parent state if it is not set or differs
|
||||
const data = res.data as UserDetailsResponse;
|
||||
|
||||
if (this.state.user.id !== data.user.id) {
|
||||
this.state.user = data.user;
|
||||
this.state.comments = data.comments;
|
||||
this.state.follows = data.follows;
|
||||
this.state.moderates = data.moderates;
|
||||
this.state.posts = data.posts;
|
||||
this.state.admins = data.admins;
|
||||
this.state.loading = false;
|
||||
|
||||
if (this.isCurrentUser) {
|
||||
this.state.userSettingsForm.show_nsfw =
|
||||
UserService.Instance.user.show_nsfw;
|
||||
|
@ -1177,60 +1051,29 @@ export class User extends Component<any, UserState> {
|
|||
UserService.Instance.user.show_avatars;
|
||||
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
|
||||
}
|
||||
document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
|
||||
window.scrollTo(0, 0);
|
||||
this.state.loading = false;
|
||||
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) {
|
||||
let data = res.data as LoginResponse;
|
||||
this.state.userSettingsLoading = false;
|
||||
this.setState(this.state);
|
||||
const data = res.data as LoginResponse;
|
||||
UserService.Instance.login(data);
|
||||
this.setState({
|
||||
userSettingsLoading: false,
|
||||
});
|
||||
window.scrollTo(0, 0);
|
||||
} else if (res.op == UserOperation.DeleteAccount) {
|
||||
this.state.deleteAccountLoading = false;
|
||||
this.state.deleteAccountShowConfirm = false;
|
||||
this.setState(this.state);
|
||||
this.setState({
|
||||
deleteAccountLoading: false,
|
||||
deleteAccountShowConfirm: false,
|
||||
});
|
||||
this.context.router.history.push('/');
|
||||
} else if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
this.state.site = data.site;
|
||||
const data = res.data as GetSiteResponse;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
131
ui/src/interfaces.ts
vendored
131
ui/src/interfaces.ts
vendored
|
@ -9,19 +9,28 @@ export enum UserOperation {
|
|||
GetCommunity,
|
||||
CreateComment,
|
||||
EditComment,
|
||||
DeleteComment,
|
||||
RemoveComment,
|
||||
MarkCommentAsRead,
|
||||
SaveComment,
|
||||
CreateCommentLike,
|
||||
GetPosts,
|
||||
CreatePostLike,
|
||||
EditPost,
|
||||
DeletePost,
|
||||
RemovePost,
|
||||
LockPost,
|
||||
StickyPost,
|
||||
SavePost,
|
||||
EditCommunity,
|
||||
DeleteCommunity,
|
||||
RemoveCommunity,
|
||||
FollowCommunity,
|
||||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
MarkUserMentionAsRead,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
|
@ -40,6 +49,8 @@ export enum UserOperation {
|
|||
PasswordChange,
|
||||
CreatePrivateMessage,
|
||||
EditPrivateMessage,
|
||||
DeletePrivateMessage,
|
||||
MarkPrivateMessageAsRead,
|
||||
GetPrivateMessages,
|
||||
UserJoin,
|
||||
GetComments,
|
||||
|
@ -355,9 +366,9 @@ export interface GetUserMentionsResponse {
|
|||
mentions: Array<Comment>;
|
||||
}
|
||||
|
||||
export interface EditUserMentionForm {
|
||||
export interface MarkUserMentionAsReadForm {
|
||||
user_mention_id: number;
|
||||
read?: boolean;
|
||||
read: boolean;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
|
@ -571,13 +582,23 @@ export interface UserSettingsForm {
|
|||
|
||||
export interface CommunityForm {
|
||||
name: string;
|
||||
edit_id?: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
category_id: number;
|
||||
edit_id?: number;
|
||||
removed?: boolean;
|
||||
deleted?: 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;
|
||||
expires?: number;
|
||||
auth?: string;
|
||||
|
@ -592,7 +613,6 @@ export interface GetCommunityForm {
|
|||
export interface GetCommunityResponse {
|
||||
community: Community;
|
||||
moderators: Array<CommunityUser>;
|
||||
admins: Array<UserView>;
|
||||
online: number;
|
||||
}
|
||||
|
||||
|
@ -619,19 +639,37 @@ export interface PostForm {
|
|||
name: string;
|
||||
url?: string;
|
||||
body?: string;
|
||||
community_id: number;
|
||||
updated?: number;
|
||||
community_id?: number;
|
||||
edit_id?: number;
|
||||
creator_id: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
nsfw: boolean;
|
||||
locked?: boolean;
|
||||
stickied?: boolean;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface DeletePostForm {
|
||||
edit_id: number;
|
||||
deleted: boolean;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface RemovePostForm {
|
||||
edit_id: number;
|
||||
removed: boolean;
|
||||
reason?: 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 {
|
||||
name: string;
|
||||
url?: string;
|
||||
|
@ -649,7 +687,6 @@ export interface GetPostResponse {
|
|||
comments: Array<Comment>;
|
||||
community: Community;
|
||||
moderators: Array<CommunityUser>;
|
||||
admins: Array<UserView>;
|
||||
online: number;
|
||||
}
|
||||
|
||||
|
@ -665,14 +702,30 @@ export interface PostResponse {
|
|||
|
||||
export interface CommentForm {
|
||||
content: string;
|
||||
post_id: number;
|
||||
post_id?: number;
|
||||
parent_id?: number;
|
||||
edit_id?: number;
|
||||
creator_id?: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
form_id?: string;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface DeleteCommentForm {
|
||||
edit_id: number;
|
||||
deleted: boolean;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface RemoveCommentForm {
|
||||
edit_id: number;
|
||||
removed: boolean;
|
||||
reason?: string;
|
||||
read?: boolean;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
export interface MarkCommentAsReadForm {
|
||||
edit_id: number;
|
||||
read: boolean;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
|
@ -685,11 +738,11 @@ export interface SaveCommentForm {
|
|||
export interface CommentResponse {
|
||||
comment: Comment;
|
||||
recipient_ids: Array<number>;
|
||||
form_id?: string;
|
||||
}
|
||||
|
||||
export interface CommentLikeForm {
|
||||
comment_id: number;
|
||||
post_id: number;
|
||||
score: number;
|
||||
auth?: string;
|
||||
}
|
||||
|
@ -758,6 +811,7 @@ export interface GetSiteResponse {
|
|||
admins: Array<UserView>;
|
||||
banned: Array<UserView>;
|
||||
online: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface SiteResponse {
|
||||
|
@ -834,9 +888,19 @@ export interface PrivateMessageFormParams {
|
|||
|
||||
export interface EditPrivateMessageForm {
|
||||
edit_id: number;
|
||||
content?: string;
|
||||
deleted?: boolean;
|
||||
read?: boolean;
|
||||
content: string;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface DeletePrivateMessageForm {
|
||||
edit_id: number;
|
||||
deleted: boolean;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
export interface MarkPrivateMessageAsReadForm {
|
||||
edit_id: number;
|
||||
read: boolean;
|
||||
auth?: string;
|
||||
}
|
||||
|
||||
|
@ -864,18 +928,26 @@ export interface UserJoinResponse {
|
|||
}
|
||||
|
||||
export type MessageType =
|
||||
| EditPrivateMessageForm
|
||||
| LoginForm
|
||||
| RegisterForm
|
||||
| CommunityForm
|
||||
| DeleteCommunityForm
|
||||
| RemoveCommunityForm
|
||||
| FollowCommunityForm
|
||||
| ListCommunitiesForm
|
||||
| GetFollowedCommunitiesForm
|
||||
| PostForm
|
||||
| DeletePostForm
|
||||
| RemovePostForm
|
||||
| LockPostForm
|
||||
| StickyPostForm
|
||||
| GetPostForm
|
||||
| GetPostsForm
|
||||
| GetCommunityForm
|
||||
| CommentForm
|
||||
| DeleteCommentForm
|
||||
| RemoveCommentForm
|
||||
| MarkCommentAsReadForm
|
||||
| CommentLikeForm
|
||||
| SaveCommentForm
|
||||
| CreatePostLikeForm
|
||||
|
@ -890,7 +962,7 @@ export type MessageType =
|
|||
| GetUserDetailsForm
|
||||
| GetRepliesForm
|
||||
| GetUserMentionsForm
|
||||
| EditUserMentionForm
|
||||
| MarkUserMentionAsReadForm
|
||||
| GetModlogForm
|
||||
| SiteForm
|
||||
| SearchForm
|
||||
|
@ -900,6 +972,8 @@ export type MessageType =
|
|||
| PasswordChangeForm
|
||||
| PrivateMessageForm
|
||||
| EditPrivateMessageForm
|
||||
| DeletePrivateMessageForm
|
||||
| MarkPrivateMessageAsReadForm
|
||||
| GetPrivateMessagesForm
|
||||
| SiteConfigForm;
|
||||
|
||||
|
@ -937,3 +1011,10 @@ export interface WebSocketJsonResponse {
|
|||
error?: string;
|
||||
reconnect?: boolean;
|
||||
}
|
||||
|
||||
export enum UserDetailsView {
|
||||
Overview,
|
||||
Comments,
|
||||
Posts,
|
||||
Saved,
|
||||
}
|
||||
|
|
114
ui/src/services/WebSocketService.ts
vendored
114
ui/src/services/WebSocketService.ts
vendored
|
@ -4,9 +4,18 @@ import {
|
|||
RegisterForm,
|
||||
UserOperation,
|
||||
CommunityForm,
|
||||
DeleteCommunityForm,
|
||||
RemoveCommunityForm,
|
||||
PostForm,
|
||||
DeletePostForm,
|
||||
RemovePostForm,
|
||||
LockPostForm,
|
||||
StickyPostForm,
|
||||
SavePostForm,
|
||||
CommentForm,
|
||||
DeleteCommentForm,
|
||||
RemoveCommentForm,
|
||||
MarkCommentAsReadForm,
|
||||
SaveCommentForm,
|
||||
CommentLikeForm,
|
||||
GetPostForm,
|
||||
|
@ -28,7 +37,7 @@ import {
|
|||
UserView,
|
||||
GetRepliesForm,
|
||||
GetUserMentionsForm,
|
||||
EditUserMentionForm,
|
||||
MarkUserMentionAsReadForm,
|
||||
SearchForm,
|
||||
UserSettingsForm,
|
||||
DeleteAccountForm,
|
||||
|
@ -36,6 +45,8 @@ import {
|
|||
PasswordChangeForm,
|
||||
PrivateMessageForm,
|
||||
EditPrivateMessageForm,
|
||||
DeletePrivateMessageForm,
|
||||
MarkPrivateMessageAsReadForm,
|
||||
GetPrivateMessagesForm,
|
||||
GetCommentsForm,
|
||||
UserJoinForm,
|
||||
|
@ -103,18 +114,24 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
|
||||
}
|
||||
|
||||
public createCommunity(communityForm: CommunityForm) {
|
||||
this.setAuth(communityForm);
|
||||
this.ws.send(
|
||||
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
|
||||
);
|
||||
public createCommunity(form: CommunityForm) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
|
||||
}
|
||||
|
||||
public editCommunity(communityForm: CommunityForm) {
|
||||
this.setAuth(communityForm);
|
||||
this.ws.send(
|
||||
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
|
||||
);
|
||||
public editCommunity(form: CommunityForm) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -140,9 +157,9 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
|
||||
}
|
||||
|
||||
public createPost(postForm: PostForm) {
|
||||
this.setAuth(postForm);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm));
|
||||
public createPost(form: PostForm) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form));
|
||||
}
|
||||
|
||||
public getPost(form: GetPostForm) {
|
||||
|
@ -155,14 +172,29 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
|
||||
}
|
||||
|
||||
public createComment(commentForm: CommentForm) {
|
||||
this.setAuth(commentForm);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
|
||||
public createComment(form: CommentForm) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form));
|
||||
}
|
||||
|
||||
public editComment(commentForm: CommentForm) {
|
||||
this.setAuth(commentForm);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm));
|
||||
public editComment(form: CommentForm) {
|
||||
this.setAuth(form);
|
||||
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) {
|
||||
|
@ -190,9 +222,29 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
|
||||
}
|
||||
|
||||
public editPost(postForm: PostForm) {
|
||||
this.setAuth(postForm);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm));
|
||||
public editPost(form: PostForm) {
|
||||
this.setAuth(form);
|
||||
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) {
|
||||
|
@ -245,9 +297,9 @@ export class WebSocketService {
|
|||
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
|
||||
}
|
||||
|
||||
public editUserMention(form: EditUserMentionForm) {
|
||||
public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form));
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form));
|
||||
}
|
||||
|
||||
public getModlog(form: GetModlogForm) {
|
||||
|
@ -315,6 +367,18 @@ export class WebSocketService {
|
|||
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) {
|
||||
this.setAuth(form);
|
||||
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
|
||||
|
|
9
ui/src/utils.ts
vendored
9
ui/src/utils.ts
vendored
|
@ -49,6 +49,8 @@ import { UserService, WebSocketService } from './services';
|
|||
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
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 markdown_it_container from 'markdown-it-container';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
|
@ -148,6 +150,8 @@ export const md = new markdown_it({
|
|||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
.use(markdown_it_sub)
|
||||
.use(markdown_it_sup)
|
||||
.use(markdown_it_container, 'spoiler', {
|
||||
validate: function (params: any) {
|
||||
return params.trim().match(/^spoiler\s+(.*)$/);
|
||||
|
@ -825,6 +829,11 @@ export function editPostRes(data: PostResponse, post: Post) {
|
|||
post.url = data.post.url;
|
||||
post.name = data.post.name;
|
||||
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
1
ui/src/version.ts
vendored
|
@ -1 +0,0 @@
|
|||
export const version: string = 'v0.7.20';
|
16
ui/translations/en.json
vendored
16
ui/translations/en.json
vendored
|
@ -51,6 +51,15 @@
|
|||
"unsticky": "unsticky",
|
||||
"link": "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",
|
||||
"mods": "mods",
|
||||
"moderates": "Moderates",
|
||||
|
@ -154,7 +163,7 @@
|
|||
"email": "Email",
|
||||
"matrix_user_id": "Matrix User",
|
||||
"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",
|
||||
"optional": "Optional",
|
||||
"expires": "Expires",
|
||||
|
@ -247,12 +256,14 @@
|
|||
"couldnt_save_post": "Couldn't save post.",
|
||||
"no_slurs": "No slurs.",
|
||||
"not_an_admin": "Not an admin.",
|
||||
"not_a_moderator": "Not a moderator.",
|
||||
"site_already_exists": "Site already exists.",
|
||||
"couldnt_update_site": "Couldn't update site.",
|
||||
"couldnt_find_that_username_or_email":
|
||||
"Couldn't find that username or email.",
|
||||
"password_incorrect": "Password incorrect.",
|
||||
"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.",
|
||||
"admin_already_created": "Sorry, there's already an admin.",
|
||||
"user_already_exists": "User already exists.",
|
||||
|
@ -269,5 +280,6 @@
|
|||
"what_is": "What is",
|
||||
"cake_day_title": "Cake day:",
|
||||
"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."
|
||||
}
|
||||
|
|
28
ui/translations/eo.json
vendored
28
ui/translations/eo.json
vendored
|
@ -29,19 +29,19 @@
|
|||
"unlock": "malŝlosi",
|
||||
"lock": "ŝlosi",
|
||||
"link": "ligilo",
|
||||
"mod": "moderanto",
|
||||
"mods": "moderantoj",
|
||||
"moderates": "Moderigas",
|
||||
"mod": "reguligisto",
|
||||
"mods": "reguligistoj",
|
||||
"moderates": "reguligas",
|
||||
"settings": "Agordoj",
|
||||
"remove_as_mod": "forigi per moderanto",
|
||||
"appoint_as_mod": "nomumi per moderanto",
|
||||
"modlog": "Moderlogo",
|
||||
"remove_as_mod": "forigi kiel reguligisto",
|
||||
"appoint_as_mod": "nomumi reguligisto",
|
||||
"modlog": "Protokolo de reguligado",
|
||||
"admin": "administranto",
|
||||
"admins": "administrantoj",
|
||||
"remove_as_admin": "forigi kiel administranto",
|
||||
"appoint_as_admin": "nomumi administranto",
|
||||
"remove": "forigi",
|
||||
"removed": "fortirita",
|
||||
"removed": "forigita de reguligisto",
|
||||
"locked": "ŝlosita",
|
||||
"reason": "Kialo",
|
||||
"mark_as_read": "marki legita",
|
||||
|
@ -77,7 +77,7 @@
|
|||
"next": "Pluen",
|
||||
"sidebar": "Flankobreto",
|
||||
"sort_type": "Ordigilo",
|
||||
"hot": "Varmaj",
|
||||
"hot": "Furoraj",
|
||||
"new": "Novaj",
|
||||
"top_day": "Supraj tagaj",
|
||||
"week": "Semajno",
|
||||
|
@ -138,7 +138,7 @@
|
|||
"transfer_community": "transdoni la komunumon",
|
||||
"transfer_site": "transdoni la retejon",
|
||||
"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.",
|
||||
"community_ban": "Vi estas forbarita de la komunumo.",
|
||||
"site_ban": "Vi estas forbarita de la retejo",
|
||||
|
@ -152,7 +152,7 @@
|
|||
"couldnt_find_community": "Ne povis trovi la komunumon.",
|
||||
"couldnt_update_community": "Ne povis ĝisdatigi la komunumon.",
|
||||
"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_user_already_banned": "Uzanto de komunumo jam estas forbarita.",
|
||||
"couldnt_create_post": "Ne povis krei la afiŝon.",
|
||||
|
@ -256,5 +256,11 @@
|
|||
"number_of_upvotes_plural": "{{count}} porvoĉoj",
|
||||
"downvote": "Kontraŭvoĉi",
|
||||
"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"
|
||||
}
|
||||
|
|
33
ui/translations/fi.json
vendored
33
ui/translations/fi.json
vendored
|
@ -28,7 +28,7 @@
|
|||
"create_private_message": "Luo yksityisviesti",
|
||||
"send_secure_message": "Lähetä suojattu viesti",
|
||||
"send_message": "Lähetä viesti",
|
||||
"message": "Viesti",
|
||||
"message": "Lähetä",
|
||||
"edit": "muokkaa",
|
||||
"reply": "vastaa",
|
||||
"cancel": "Peru",
|
||||
|
@ -49,12 +49,12 @@
|
|||
"mods": "moderaattorit",
|
||||
"moderates": "Moderoi",
|
||||
"settings": "Asetukset",
|
||||
"remove_as_mod": "Poista moderaattorina",
|
||||
"remove_as_mod": "Poista moderaattorin asemasta",
|
||||
"appoint_as_mod": "Nimitä moderaattoriksi",
|
||||
"modlog": "Moderoinnin loki",
|
||||
"admin": "ylläpitäjä",
|
||||
"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",
|
||||
"remove": "poista",
|
||||
"removed": "poistettu",
|
||||
|
@ -126,7 +126,7 @@
|
|||
"login_sign_up": "Kirjaudu sisään / Rekisteröidy",
|
||||
"login": "Kirjaudu sisään",
|
||||
"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",
|
||||
"messages": "Viestit",
|
||||
"password": "Salasana",
|
||||
|
@ -169,31 +169,31 @@
|
|||
"theme": "Teema",
|
||||
"sponsors": "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",
|
||||
"donate_to_lemmy": "Lahjoita Lemmylle",
|
||||
"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",
|
||||
"bitcoin": "Bitcoin",
|
||||
"ethereum": "Ethereum",
|
||||
"monero": "Monero",
|
||||
"code": "Code",
|
||||
"code": "Lähdekoodi",
|
||||
"joined": "Liittyi",
|
||||
"by": "käyttäjältä",
|
||||
"to": "yhteisössä",
|
||||
"from": "paikasta",
|
||||
"transfer_community": "siirron yhteisö",
|
||||
"transfer_site": "siirron määrä",
|
||||
"transfer_community": "siirrä yhteisö",
|
||||
"transfer_site": "siirrä sivusto",
|
||||
"are_you_sure": "oletko varma?",
|
||||
"yes": "kyllä",
|
||||
"no": "ei",
|
||||
"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.",
|
||||
"logged_in": "Kirjautunut sisää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_like_comment": "Kommentista ei voitu tykätä.",
|
||||
"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.",
|
||||
"password_incorrect": "Salasana on väärin.",
|
||||
"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ä.",
|
||||
"user_already_exists": "Käyttäjä 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ä.",
|
||||
"couldnt_update_private_message": "Yksityisviestiä ei voitu päivittää.",
|
||||
"more": "lisää",
|
||||
"cross_posted_to": "ristipostattu: ",
|
||||
"cross_posted_to": "jaettu ristiin: ",
|
||||
"sorting_help": "apua lajitteluun",
|
||||
"show_context": "Näytä yhteys",
|
||||
"admin_settings": "Ylläpitäjän asetukset",
|
||||
|
@ -250,6 +251,10 @@
|
|||
"block_leaving": "Haluatko varmasti poistua?",
|
||||
"silver_sponsors": "Hopeasponsoreita ovat ne, jotka lupaavat 40 dollaria Lemmylle.",
|
||||
"post_title_too_long": "Viestin otsikko on liian pitkä.",
|
||||
"support_on_open_collective": "Tie OpenCollectivessa",
|
||||
"site_saved": "Sivu tallennettu."
|
||||
"support_on_open_collective": "Tue OpenCollectivessa",
|
||||
"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"
|
||||
}
|
||||
|
|
9
ui/translations/fr.json
vendored
9
ui/translations/fr.json
vendored
|
@ -192,7 +192,7 @@
|
|||
"yes": "oui",
|
||||
"no": "non",
|
||||
"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é.",
|
||||
"logged_in": "Vous êtes connecté.",
|
||||
"community_ban": "Vous avez été banni de cette communauté.",
|
||||
|
@ -256,5 +256,10 @@
|
|||
"invalid_username": "Nom d'utilisateur invalide.",
|
||||
"invalid_community_name": "Nom invalide.",
|
||||
"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 :"
|
||||
}
|
||||
|
|
10
ui/translations/it.json
vendored
10
ui/translations/it.json
vendored
|
@ -160,7 +160,7 @@
|
|||
"yes": "sì",
|
||||
"no": "no",
|
||||
"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.",
|
||||
"community_ban": "Sei stato escluso da questa comunità.",
|
||||
"site_ban": "Sei stato escluso dal sito",
|
||||
|
@ -256,5 +256,11 @@
|
|||
"click_to_delete_picture": "Clicca per eliminare la foto.",
|
||||
"picture_deleted": "Foto eliminata.",
|
||||
"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"
|
||||
}
|
||||
|
|
9
ui/translations/pt_BR.json
vendored
9
ui/translations/pt_BR.json
vendored
|
@ -192,7 +192,7 @@
|
|||
"yes": "sim",
|
||||
"no": "não",
|
||||
"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.",
|
||||
"logged_in": "Autenticado.",
|
||||
"community_ban": "Você foi banido desta comunidade.",
|
||||
|
@ -256,5 +256,10 @@
|
|||
"site_saved": "Site Salvo.",
|
||||
"emoji_picker": "Selecionador de Emoji",
|
||||
"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:"
|
||||
}
|
||||
|
|
7
ui/translations/ru.json
vendored
7
ui/translations/ru.json
vendored
|
@ -135,7 +135,7 @@
|
|||
"code": "Код",
|
||||
"joined": "Присоединился",
|
||||
"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": "Не авторизованы.",
|
||||
"community_ban": "Вы были заблокированы на данном сообществе.",
|
||||
"site_ban": "Вы были заблокированы на данном сайте",
|
||||
|
@ -235,7 +235,7 @@
|
|||
"matrix_user_id": "Матрица пользователя",
|
||||
"are_you_sure": "вы уверены?",
|
||||
"archive_link": "архивировать ссылку",
|
||||
"logged_in": "Войти в систему.",
|
||||
"logged_in": "Вошли в систему.",
|
||||
"couldnt_get_comments": "Не удалось получить комментарии.",
|
||||
"from": "от",
|
||||
"transfer_site": "трансфер сайт",
|
||||
|
@ -265,5 +265,6 @@
|
|||
"monero": "Monero",
|
||||
"emoji_picker": "Сборщик эмодзи",
|
||||
"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
Loading…
Reference in a new issue