Compare commits
74 commits
master
...
cache-cont
Author | SHA1 | Date | |
---|---|---|---|
cb928b037e | |||
02bcbc42d6 | |||
66c95993dc | |||
|
8079b6faef | ||
|
fe264a2f30 | ||
|
2cb57b833d | ||
|
588010ea88 | ||
|
2d95db8a7d | ||
|
bd99f4994a | ||
|
813b053b5f | ||
|
0f09171d68 | ||
|
7b492cc477 | ||
|
912871d0ac | ||
|
07d7664a38 | ||
|
02cf67de4a | ||
|
4157bf9a02 | ||
|
1180b89268 | ||
|
d22bbafebb | ||
|
e3d4f9418e | ||
|
beb63aedc8 | ||
|
e339f90737 | ||
|
8c1316aa96 | ||
|
ec146a0dea | ||
|
c939c15530 | ||
c01b40c517 | |||
ddd4baf103 | |||
|
a998bfc1f5 | ||
|
3c6eb37a1b | ||
|
2512babff1 | ||
|
f71d19729a | ||
|
04da8146ba | ||
|
b63aabfdc2 | ||
4b6bba0e7b | |||
|
a95704d5fc | ||
|
dbd1d8faa5 | ||
|
868ba5b64c | ||
49de4ccbd9 | |||
af83ec951f | |||
|
b365dd2349 | ||
|
9e20ddbfa4 | ||
|
22904e1c66 | ||
f7156bdac3 | |||
|
b6c297766b | ||
|
a6bc0edc91 | ||
|
ddb512b1ae | ||
|
88fed73ea3 | ||
|
51c8735682 | ||
|
94c4504b33 | ||
|
c06d01f753 | ||
|
daf22a12d9 | ||
|
dc1fc1e04c | ||
|
18d4b3d2aa | ||
|
3a85515bd5 | ||
|
b4b8e9d7f5 | ||
|
807dd8d82c | ||
|
c31fe3857c | ||
|
14418c5a0d | ||
|
0799ae1a1f | ||
|
8a9f1dbb59 | ||
|
a42e2af203 | ||
|
8bf5d0cca6 | ||
|
6b68d54e35 | ||
|
7d291ee95a | ||
|
6248392992 | ||
f18ebed740 | |||
10da3f2554 | |||
8fb34843aa | |||
a882fbea97 | |||
ae3fccf701 | |||
f7333705dc | |||
140eff181c | |||
2c26cc26b8 | |||
bad4868a10 | |||
|
844a97a6a5 |
110 changed files with 4967 additions and 3655 deletions
1
.dockerignore
vendored
1
.dockerignore
vendored
|
@ -1,5 +1,4 @@
|
||||||
ui/node_modules
|
ui/node_modules
|
||||||
ui/dist
|
ui/dist
|
||||||
server/target
|
server/target
|
||||||
docs
|
|
||||||
.git
|
.git
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
ansible/inventory
|
ansible/inventory
|
||||||
ansible/passwords/
|
ansible/passwords/
|
||||||
|
docker/lemmy_mine.hjson
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
22
.travis.yml
vendored
22
.travis.yml
vendored
|
@ -5,21 +5,27 @@ matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rust: nightly
|
- rust: nightly
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
cache:
|
cache: cargo
|
||||||
directories:
|
|
||||||
- /home/travis/.cargo
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -rf /home/travis/.cargo/registry
|
- rm -rfv target/debug/incremental/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/.fingerprint/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/build/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/deps/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/lemmy_server.d
|
||||||
|
- cargo clean
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
- psql -c 'create database rrr with owner rrr;' -U postgres
|
- psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
|
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
|
||||||
before_install:
|
before_install:
|
||||||
- cd server
|
- cd server
|
||||||
script:
|
script:
|
||||||
- diesel migration run
|
# Default checks, but fail if anything is detected
|
||||||
|
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
||||||
- cargo build
|
- cargo build
|
||||||
|
- diesel migration run
|
||||||
- cargo test
|
- cargo test
|
||||||
env:
|
env:
|
||||||
- DATABASE_URL=postgres://rrr:rrr@localhost/rrr
|
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.4"
|
||||||
|
|
141
README.md
vendored
141
README.md
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
||||||
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
||||||
![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)
|
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
|
||||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||||
|
@ -36,30 +36,17 @@ Front Page|Post
|
||||||
---|---
|
---|---
|
||||||
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
||||||
|
|
||||||
## 📝 Table of Contents
|
[Lemmy](https://github.com/dessalines/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).
|
||||||
|
|
||||||
<!-- toc -->
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
- [Features](#features)
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
- [About](#about)
|
|
||||||
* [Why's it called Lemmy?](#whys-it-called-lemmy)
|
|
||||||
- [Install](#install)
|
|
||||||
* [Docker](#docker)
|
|
||||||
+ [Updating](#updating)
|
|
||||||
* [Ansible](#ansible)
|
|
||||||
* [Kubernetes](#kubernetes)
|
|
||||||
- [Develop](#develop)
|
|
||||||
* [Docker Development](#docker-development)
|
|
||||||
* [Local Development](#local-development)
|
|
||||||
+ [Requirements](#requirements)
|
|
||||||
+ [Set up Postgres DB](#set-up-postgres-db)
|
|
||||||
+ [Running](#running)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [Support](#support)
|
|
||||||
- [Translations](#translations)
|
|
||||||
- [Credits](#credits)
|
|
||||||
|
|
||||||
<!-- tocstop -->
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
||||||
|
|
||||||
|
[Documentation](https://dev.lemmy.ml/docs/index.html)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -90,25 +77,13 @@ Front Page|Post
|
||||||
- Front end is `~80kB` gzipped.
|
- Front end is `~80kB` gzipped.
|
||||||
- Supports arm64 / Raspberry Pi.
|
- Supports arm64 / Raspberry Pi.
|
||||||
|
|
||||||
## About
|
## Why's it called Lemmy?
|
||||||
|
|
||||||
[Lemmy](https://github.com/dessalines/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).
|
|
||||||
|
|
||||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
|
||||||
|
|
||||||
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
|
||||||
|
|
||||||
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
|
||||||
|
|
||||||
### Why's it called Lemmy?
|
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
@ -119,8 +94,8 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||||
mkdir lemmy/
|
mkdir lemmy/
|
||||||
cd lemmy/
|
cd lemmy/
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/.env
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
# Edit the .env if you want custom passwords
|
# Edit lemmy.hjson to do more configuration
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -156,79 +131,6 @@ nano inventory # enter your server, domain, contact email
|
||||||
ansible-playbook lemmy.yml --become
|
ansible-playbook lemmy.yml --become
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kubernetes
|
|
||||||
|
|
||||||
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
|
|
||||||
Setting this up will vary depending on your provider.
|
|
||||||
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
|
||||||
|
|
||||||
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
|
|
||||||
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
|
|
||||||
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
|
|
||||||
|
|
||||||
**Important** Running a database in Kubernetes will work, but is generally not recommended.
|
|
||||||
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
|
|
||||||
|
|
||||||
Now you can deploy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
|
|
||||||
# otherwise your resources will be created in the `default` namespace.
|
|
||||||
kubectl apply -f docker/k8s/db.yml
|
|
||||||
kubectl apply -f docker/k8s/pictshare.yml
|
|
||||||
kubectl apply -f docker/k8s/lemmy.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
### Docker Development
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dessalines/lemmy
|
|
||||||
cd lemmy/docker/dev
|
|
||||||
./docker_update.sh # This builds and runs it, updating for your changes
|
|
||||||
```
|
|
||||||
|
|
||||||
and go to http://localhost:8536.
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
#### Requirements
|
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org/)
|
|
||||||
- [Yarn](https://yarnpkg.com/en/)
|
|
||||||
- [Postgres](https://www.postgresql.org/)
|
|
||||||
|
|
||||||
#### Set up Postgres DB
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
|
||||||
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
|
||||||
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dessalines/lemmy
|
|
||||||
cd lemmy
|
|
||||||
./install.sh
|
|
||||||
# For live coding, where both the front and back end, automagically reload on any save, do:
|
|
||||||
# cd ui && yarn start
|
|
||||||
# cd server && cargo watch -x run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Websocket API for App developers](docs/api.md)
|
|
||||||
- [ActivityPub API.md](docs/apub_api_outline.md)
|
|
||||||
- [Goals](docs/goals.md)
|
|
||||||
- [Ranking Algorithm](docs/ranking.md)
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
@ -247,16 +149,15 @@ If you'd like to add translations, take a look a look at the [English translatio
|
||||||
|
|
||||||
lang | done | missing
|
lang | done | missing
|
||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
de | 100% |
|
de | 95% | avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
eo | 86% | number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme,are_you_sure,yes,no
|
eo | 82% | number_of_communities,preview,upload_image,avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
|
||||||
es | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
es | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
fr | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
fr | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
it | 96% | archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
it | 91% | avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
nl | 88% | preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme
|
nl | 84% | preview,upload_image,avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
|
||||||
ru | 82% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
ru | 78% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
sv | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
sv | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
zh | 80% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
zh | 76% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
|
|
||||||
|
|
||||||
If you'd like to update this report, run:
|
If you'd like to update this report, run:
|
||||||
|
|
||||||
|
|
10
ansible/lemmy.yml
vendored
10
ansible/lemmy.yml
vendored
|
@ -32,21 +32,13 @@
|
||||||
- name: add all template files
|
- name: add all template files
|
||||||
template: src={{item.src}} dest={{item.dest}}
|
template: src={{item.src}} dest={{item.dest}}
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/env', dest: '/lemmy/.env' }
|
|
||||||
- { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
|
- { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
|
||||||
|
- { src: 'templates/config.hjson', dest: '/lemmy/lemmy.hjson' }
|
||||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
|
||||||
vars:
|
vars:
|
||||||
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
||||||
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
||||||
|
|
||||||
- name: set env file permissions
|
|
||||||
file:
|
|
||||||
path: "/lemmy/.env"
|
|
||||||
state: touch
|
|
||||||
mode: 0600
|
|
||||||
access_time: preserve
|
|
||||||
modification_time: preserve
|
|
||||||
|
|
||||||
- name: enable and start docker service
|
- name: enable and start docker service
|
||||||
systemd:
|
systemd:
|
||||||
name: docker
|
name: docker
|
||||||
|
|
15
ansible/templates/config.hjson
vendored
Normal file
15
ansible/templates/config.hjson
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
database: {
|
||||||
|
password: "{{ postgres_password }}"
|
||||||
|
host: "lemmy_db"
|
||||||
|
}
|
||||||
|
hostname: "{{ domain }}"
|
||||||
|
jwt_secret: "{{ jwt_password }}"
|
||||||
|
front_end_dir: "/app/dist"
|
||||||
|
email: {
|
||||||
|
smtp_server: "{{ smtp_server }}"
|
||||||
|
smtp_login: "{{ smtp_login }}"
|
||||||
|
smtp_password: "{{ smtp_password }}"
|
||||||
|
smtp_from_address: "{{ smtp_from_address }}"
|
||||||
|
}
|
||||||
|
}
|
14
ansible/templates/env
vendored
14
ansible/templates/env
vendored
|
@ -1,14 +0,0 @@
|
||||||
DOMAIN={{ domain }}
|
|
||||||
DATABASE_PASSWORD={{ postgres_password }}
|
|
||||||
DATABASE_URL=postgres://lemmy:{{ postgres_password }}@lemmy_db:5432/lemmy
|
|
||||||
JWT_SECRET={{ jwt_password }}
|
|
||||||
RATE_LIMIT_MESSAGE=30
|
|
||||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
|
||||||
RATE_LIMIT_POST=3
|
|
||||||
RATE_LIMIT_POST_PER_SECOND=600
|
|
||||||
RATE_LIMIT_REGISTER=3
|
|
||||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
|
||||||
SMTP_SERVER={{ smtp_server }}
|
|
||||||
SMTP_LOGIN={{ smtp_login }}
|
|
||||||
SMTP_PASSWORD={{ smtp_password }}
|
|
||||||
SMTP_FROM_ADDRESS={{ smtp_from_address }}
|
|
17
docker/dev/.env
vendored
17
docker/dev/.env
vendored
|
@ -1,17 +0,0 @@
|
||||||
DOMAIN=my_domain
|
|
||||||
DATABASE_PASSWORD=password
|
|
||||||
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
|
|
||||||
JWT_SECRET=changeme
|
|
||||||
|
|
||||||
RATE_LIMIT_MESSAGE=30
|
|
||||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
|
||||||
RATE_LIMIT_POST=6
|
|
||||||
RATE_LIMIT_POST_PER_SECOND=600
|
|
||||||
RATE_LIMIT_REGISTER=3
|
|
||||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
|
||||||
|
|
||||||
# Optional email fields
|
|
||||||
SMTP_SERVER=
|
|
||||||
SMTP_LOGIN=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>
|
|
11
docker/dev/Dockerfile
vendored
11
docker/dev/Dockerfile
vendored
|
@ -32,14 +32,25 @@ RUN cargo build --frozen --release
|
||||||
# Get diesel-cli on there just in case
|
# Get diesel-cli on there just in case
|
||||||
# RUN cargo install diesel_cli --no-default-features --features postgres
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
|
||||||
|
FROM ekidd/rust-musl-builder:1.38.0-openssl11 as docs
|
||||||
|
WORKDIR /app
|
||||||
|
COPY docs ./docs
|
||||||
|
RUN sudo chown -R rust:rust .
|
||||||
|
RUN mdbook build docs/
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.10
|
FROM alpine:3.10
|
||||||
|
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
|
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
|
||||||
|
COPY --from=docs /app/docs/book/ /app/dist/documentation/
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
RUN addgroup -g 1000 lemmy
|
RUN addgroup -g 1000 lemmy
|
||||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
RUN chown lemmy:lemmy /app/lemmy
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
|
|
1
docker/dev/Dockerfile.aarch64
vendored
1
docker/dev/Dockerfile.aarch64
vendored
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
||||||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
|
1
docker/dev/Dockerfile.armv7hf
vendored
1
docker/dev/Dockerfile.armv7hf
vendored
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
||||||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
|
2
docker/dev/Dockerfile.libc
vendored
2
docker/dev/Dockerfile.libc
vendored
|
@ -65,8 +65,10 @@ RUN addgroup --gid 1000 lemmy
|
||||||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
RUN chown lemmy:lemmy /app/lemmy
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
USER lemmy
|
USER lemmy
|
||||||
EXPOSE 8536
|
EXPOSE 8536
|
||||||
|
|
12
docker/dev/deploy.sh
vendored
12
docker/dev/deploy.sh
vendored
|
@ -5,12 +5,14 @@ git checkout master
|
||||||
new_tag="$1"
|
new_tag="$1"
|
||||||
git tag $new_tag
|
git tag $new_tag
|
||||||
|
|
||||||
|
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||||
|
|
||||||
# Setting the version on the front end
|
# Setting the version on the front end
|
||||||
cd ../../
|
cd ../../
|
||||||
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||||
git add "ui/src/version.ts"
|
git add "ui/src/version.ts"
|
||||||
# Setting the version on the backend
|
# Setting the version on the backend
|
||||||
echo "pub const VERSION: &'static str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||||
git add "server/src/version.rs"
|
git add "server/src/version.rs"
|
||||||
|
|
||||||
cd docker/dev
|
cd docker/dev
|
||||||
|
@ -38,14 +40,22 @@ docker push dessalines/lemmy:x64-$new_tag
|
||||||
# docker push dessalines/lemmy:armv7hf-$new_tag
|
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||||
|
|
||||||
# aarch64
|
# aarch64
|
||||||
|
# Only do this on major releases (IE the third semver is 0)
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||||
docker push dessalines/lemmy:arm64-$new_tag
|
docker push dessalines/lemmy:arm64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
# Creating the manifest for the multi-arch build
|
# Creating the manifest for the multi-arch build
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
docker manifest create dessalines/lemmy:$new_tag \
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
dessalines/lemmy:x64-$new_tag \
|
dessalines/lemmy:x64-$new_tag \
|
||||||
dessalines/lemmy:arm64-$new_tag
|
dessalines/lemmy:arm64-$new_tag
|
||||||
|
else
|
||||||
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
|
dessalines/lemmy:x64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
docker manifest push dessalines/lemmy:$new_tag
|
docker manifest push dessalines/lemmy:$new_tag
|
||||||
|
|
||||||
|
|
19
docker/dev/docker-compose.yml
vendored
19
docker/dev/docker-compose.yml
vendored
|
@ -5,7 +5,7 @@ services:
|
||||||
image: postgres:12-alpine
|
image: postgres:12-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=lemmy
|
- POSTGRES_USER=lemmy
|
||||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
- POSTGRES_PASSWORD=password
|
||||||
- POSTGRES_DB=lemmy
|
- POSTGRES_DB=lemmy
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
|
@ -16,22 +16,9 @@ services:
|
||||||
dockerfile: docker/dev/Dockerfile
|
dockerfile: docker/dev/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
environment:
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- HOSTNAME=${DOMAIN}
|
|
||||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
|
||||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
|
||||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
|
||||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
|
||||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
|
||||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
|
||||||
- SMTP_SERVER=${SMTP_SERVER}
|
|
||||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ../lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- lemmy_db
|
- lemmy_db
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
|
6
docker/k8s/lemmy.yml
vendored
6
docker/k8s/lemmy.yml
vendored
|
@ -14,13 +14,13 @@ spec:
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- env:
|
- env:
|
||||||
- name: DATABASE_URL
|
- name: LEMMY_DATABASE_URL
|
||||||
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: HOSTNAME
|
- name: LEMMY_HOSTNAME
|
||||||
# example: 'lemmy.example.com'
|
# example: 'lemmy.example.com'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: JWT_SECRET
|
- name: LEMMY_JWT_SECRET
|
||||||
# example: 'very-super-good-secret'
|
# example: 'very-super-good-secret'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: LEMMY_FRONT_END_DIR
|
- name: LEMMY_FRONT_END_DIR
|
||||||
|
|
56
docker/lemmy.hjson
vendored
Normal file
56
docker/lemmy.hjson
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
database: {
|
||||||
|
# username to connect to postgres
|
||||||
|
user: "lemmy"
|
||||||
|
# password to connect to postgres
|
||||||
|
password: "password"
|
||||||
|
# host where postgres is running
|
||||||
|
host: "lemmy_db"
|
||||||
|
# port where postgres can be accessed
|
||||||
|
port: 5432
|
||||||
|
# name of the postgres database for lemmy
|
||||||
|
database: "lemmy"
|
||||||
|
# maximum number of active sql connections
|
||||||
|
pool_size: 5
|
||||||
|
}
|
||||||
|
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||||
|
hostname: "rrr"
|
||||||
|
# address where lemmy should listen for incoming requests
|
||||||
|
bind: "0.0.0.0"
|
||||||
|
# port where lemmy should listen for incoming requests
|
||||||
|
port: 8536
|
||||||
|
# json web token for authorization between server and client
|
||||||
|
jwt_secret: "changeme"
|
||||||
|
# The dir for the front end
|
||||||
|
front_end_dir: "/app/dist"
|
||||||
|
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
|
||||||
|
# cause problems like remote instances fetching and permanently storing bad data.
|
||||||
|
federation_enabled: false
|
||||||
|
# rate limits for various user actions, by user ip
|
||||||
|
rate_limit: {
|
||||||
|
# maximum number of messages created in interval
|
||||||
|
message: 30
|
||||||
|
# interval length for message limit
|
||||||
|
message_per_second: 60
|
||||||
|
# maximum number of posts created in interval
|
||||||
|
post: 6
|
||||||
|
# interval length for post limit
|
||||||
|
post_per_second: 600
|
||||||
|
# maximum number of registrations in interval
|
||||||
|
register: 3
|
||||||
|
# interval length for registration limit
|
||||||
|
register_per_second: 3600
|
||||||
|
}
|
||||||
|
# # email sending configuration
|
||||||
|
# email: {
|
||||||
|
# # hostname of the smtp server
|
||||||
|
# smtp_server: ""
|
||||||
|
# # login name for smtp server
|
||||||
|
# smtp_login: ""
|
||||||
|
# # password to login to the smtp server
|
||||||
|
# smtp_password: ""
|
||||||
|
# # address to send emails from, eg "info@your-instance.com"
|
||||||
|
# smtp_from_address: ""
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
|
17
docker/prod/.env
vendored
17
docker/prod/.env
vendored
|
@ -1,17 +0,0 @@
|
||||||
DOMAIN=my_domain
|
|
||||||
DATABASE_PASSWORD=password
|
|
||||||
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
|
|
||||||
JWT_SECRET=changeme
|
|
||||||
|
|
||||||
RATE_LIMIT_MESSAGE=30
|
|
||||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
|
||||||
RATE_LIMIT_POST=6
|
|
||||||
RATE_LIMIT_POST_PER_SECOND=600
|
|
||||||
RATE_LIMIT_REGISTER=3
|
|
||||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
|
||||||
|
|
||||||
# Optional email fields
|
|
||||||
SMTP_SERVER=
|
|
||||||
SMTP_LOGIN=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>
|
|
21
docker/prod/docker-compose.yml
vendored
21
docker/prod/docker-compose.yml
vendored
|
@ -5,31 +5,18 @@ services:
|
||||||
image: postgres:12-alpine
|
image: postgres:12-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=lemmy
|
- POSTGRES_USER=lemmy
|
||||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
- POSTGRES_PASSWORD=password
|
||||||
- POSTGRES_DB=lemmy
|
- POSTGRES_DB=lemmy
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.5.0.3
|
image: dessalines/lemmy:v0.5.17
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
environment:
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- HOSTNAME=${DOMAIN}
|
|
||||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
|
||||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
|
||||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
|
||||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
|
||||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
|
||||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
|
||||||
- SMTP_SERVER=${SMTP_SERVER}
|
|
||||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- lemmy_db
|
- lemmy_db
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
book
|
6
docs/book.toml
vendored
Normal file
6
docs/book.toml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[book]
|
||||||
|
authors = ["Felix Ableitner"]
|
||||||
|
language = "en"
|
||||||
|
multilingual = false
|
||||||
|
src = "src"
|
||||||
|
title = "Lemmy Documentation"
|
16
docs/src/SUMMARY.md
vendored
Normal file
16
docs/src/SUMMARY.md
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
- [About](about.md)
|
||||||
|
- [Features](about_features.md)
|
||||||
|
- [Goals](about_goals.md)
|
||||||
|
- [Post and Comment Ranking](about_ranking.md)
|
||||||
|
- [Administration](administration.md)
|
||||||
|
- [Install with Docker](administration_install_docker.md)
|
||||||
|
- [Install with Ansible](administration_install_ansible.md)
|
||||||
|
- [Install with Kubernetes](administration_install_kubernetes.md)
|
||||||
|
- [Configuration](administration_configuration.md)
|
||||||
|
- [Contributing](contributing.md)
|
||||||
|
- [Docker Development](contributing_docker_development.md)
|
||||||
|
- [Local Development](contributing_local_development.md)
|
||||||
|
- [Websocket API](contributing_websocket_api.md)
|
||||||
|
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
20
docs/src/about.md
vendored
Normal file
20
docs/src/about.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Lemmy - A link aggregator / reddit clone for the fediverse.
|
||||||
|
|
||||||
|
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
|
||||||
|
|
||||||
|
[Lemmy](https://github.com/dessalines/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).
|
||||||
|
|
||||||
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
|
|
||||||
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
### Why's it called Lemmy?
|
||||||
|
|
||||||
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
27
docs/src/about_features.md
vendored
Normal file
27
docs/src/about_features.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Features
|
||||||
|
- Open source, [AGPL License](/LICENSE).
|
||||||
|
- Self hostable, easy to deploy.
|
||||||
|
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||||
|
- Clean, mobile-friendly interface.
|
||||||
|
- Live-updating Comment threads.
|
||||||
|
- Full vote scores `(+/-)` like old reddit.
|
||||||
|
- Themes, including light, dark, and solarized.
|
||||||
|
- Emojis with autocomplete support. Start typing `:`
|
||||||
|
- User tagging using `@`, Community tagging using `#`.
|
||||||
|
- Notifications, on comment replies and when you're tagged.
|
||||||
|
- i18n / internationalization support.
|
||||||
|
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||||
|
- Cross-posting support.
|
||||||
|
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||||
|
- Moderation abilities.
|
||||||
|
- Public Moderation Logs.
|
||||||
|
- Both site admins, and community moderators, who can appoint other moderators.
|
||||||
|
- Can lock, remove, and restore posts and comments.
|
||||||
|
- Can ban and unban users from communities and the site.
|
||||||
|
- Can transfer site and communities to others.
|
||||||
|
- Can fully erase your data, replacing all posts and comments.
|
||||||
|
- NSFW post / community support.
|
||||||
|
- High performance.
|
||||||
|
- Server is written in rust.
|
||||||
|
- Front end is `~80kB` gzipped.
|
||||||
|
- Supports arm64 / Raspberry Pi.
|
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
1
docs/src/administration.md
vendored
Normal file
1
docs/src/administration.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Information for Lemmy instance admins, and those who want to start an instance.
|
6
docs/src/administration_configuration.md
vendored
Normal file
6
docs/src/administration_configuration.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
The configuration is based on the file [defaults.hjson](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.
|
||||||
|
|
||||||
|
Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with
|
||||||
|
`LEMMY__DATABASE__POOL_SIZE=10`.
|
||||||
|
|
||||||
|
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once.
|
11
docs/src/administration_install_ansible.md
vendored
Normal file
11
docs/src/administration_install_ansible.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
|
||||||
|
|
||||||
|
Then run the following commands on your local computer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy.git
|
||||||
|
cd lemmy/ansible/
|
||||||
|
cp inventory.example inventory
|
||||||
|
nano inventory # enter your server, domain, contact email
|
||||||
|
ansible-playbook lemmy.yml --become
|
||||||
|
```
|
28
docs/src/administration_install_docker.md
vendored
Normal file
28
docs/src/administration_install_docker.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir lemmy/
|
||||||
|
cd lemmy/
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
|
# Edit lemmy.hjson to do more configuration
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
|
[A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
||||||
|
# Replace the {{ vars }}
|
||||||
|
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||||
|
```
|
||||||
|
#### Updating
|
||||||
|
|
||||||
|
To update to the newest version, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
|
||||||
|
Setting this up will vary depending on your provider.
|
||||||
|
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
||||||
|
|
||||||
|
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
|
||||||
|
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
|
||||||
|
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
|
||||||
|
|
||||||
|
**Important** Running a database in Kubernetes will work, but is generally not recommended.
|
||||||
|
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
|
||||||
|
|
||||||
|
Now you can deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
|
||||||
|
# otherwise your resources will be created in the `default` namespace.
|
||||||
|
kubectl apply -f docker/k8s/db.yml
|
||||||
|
kubectl apply -f docker/k8s/pictshare.yml
|
||||||
|
kubectl apply -f docker/k8s/lemmy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
|
1
docs/src/contributing.md
vendored
Normal file
1
docs/src/contributing.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.
|
11
docs/src/contributing_docker_development.md
vendored
Normal file
11
docs/src/contributing_docker_development.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy
|
||||||
|
cd lemmy/docker/dev
|
||||||
|
./docker_update.sh # This builds and runs it, updating for your changes
|
||||||
|
```
|
||||||
|
|
||||||
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
|
Note that compile times are relatively long with Docker, because builds can't be properly cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
|
24
docs/src/contributing_local_development.md
vendored
Normal file
24
docs/src/contributing_local_development.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
- [Rust](https://www.rust-lang.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/en/)
|
||||||
|
- [Postgres](https://www.postgresql.org/)
|
||||||
|
|
||||||
|
#### Set up Postgres DB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
|
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
|
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy
|
||||||
|
cd lemmy
|
||||||
|
./install.sh
|
||||||
|
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||||
|
# cd ui && yarn start
|
||||||
|
# cd server && cargo watch -x run
|
||||||
|
```
|
2346
server/Cargo.lock
generated
vendored
2346
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
27
server/Cargo.toml
vendored
27
server/Cargo.toml
vendored
|
@ -5,28 +5,31 @@ authors = ["Dessalines <happydooby@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "1.4.2", features = ["postgres","chrono"] }
|
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
dotenv = "0.14.1"
|
dotenv = "0.15.0"
|
||||||
bcrypt = "0.5.0"
|
bcrypt = "0.6.1"
|
||||||
activitypub = "0.1.5"
|
activitypub = "0.2.0"
|
||||||
chrono = { version = "0.4.7", features = ["serde"] }
|
chrono = { version = "0.4.7", features = ["serde"] }
|
||||||
failure = "0.1.5"
|
failure = "0.1.5"
|
||||||
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
||||||
serde = { version = "1.0.94", features = ["derive"] }
|
serde = { version = "1.0.94", features = ["derive"] }
|
||||||
actix = "0.8.3"
|
actix = "0.9.0"
|
||||||
actix-web = "1.0"
|
actix-web = "2.0.0"
|
||||||
actix-files = "0.1.3"
|
actix-files = "0.2.1"
|
||||||
actix-web-actors = "1.0"
|
actix-web-actors = "2.0.0"
|
||||||
env_logger = "0.6.2"
|
actix-rt = "1.0.0"
|
||||||
|
env_logger = "0.7.1"
|
||||||
rand = "0.7.0"
|
rand = "0.7.0"
|
||||||
strum = "0.15.0"
|
strum = "0.17.1"
|
||||||
strum_macros = "0.15.0"
|
strum_macros = "0.17.1"
|
||||||
jsonwebtoken = "6.0.1"
|
jsonwebtoken = "6.0.1"
|
||||||
regex = "1.1.9"
|
regex = "1.1.9"
|
||||||
lazy_static = "1.3.0"
|
lazy_static = "1.3.0"
|
||||||
lettre = "0.9.2"
|
lettre = "0.9.2"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
rust-crypto = "^0.2"
|
sha2 = "0.8.0"
|
||||||
rss = "1.8.0"
|
rss = "1.8.0"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
|
config = "0.10.1"
|
||||||
|
hjson = "0.8.2"
|
||||||
|
|
7
server/clean.sh
vendored
Executable file
7
server/clean.sh
vendored
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cargo update
|
||||||
|
cargo fmt
|
||||||
|
cargo check
|
||||||
|
cargo clippy
|
||||||
|
cargo outdated -R
|
56
server/config/defaults.hjson
vendored
Normal file
56
server/config/defaults.hjson
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
# settings related to the postgresql database
|
||||||
|
database: {
|
||||||
|
# username to connect to postgres
|
||||||
|
user: "lemmy"
|
||||||
|
# password to connect to postgres
|
||||||
|
password: "password"
|
||||||
|
# host where postgres is running
|
||||||
|
host: "localhost"
|
||||||
|
# port where postgres can be accessed
|
||||||
|
port: 5432
|
||||||
|
# name of the postgres database for lemmy
|
||||||
|
database: "lemmy"
|
||||||
|
# maximum number of active sql connections
|
||||||
|
pool_size: 5
|
||||||
|
}
|
||||||
|
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||||
|
hostname: "rrr"
|
||||||
|
# address where lemmy should listen for incoming requests
|
||||||
|
bind: "0.0.0.0"
|
||||||
|
# port where lemmy should listen for incoming requests
|
||||||
|
port: 8536
|
||||||
|
# json web token for authorization between server and client
|
||||||
|
jwt_secret: "changeme"
|
||||||
|
# The dir for the front end
|
||||||
|
front_end_dir: "../ui/dist"
|
||||||
|
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
|
||||||
|
# cause problems like remote instances fetching and permanently storing bad data.
|
||||||
|
federation_enabled: false
|
||||||
|
# rate limits for various user actions, by user ip
|
||||||
|
rate_limit: {
|
||||||
|
# maximum number of messages created in interval
|
||||||
|
message: 30
|
||||||
|
# interval length for message limit
|
||||||
|
message_per_second: 60
|
||||||
|
# maximum number of posts created in interval
|
||||||
|
post: 6
|
||||||
|
# interval length for post limit
|
||||||
|
post_per_second: 600
|
||||||
|
# maximum number of registrations in interval
|
||||||
|
register: 3
|
||||||
|
# interval length for registration limit
|
||||||
|
register_per_second: 3600
|
||||||
|
}
|
||||||
|
# # email sending configuration
|
||||||
|
# email: {
|
||||||
|
# # hostname of the smtp server
|
||||||
|
# smtp_server: ""
|
||||||
|
# # login name for smtp server
|
||||||
|
# smtp_login: ""
|
||||||
|
# # password to login to the smtp server
|
||||||
|
# smtp_password: ""
|
||||||
|
# # address to send emails from, eg "info@your-instance.com"
|
||||||
|
# smtp_from_address: ""
|
||||||
|
# }
|
||||||
|
}
|
224
server/migrations/2019-12-29-164820_add_avatar/down.sql
vendored
Normal file
224
server/migrations/2019-12-29-164820_add_avatar/down.sql
vendored
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
-- the views
|
||||||
|
drop view user_mention_view;
|
||||||
|
drop view reply_view;
|
||||||
|
drop view comment_view;
|
||||||
|
drop view user_view;
|
||||||
|
|
||||||
|
-- user
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
||||||
|
|
||||||
|
-- post
|
||||||
|
-- Recreate the view
|
||||||
|
drop view post_view;
|
||||||
|
create view post_view as
|
||||||
|
with all_post as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
p.*,
|
||||||
|
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||||
|
(select name from community where p.community_id = community.id) as community_name,
|
||||||
|
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||||
|
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||||
|
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||||
|
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||||
|
coalesce(sum(pl.score), 0) as score,
|
||||||
|
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||||
|
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||||
|
from post p
|
||||||
|
left join post_like pl on p.id = pl.post_id
|
||||||
|
group by p.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(pl.score, 0) as my_vote,
|
||||||
|
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||||
|
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||||
|
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_post ap
|
||||||
|
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as subscribed,
|
||||||
|
null as read,
|
||||||
|
null as saved
|
||||||
|
from all_post ap
|
||||||
|
;
|
||||||
|
|
||||||
|
-- community
|
||||||
|
|
||||||
|
drop view community_view;
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
||||||
|
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
||||||
|
|
||||||
|
-- Reply and comment view
|
||||||
|
create view comment_view as
|
||||||
|
with all_comment as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
c.*,
|
||||||
|
(select community_id from post p where p.id = c.post_id),
|
||||||
|
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||||
|
coalesce(sum(cl.score), 0) as score,
|
||||||
|
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||||
|
from comment c
|
||||||
|
left join comment_like cl on c.id = cl.comment_id
|
||||||
|
group by c.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(cl.score, 0) as my_vote,
|
||||||
|
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_comment ac
|
||||||
|
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as saved
|
||||||
|
from all_comment ac
|
||||||
|
;
|
||||||
|
|
||||||
|
create view reply_view as
|
||||||
|
with closereply as (
|
||||||
|
select
|
||||||
|
c2.id,
|
||||||
|
c2.creator_id as sender_id,
|
||||||
|
c.creator_id as recipient_id
|
||||||
|
from comment c
|
||||||
|
inner join comment c2 on c.id = c2.parent_id
|
||||||
|
where c2.creator_id != c.creator_id
|
||||||
|
-- Do union where post is null
|
||||||
|
union
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
c.creator_id as sender_id,
|
||||||
|
p.creator_id as recipient_id
|
||||||
|
from comment c, post p
|
||||||
|
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||||
|
)
|
||||||
|
select cv.*,
|
||||||
|
closereply.recipient_id
|
||||||
|
from comment_view cv, closereply
|
||||||
|
where closereply.id = cv.id
|
||||||
|
;
|
||||||
|
|
||||||
|
-- user mention
|
||||||
|
create view user_mention_view as
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
um.id as user_mention_id,
|
||||||
|
c.creator_id,
|
||||||
|
c.post_id,
|
||||||
|
c.parent_id,
|
||||||
|
c.content,
|
||||||
|
c.removed,
|
||||||
|
um.read,
|
||||||
|
c.published,
|
||||||
|
c.updated,
|
||||||
|
c.deleted,
|
||||||
|
c.community_id,
|
||||||
|
c.banned,
|
||||||
|
c.banned_from_community,
|
||||||
|
c.creator_name,
|
||||||
|
c.score,
|
||||||
|
c.upvotes,
|
||||||
|
c.downvotes,
|
||||||
|
c.user_id,
|
||||||
|
c.my_vote,
|
||||||
|
c.saved,
|
||||||
|
um.recipient_id
|
||||||
|
from user_mention um, comment_view c
|
||||||
|
where um.comment_id = c.id;
|
||||||
|
|
||||||
|
-- community tables
|
||||||
|
drop view community_moderator_view;
|
||||||
|
drop view community_follower_view;
|
||||||
|
drop view community_user_ban_view;
|
||||||
|
drop view site_view;
|
||||||
|
|
||||||
|
create view community_moderator_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||||
|
(select name from community c where cm.community_id = c.id) as community_name
|
||||||
|
from community_moderator cm;
|
||||||
|
|
||||||
|
create view community_follower_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cf.user_id = u.id) as user_name,
|
||||||
|
(select name from community c where cf.community_id = c.id) as community_name
|
||||||
|
from community_follower cf;
|
||||||
|
|
||||||
|
create view community_user_ban_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||||
|
(select name from community c where cm.community_id = c.id) as community_name
|
||||||
|
from community_user_ban cm;
|
||||||
|
|
||||||
|
create view site_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where s.creator_id = u.id) as creator_name,
|
||||||
|
(select count(*) from user_) as number_of_users,
|
||||||
|
(select count(*) from post) as number_of_posts,
|
||||||
|
(select count(*) from comment) as number_of_comments,
|
||||||
|
(select count(*) from community) as number_of_communities
|
||||||
|
from site s;
|
||||||
|
|
||||||
|
alter table user_ rename column avatar to icon;
|
||||||
|
alter table user_ alter column icon type bytea using icon::bytea;
|
234
server/migrations/2019-12-29-164820_add_avatar/up.sql
vendored
Normal file
234
server/migrations/2019-12-29-164820_add_avatar/up.sql
vendored
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
-- Rename to avatar
|
||||||
|
alter table user_ rename column icon to avatar;
|
||||||
|
alter table user_ alter column avatar type text;
|
||||||
|
|
||||||
|
-- Rebuild nearly all the views, to include the creator avatars
|
||||||
|
|
||||||
|
-- user
|
||||||
|
drop view user_view;
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
||||||
|
|
||||||
|
-- post
|
||||||
|
-- Recreate the view
|
||||||
|
drop view post_view;
|
||||||
|
create view post_view as
|
||||||
|
with all_post as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
p.*,
|
||||||
|
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||||
|
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
||||||
|
(select name from community where p.community_id = community.id) as community_name,
|
||||||
|
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||||
|
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||||
|
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||||
|
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||||
|
coalesce(sum(pl.score), 0) as score,
|
||||||
|
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||||
|
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||||
|
from post p
|
||||||
|
left join post_like pl on p.id = pl.post_id
|
||||||
|
group by p.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(pl.score, 0) as my_vote,
|
||||||
|
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||||
|
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||||
|
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_post ap
|
||||||
|
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ap.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as subscribed,
|
||||||
|
null as read,
|
||||||
|
null as saved
|
||||||
|
from all_post ap
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
-- community
|
||||||
|
drop view community_view;
|
||||||
|
create view community_view as
|
||||||
|
with all_community as
|
||||||
|
(
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||||
|
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
|
||||||
|
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||||
|
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||||
|
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||||
|
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
||||||
|
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
||||||
|
from community c
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||||
|
from user_ u
|
||||||
|
cross join all_community ac
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as subscribed
|
||||||
|
from all_community ac
|
||||||
|
;
|
||||||
|
|
||||||
|
-- reply and comment view
|
||||||
|
drop view reply_view;
|
||||||
|
drop view user_mention_view;
|
||||||
|
drop view comment_view;
|
||||||
|
create view comment_view as
|
||||||
|
with all_comment as
|
||||||
|
(
|
||||||
|
select
|
||||||
|
c.*,
|
||||||
|
(select community_id from post p where p.id = c.post_id),
|
||||||
|
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||||
|
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||||
|
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||||
|
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
|
||||||
|
coalesce(sum(cl.score), 0) as score,
|
||||||
|
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||||
|
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||||
|
from comment c
|
||||||
|
left join comment_like cl on c.id = cl.comment_id
|
||||||
|
group by c.id
|
||||||
|
)
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
u.id as user_id,
|
||||||
|
coalesce(cl.score, 0) as my_vote,
|
||||||
|
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||||
|
from user_ u
|
||||||
|
cross join all_comment ac
|
||||||
|
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||||
|
|
||||||
|
union all
|
||||||
|
|
||||||
|
select
|
||||||
|
ac.*,
|
||||||
|
null as user_id,
|
||||||
|
null as my_vote,
|
||||||
|
null as saved
|
||||||
|
from all_comment ac
|
||||||
|
;
|
||||||
|
|
||||||
|
create view reply_view as
|
||||||
|
with closereply as (
|
||||||
|
select
|
||||||
|
c2.id,
|
||||||
|
c2.creator_id as sender_id,
|
||||||
|
c.creator_id as recipient_id
|
||||||
|
from comment c
|
||||||
|
inner join comment c2 on c.id = c2.parent_id
|
||||||
|
where c2.creator_id != c.creator_id
|
||||||
|
-- Do union where post is null
|
||||||
|
union
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
c.creator_id as sender_id,
|
||||||
|
p.creator_id as recipient_id
|
||||||
|
from comment c, post p
|
||||||
|
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||||
|
)
|
||||||
|
select cv.*,
|
||||||
|
closereply.recipient_id
|
||||||
|
from comment_view cv, closereply
|
||||||
|
where closereply.id = cv.id
|
||||||
|
;
|
||||||
|
|
||||||
|
-- user mention
|
||||||
|
create view user_mention_view as
|
||||||
|
select
|
||||||
|
c.id,
|
||||||
|
um.id as user_mention_id,
|
||||||
|
c.creator_id,
|
||||||
|
c.post_id,
|
||||||
|
c.parent_id,
|
||||||
|
c.content,
|
||||||
|
c.removed,
|
||||||
|
um.read,
|
||||||
|
c.published,
|
||||||
|
c.updated,
|
||||||
|
c.deleted,
|
||||||
|
c.community_id,
|
||||||
|
c.banned,
|
||||||
|
c.banned_from_community,
|
||||||
|
c.creator_name,
|
||||||
|
c.creator_avatar,
|
||||||
|
c.score,
|
||||||
|
c.upvotes,
|
||||||
|
c.downvotes,
|
||||||
|
c.user_id,
|
||||||
|
c.my_vote,
|
||||||
|
c.saved,
|
||||||
|
um.recipient_id
|
||||||
|
from user_mention um, comment_view c
|
||||||
|
where um.comment_id = c.id;
|
||||||
|
|
||||||
|
-- community views
|
||||||
|
drop view community_moderator_view;
|
||||||
|
drop view community_follower_view;
|
||||||
|
drop view community_user_ban_view;
|
||||||
|
drop view site_view;
|
||||||
|
|
||||||
|
create view community_moderator_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||||
|
(select avatar from user_ u where cm.user_id = u.id),
|
||||||
|
(select name from community c where cm.community_id = c.id) as community_name
|
||||||
|
from community_moderator cm;
|
||||||
|
|
||||||
|
create view community_follower_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cf.user_id = u.id) as user_name,
|
||||||
|
(select avatar from user_ u where cf.user_id = u.id),
|
||||||
|
(select name from community c where cf.community_id = c.id) as community_name
|
||||||
|
from community_follower cf;
|
||||||
|
|
||||||
|
create view community_user_ban_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||||
|
(select avatar from user_ u where cm.user_id = u.id),
|
||||||
|
(select name from community c where cm.community_id = c.id) as community_name
|
||||||
|
from community_user_ban cm;
|
||||||
|
|
||||||
|
create view site_view as
|
||||||
|
select *,
|
||||||
|
(select name from user_ u where s.creator_id = u.id) as creator_name,
|
||||||
|
(select avatar from user_ u where s.creator_id = u.id) as creator_avatar,
|
||||||
|
(select count(*) from user_) as number_of_users,
|
||||||
|
(select count(*) from post) as number_of_posts,
|
||||||
|
(select count(*) from comment) as number_of_comments,
|
||||||
|
(select count(*) from community) as number_of_communities
|
||||||
|
from site s;
|
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
-- user
|
||||||
|
drop view user_view;
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
-- user
|
||||||
|
drop view user_view;
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
email,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
20
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/down.sql
vendored
Normal file
20
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/down.sql
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
-- Drop the columns
|
||||||
|
drop view user_view;
|
||||||
|
alter table user_ drop column show_avatars;
|
||||||
|
alter table user_ drop column send_notifications_to_email;
|
||||||
|
|
||||||
|
-- Rebuild the view
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
email,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
22
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/up.sql
vendored
Normal file
22
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/up.sql
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
-- Add columns
|
||||||
|
alter table user_ add column show_avatars boolean default true not null;
|
||||||
|
alter table user_ add column send_notifications_to_email boolean default false not null;
|
||||||
|
|
||||||
|
-- Rebuild the user_view
|
||||||
|
drop view user_view;
|
||||||
|
create view user_view as
|
||||||
|
select id,
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
email,
|
||||||
|
fedi_name,
|
||||||
|
admin,
|
||||||
|
banned,
|
||||||
|
show_avatars,
|
||||||
|
send_notifications_to_email,
|
||||||
|
published,
|
||||||
|
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||||
|
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||||
|
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||||
|
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||||
|
from user_ u;
|
|
@ -1,4 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::send_email;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateComment {
|
pub struct CreateComment {
|
||||||
|
@ -51,20 +53,22 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
@ -82,24 +86,20 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||||
|
|
||||||
if mention_user.is_ok() {
|
|
||||||
let mention_user_id = mention_user?.id;
|
|
||||||
|
|
||||||
// You can't mention yourself
|
// You can't mention yourself
|
||||||
// At some point, make it so you can't tag the parent creator either
|
// At some point, make it so you can't tag the parent creator either
|
||||||
// This can cause two notifications, one for reply and the other for mention
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
if mention_user_id != user_id {
|
if mention_user.id != user_id {
|
||||||
let user_mention_form = UserMentionForm {
|
let user_mention_form = UserMentionForm {
|
||||||
recipient_id: mention_user_id,
|
recipient_id: mention_user.id,
|
||||||
comment_id: inserted_comment.id,
|
comment_id: inserted_comment.id,
|
||||||
read: None,
|
read: None,
|
||||||
};
|
};
|
||||||
|
@ -109,10 +109,79 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
match UserMention::create(&conn, &user_mention_form) {
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
Ok(_mention) => (),
|
Ok(_mention) => (),
|
||||||
Err(_e) => eprintln!("{}", &_e),
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send an email to those users that have notifications on
|
||||||
|
if mention_user.send_notifications_to_email {
|
||||||
|
if let Some(mention_email) = mention_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Mentioned by {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &mention_email, &mention_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifs to the parent commenter / poster
|
||||||
|
match data.parent_id {
|
||||||
|
Some(parent_id) => {
|
||||||
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
|
if parent_comment.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(comment_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Its a post
|
||||||
|
None => {
|
||||||
|
if post.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, post.creator_id)?;
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(post_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -124,7 +193,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||||
|
@ -143,7 +212,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -163,17 +232,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_comment_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,14 +265,14 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
|
||||||
|
|
||||||
if mention_user.is_ok() {
|
if mention_user.is_ok() {
|
||||||
let mention_user_id = mention_user?.id;
|
let mention_user_id = mention_user?.id;
|
||||||
|
@ -255,7 +324,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -268,12 +337,12 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match CommentSaved::save(&conn, &comment_saved_form) {
|
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +362,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -301,20 +370,20 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err(&self.op, "downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -328,11 +397,11 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
CommentLike::remove(&conn, &like_form)?;
|
CommentLike::remove(&conn, &like_form)?;
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,21 +136,24 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
let community_id = match data.id {
|
let community_id = match data.id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
match Community::read_from_name(&conn, data.name.to_owned().unwrap_or("main".to_string())) {
|
match Community::read_from_name(
|
||||||
|
&conn,
|
||||||
|
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
|
||||||
|
) {
|
||||||
Ok(community) => community.id,
|
Ok(community) => community.id,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
|
@ -176,21 +179,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| has_slurs(&data.title)
|
|| has_slurs(&data.title)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When you create a community, make sure the user becomes a moderator and a follower
|
// When you create a community, make sure the user becomes a moderator and a follower
|
||||||
|
@ -208,7 +211,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
|
|
||||||
let inserted_community = match Community::create(&conn, &community_form) {
|
let inserted_community = match Community::create(&conn, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let community_moderator_form = CommunityModeratorForm {
|
||||||
|
@ -220,10 +223,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
||||||
|
@ -252,21 +252,21 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
let data: &EditCommunity = &self.data;
|
let data: &EditCommunity = &self.data;
|
||||||
|
|
||||||
if has_slurs(&data.name) || has_slurs(&data.title) {
|
if has_slurs(&data.name) || has_slurs(&data.title) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify its a mod
|
// Verify its a mod
|
||||||
|
@ -279,7 +279,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
);
|
);
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_community_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_community_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
|
@ -296,7 +296,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
|
|
||||||
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -351,7 +351,7 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||||
|
|
||||||
let communities = CommunityQueryBuilder::create(&conn)
|
let communities = CommunityQueryBuilder::create(&conn)
|
||||||
.sort(&sort)
|
.sort(&sort)
|
||||||
.from_user_id(user_id)
|
.for_user(user_id)
|
||||||
.show_nsfw(show_nsfw)
|
.show_nsfw(show_nsfw)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
|
@ -372,7 +372,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -385,12 +385,12 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
if data.follow {
|
if data.follow {
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -418,7 +418,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
let communities: Vec<CommunityFollowerView> =
|
let communities: Vec<CommunityFollowerView> =
|
||||||
match CommunityFollowerView::for_user(&conn, user_id) {
|
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||||
Ok(communities) => communities,
|
Ok(communities) => communities,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "system_err_login"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -436,7 +436,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -449,12 +449,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
if data.ban {
|
if data.ban {
|
||||||
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,7 +491,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -505,20 +505,14 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -548,7 +542,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -562,14 +556,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
// Make sure user is the creator, or an admin
|
// Make sure user is the creator, or an admin
|
||||||
if user_id != read_community.creator_id
|
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
|
||||||
&& !admins
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
.iter()
|
|
||||||
.map(|a| a.id)
|
|
||||||
.collect::<Vec<i32>>()
|
|
||||||
.contains(&user_id)
|
|
||||||
{
|
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
|
@ -586,7 +574,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// You also have to re-do the community_moderator table, reordering it.
|
// You also have to re-do the community_moderator table, reordering it.
|
||||||
|
@ -610,10 +598,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -629,12 +614,12 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::db::user_mention::*;
|
||||||
use crate::db::user_mention_view::*;
|
use crate::db::user_mention_view::*;
|
||||||
use crate::db::user_view::*;
|
use crate::db::user_view::*;
|
||||||
use crate::db::*;
|
use crate::db::*;
|
||||||
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
|
@ -93,23 +93,23 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
|
@ -128,7 +128,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let inserted_post = match Post::create(&conn, &post_form) {
|
let inserted_post = match Post::create(&conn, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// They like their own post by default
|
// They like their own post by default
|
||||||
|
@ -141,13 +141,13 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostResponse {
|
Ok(PostResponse {
|
||||||
|
@ -175,7 +175,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||||
|
|
||||||
let post_view = match PostView::read(&conn, data.id, user_id) {
|
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comments = CommentQueryBuilder::create(&conn)
|
let comments = CommentQueryBuilder::create(&conn)
|
||||||
|
@ -243,7 +243,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||||
.list()
|
.list()
|
||||||
{
|
{
|
||||||
Ok(posts) => posts,
|
Ok(posts) => posts,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(GetPostsResponse {
|
Ok(GetPostsResponse {
|
||||||
|
@ -260,7 +260,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -268,20 +268,20 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err(&self.op, "downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = PostLikeForm {
|
let like_form = PostLikeForm {
|
||||||
|
@ -294,17 +294,17 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
PostLike::remove(&conn, &like_form)?;
|
PostLike::remove(&conn, &like_form)?;
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// just output the score
|
// just output the score
|
||||||
|
@ -319,14 +319,14 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
fn perform(&self) -> Result<PostResponse, Error> {
|
fn perform(&self) -> Result<PostResponse, Error> {
|
||||||
let data: &EditPost = &self.data;
|
let data: &EditPost = &self.data;
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -341,17 +341,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
);
|
);
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_post_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_post_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
|
@ -370,7 +370,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -418,7 +418,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -431,12 +431,12 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match PostSaved::save(&conn, &post_saved_form) {
|
match PostSaved::save(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match PostSaved::unsave(&conn, &post_saved_form) {
|
match PostSaved::unsave(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,16 +160,15 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// These arrays are only for the full modlog, when a community isn't given
|
// These arrays are only for the full modlog, when a community isn't given
|
||||||
let mut removed_communities = Vec::new();
|
let (removed_communities, banned, added) = if data.community_id.is_none() {
|
||||||
let mut banned = Vec::new();
|
(
|
||||||
let mut added = Vec::new();
|
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
|
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
if data.community_id.is_none() {
|
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
removed_communities =
|
)
|
||||||
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
} else {
|
||||||
banned = ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
added = ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetModlogResponse {
|
Ok(GetModlogResponse {
|
||||||
|
@ -194,20 +193,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if !UserView::read(&conn, user_id)?.admin {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -222,7 +221,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
|
|
||||||
match Site::create(&conn, &site_form) {
|
match Site::create(&conn, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
@ -241,20 +240,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let found_site = Site::read(&conn, 1)?;
|
let found_site = Site::read(&conn, 1)?;
|
||||||
|
@ -271,7 +270,7 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
@ -426,7 +425,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -435,7 +434,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
// Make sure user is the creator
|
// Make sure user is the creator
|
||||||
if read_site.creator_id != user_id {
|
if read_site.creator_id != user_id {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -450,7 +449,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
use crate::{generate_random_string, send_email};
|
use crate::{generate_random_string, send_email};
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -26,6 +27,13 @@ pub struct SaveUserSettings {
|
||||||
default_sort_type: i16,
|
default_sort_type: i16,
|
||||||
default_listing_type: i16,
|
default_listing_type: i16,
|
||||||
lang: String,
|
lang: String,
|
||||||
|
avatar: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
new_password: Option<String>,
|
||||||
|
new_password_verify: Option<String>,
|
||||||
|
old_password: Option<String>,
|
||||||
|
show_avatars: bool,
|
||||||
|
send_notifications_to_email: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,18 +174,13 @@ impl Perform<LoginResponse> for Oper<Login> {
|
||||||
// Fetch that username / email
|
// Fetch that username / email
|
||||||
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -196,29 +199,30 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
// Make sure site has open registration
|
// Make sure site has open registration
|
||||||
if let Ok(site) = SiteView::read(&conn) {
|
if let Ok(site) = SiteView::read(&conn) {
|
||||||
if !site.open_registration {
|
if !site.open_registration {
|
||||||
return Err(APIError::err(&self.op, "registration_closed"))?;
|
return Err(APIError::err(&self.op, "registration_closed").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_slurs(&data.username) {
|
if has_slurs(&data.username) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are no admins
|
// Make sure there are no admins
|
||||||
if data.admin && UserView::admins(&conn)?.len() > 0 {
|
if data.admin && !UserView::admins(&conn)?.is_empty() {
|
||||||
return Err(APIError::err(&self.op, "admin_already_created"))?;
|
return Err(APIError::err(&self.op, "admin_already_created").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the new user
|
// Register the new user
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: data.username.to_owned(),
|
name: data.username.to_owned(),
|
||||||
fedi_name: Settings::get().hostname.into(),
|
fedi_name: Settings::get().hostname.to_owned(),
|
||||||
email: data.email.to_owned(),
|
email: data.email.to_owned(),
|
||||||
|
avatar: None,
|
||||||
password_encrypted: data.password.to_owned(),
|
password_encrypted: data.password.to_owned(),
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -229,12 +233,14 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the user
|
// Create the user
|
||||||
let inserted_user = match User_::register(&conn, &user_form) {
|
let inserted_user = match User_::register(&conn, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the main community if it doesn't exist
|
// Create the main community if it doesn't exist
|
||||||
|
@ -265,7 +271,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If its an admin, add them as a mod and follower to main
|
// If its an admin, add them as a mod and follower to main
|
||||||
|
@ -279,10 +285,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -302,18 +305,52 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
let read_user = User_::read(&conn, user_id)?;
|
||||||
|
|
||||||
|
let email = match &data.email {
|
||||||
|
Some(email) => Some(email.to_owned()),
|
||||||
|
None => read_user.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
let password_encrypted = match &data.new_password {
|
||||||
|
Some(new_password) => {
|
||||||
|
match &data.new_password_verify {
|
||||||
|
Some(new_password_verify) => {
|
||||||
|
// Make sure passwords match
|
||||||
|
if new_password != new_password_verify {
|
||||||
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the old password
|
||||||
|
match &data.old_password {
|
||||||
|
Some(old_password) => {
|
||||||
|
let valid: bool =
|
||||||
|
verify(old_password, &read_user.password_encrypted).unwrap_or(false);
|
||||||
|
if !valid {
|
||||||
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
|
}
|
||||||
|
User_::update_password(&conn, user_id, &new_password)?.password_encrypted
|
||||||
|
}
|
||||||
|
None => return Err(APIError::err(&self.op, "password_incorrect").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(APIError::err(&self.op, "passwords_dont_match").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => read_user.password_encrypted,
|
||||||
|
};
|
||||||
|
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email,
|
||||||
password_encrypted: read_user.password_encrypted,
|
avatar: data.avatar.to_owned(),
|
||||||
|
password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
admin: read_user.admin,
|
admin: read_user.admin,
|
||||||
|
@ -323,11 +360,13 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
default_sort_type: data.default_sort_type,
|
default_sort_type: data.default_sort_type,
|
||||||
default_listing_type: data.default_listing_type,
|
default_listing_type: data.default_listing_type,
|
||||||
lang: data.lang.to_owned(),
|
lang: data.lang.to_owned(),
|
||||||
|
show_avatars: data.show_avatars,
|
||||||
|
send_notifications_to_email: data.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -366,11 +405,18 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
let user_details_id = match data.user_id {
|
let user_details_id = match data.user_id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
User_::read_from_name(
|
match User_::read_from_name(
|
||||||
&conn,
|
&conn,
|
||||||
data.username.to_owned().unwrap_or("admin".to_string()),
|
data
|
||||||
)?
|
.username
|
||||||
.id
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| "admin".to_string()),
|
||||||
|
) {
|
||||||
|
Ok(user) => user.id,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -430,14 +476,14 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
@ -446,6 +492,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email: read_user.email,
|
||||||
|
avatar: read_user.avatar,
|
||||||
password_encrypted: read_user.password_encrypted,
|
password_encrypted: read_user.password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
|
@ -456,11 +503,13 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
default_sort_type: read_user.default_sort_type,
|
default_sort_type: read_user.default_sort_type,
|
||||||
default_listing_type: read_user.default_listing_type,
|
default_listing_type: read_user.default_listing_type,
|
||||||
lang: read_user.lang,
|
lang: read_user.lang,
|
||||||
|
show_avatars: read_user.show_avatars,
|
||||||
|
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -492,14 +541,14 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
@ -508,6 +557,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email: read_user.email,
|
||||||
|
avatar: read_user.avatar,
|
||||||
password_encrypted: read_user.password_encrypted,
|
password_encrypted: read_user.password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
|
@ -518,11 +568,13 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
default_sort_type: read_user.default_sort_type,
|
default_sort_type: read_user.default_sort_type,
|
||||||
default_listing_type: read_user.default_listing_type,
|
default_listing_type: read_user.default_listing_type,
|
||||||
lang: read_user.lang,
|
lang: read_user.lang,
|
||||||
|
show_avatars: read_user.show_avatars,
|
||||||
|
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -558,7 +610,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -586,7 +638,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -614,7 +666,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -630,7 +682,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
let _updated_user_mention =
|
let _updated_user_mention =
|
||||||
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||||
|
@ -649,7 +701,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -674,7 +726,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,7 +747,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
let _updated_mention =
|
let _updated_mention =
|
||||||
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||||
Ok(mention) => mention,
|
Ok(mention) => mention,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,7 +765,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -723,7 +775,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
|
@ -746,7 +798,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,7 +826,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -793,12 +845,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||||
// Fetch that email
|
// Fetch that email
|
||||||
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a random token
|
// Generate a random token
|
||||||
|
@ -815,7 +862,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||||
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
||||||
match send_email(subject, user_email, &user.name, html) {
|
match send_email(subject, user_email, &user.name, html) {
|
||||||
Ok(_o) => _o,
|
Ok(_o) => _o,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?,
|
Err(_e) => return Err(APIError::err(&self.op, &_e).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PasswordResetResponse {
|
Ok(PasswordResetResponse {
|
||||||
|
@ -833,33 +880,14 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
|
||||||
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the user
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
|
||||||
|
|
||||||
// Update the user with the new password
|
// Update the user with the new password
|
||||||
let user_form = UserForm {
|
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
|
||||||
name: read_user.name,
|
|
||||||
fedi_name: read_user.fedi_name,
|
|
||||||
email: read_user.email,
|
|
||||||
password_encrypted: data.password.to_owned(),
|
|
||||||
preferred_username: read_user.preferred_username,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
admin: read_user.admin,
|
|
||||||
banned: read_user.banned,
|
|
||||||
show_nsfw: read_user.show_nsfw,
|
|
||||||
theme: read_user.theme,
|
|
||||||
default_sort_type: read_user.default_sort_type,
|
|
||||||
default_listing_type: read_user.default_listing_type,
|
|
||||||
lang: read_user.lang,
|
|
||||||
};
|
|
||||||
|
|
||||||
let updated_user = match User_::update_password(&conn, user_id, &user_form) {
|
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
extern crate activitypub;
|
|
||||||
use self::activitypub::{actor::Person, context};
|
|
||||||
use crate::db::user::User_;
|
|
||||||
|
|
||||||
impl User_ {
|
|
||||||
pub fn person(&self) -> Person {
|
|
||||||
use crate::{to_datetime_utc, Settings};
|
|
||||||
let base_url = &format!("{}/user/{}", Settings::get().api_endpoint(), self.name);
|
|
||||||
let mut person = Person::default();
|
|
||||||
person.object_props.set_context_object(context()).ok();
|
|
||||||
person.object_props.set_id_string(base_url.to_string()).ok();
|
|
||||||
person
|
|
||||||
.object_props
|
|
||||||
.set_name_string(self.name.to_owned())
|
|
||||||
.ok();
|
|
||||||
person
|
|
||||||
.object_props
|
|
||||||
.set_published_utctime(to_datetime_utc(self.published))
|
|
||||||
.ok();
|
|
||||||
if let Some(i) = self.updated {
|
|
||||||
person
|
|
||||||
.object_props
|
|
||||||
.set_updated_utctime(to_datetime_utc(i))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
// person.object_props.summary = self.summary;
|
|
||||||
|
|
||||||
person
|
|
||||||
.ap_actor_props
|
|
||||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
|
||||||
.ok();
|
|
||||||
person
|
|
||||||
.ap_actor_props
|
|
||||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
|
||||||
.ok();
|
|
||||||
person
|
|
||||||
.ap_actor_props
|
|
||||||
.set_following_string(format!("{}/following", &base_url))
|
|
||||||
.ok();
|
|
||||||
person
|
|
||||||
.ap_actor_props
|
|
||||||
.set_liked_string(format!("{}/liked", &base_url))
|
|
||||||
.ok();
|
|
||||||
if let Some(i) = &self.preferred_username {
|
|
||||||
person
|
|
||||||
.ap_actor_props
|
|
||||||
.set_preferred_username_string(i.to_string())
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
person
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::User_;
|
|
||||||
use crate::db::{ListingType, SortType};
|
|
||||||
use crate::naive_now;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_person() {
|
|
||||||
let expected_user = User_ {
|
|
||||||
id: 52,
|
|
||||||
name: "thom".into(),
|
|
||||||
fedi_name: "rrf".into(),
|
|
||||||
preferred_username: None,
|
|
||||||
password_encrypted: "here".into(),
|
|
||||||
email: None,
|
|
||||||
icon: None,
|
|
||||||
published: naive_now(),
|
|
||||||
admin: false,
|
|
||||||
banned: false,
|
|
||||||
updated: None,
|
|
||||||
show_nsfw: false,
|
|
||||||
theme: "darkly".into(),
|
|
||||||
default_sort_type: SortType::Hot as i16,
|
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
|
||||||
lang: "browser".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let person = expected_user.person();
|
|
||||||
assert_eq!(
|
|
||||||
"rrr/api/v1/user/thom",
|
|
||||||
person.object_props.id_string().unwrap()
|
|
||||||
);
|
|
||||||
let json = serde_json::to_string_pretty(&person).unwrap();
|
|
||||||
println!("{}", json);
|
|
||||||
}
|
|
||||||
}
|
|
117
server/src/apub/community.rs
Normal file
117
server/src/apub/community.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use crate::apub::make_apub_endpoint;
|
||||||
|
use crate::constants::CACHE_INTERVAL_FEDERATION;
|
||||||
|
use crate::db::community::Community;
|
||||||
|
use crate::db::community_view::CommunityFollowerView;
|
||||||
|
use crate::db::establish_connection;
|
||||||
|
use crate::to_datetime_utc;
|
||||||
|
use activitypub::{actor::Group, collection::UnorderedCollection, context};
|
||||||
|
use actix_web::body::Body;
|
||||||
|
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||||
|
use actix_web::web::Path;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
impl Community {
|
||||||
|
pub fn as_group(&self) -> Group {
|
||||||
|
let base_url = make_apub_endpoint("c", &self.name);
|
||||||
|
|
||||||
|
let mut group = Group::default();
|
||||||
|
|
||||||
|
group.object_props.set_context_object(context()).ok();
|
||||||
|
group.object_props.set_id_string(base_url.to_string()).ok();
|
||||||
|
group
|
||||||
|
.object_props
|
||||||
|
.set_name_string(self.name.to_owned())
|
||||||
|
.ok();
|
||||||
|
group
|
||||||
|
.object_props
|
||||||
|
.set_published_utctime(to_datetime_utc(self.published))
|
||||||
|
.ok();
|
||||||
|
if let Some(updated) = self.updated {
|
||||||
|
group
|
||||||
|
.object_props
|
||||||
|
.set_updated_utctime(to_datetime_utc(updated))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &self.description {
|
||||||
|
group
|
||||||
|
.object_props
|
||||||
|
.set_summary_string(description.to_string())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
group
|
||||||
|
.ap_actor_props
|
||||||
|
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||||
|
.ok();
|
||||||
|
group
|
||||||
|
.ap_actor_props
|
||||||
|
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||||
|
.ok();
|
||||||
|
group
|
||||||
|
.ap_actor_props
|
||||||
|
.set_followers_string(format!("{}/followers", &base_url))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
group
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn followers_as_collection(&self) -> UnorderedCollection {
|
||||||
|
let base_url = make_apub_endpoint("c", &self.name);
|
||||||
|
|
||||||
|
let mut collection = UnorderedCollection::default();
|
||||||
|
collection.object_props.set_context_object(context()).ok();
|
||||||
|
collection.object_props.set_id_string(base_url).ok();
|
||||||
|
|
||||||
|
let connection = establish_connection();
|
||||||
|
//As we are an object, we validated that the community id was valid
|
||||||
|
let community_followers = CommunityFollowerView::for_community(&connection, self.id).unwrap();
|
||||||
|
|
||||||
|
let ap_followers = community_followers
|
||||||
|
.iter()
|
||||||
|
.map(|follower| make_apub_endpoint("u", &follower.user_name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
collection
|
||||||
|
.collection_props
|
||||||
|
.set_items_string_vec(ap_followers)
|
||||||
|
.unwrap();
|
||||||
|
collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CommunityQuery {
|
||||||
|
community_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_apub_community(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||||
|
let connection = establish_connection();
|
||||||
|
|
||||||
|
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||||
|
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||||
|
)]))
|
||||||
|
.body(serde_json::to_string(&community.as_group()).unwrap())
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||||
|
let connection = establish_connection();
|
||||||
|
|
||||||
|
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||||
|
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||||
|
)]))
|
||||||
|
.body(serde_json::to_string(&community.followers_as_collection()).unwrap())
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
||||||
|
}
|
102
server/src/apub/mod.rs
Normal file
102
server/src/apub/mod.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
pub mod community;
|
||||||
|
pub mod post;
|
||||||
|
pub mod user;
|
||||||
|
use crate::Settings;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::db::community::Community;
|
||||||
|
use crate::db::post::Post;
|
||||||
|
use crate::db::user::User_;
|
||||||
|
use crate::db::{ListingType, SortType};
|
||||||
|
use crate::{naive_now, Settings};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_person() {
|
||||||
|
let user = User_ {
|
||||||
|
id: 52,
|
||||||
|
name: "thom".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "here".into(),
|
||||||
|
email: None,
|
||||||
|
avatar: None,
|
||||||
|
published: naive_now(),
|
||||||
|
admin: false,
|
||||||
|
banned: false,
|
||||||
|
updated: None,
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "darkly".into(),
|
||||||
|
default_sort_type: SortType::Hot as i16,
|
||||||
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let person = user.as_person();
|
||||||
|
assert_eq!(
|
||||||
|
format!("https://{}/federation/u/thom", Settings::get().hostname),
|
||||||
|
person.object_props.id_string().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_community() {
|
||||||
|
let community = Community {
|
||||||
|
id: 42,
|
||||||
|
name: "Test".into(),
|
||||||
|
title: "Test Title".into(),
|
||||||
|
description: Some("Test community".into()),
|
||||||
|
category_id: 32,
|
||||||
|
creator_id: 52,
|
||||||
|
removed: false,
|
||||||
|
published: naive_now(),
|
||||||
|
updated: Some(naive_now()),
|
||||||
|
deleted: false,
|
||||||
|
nsfw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let group = community.as_group();
|
||||||
|
assert_eq!(
|
||||||
|
format!("https://{}/federation/c/Test", Settings::get().hostname),
|
||||||
|
group.object_props.id_string().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_post() {
|
||||||
|
let post = Post {
|
||||||
|
id: 62,
|
||||||
|
name: "A test post".into(),
|
||||||
|
url: None,
|
||||||
|
body: None,
|
||||||
|
creator_id: 52,
|
||||||
|
community_id: 42,
|
||||||
|
published: naive_now(),
|
||||||
|
removed: false,
|
||||||
|
locked: false,
|
||||||
|
stickied: false,
|
||||||
|
nsfw: false,
|
||||||
|
deleted: false,
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let page = post.as_page();
|
||||||
|
assert_eq!(
|
||||||
|
format!("https://{}/federation/post/62", Settings::get().hostname),
|
||||||
|
page.object_props.id_string().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_apub_endpoint<S: Display, T: Display>(point: S, value: T) -> String {
|
||||||
|
format!(
|
||||||
|
"https://{}/federation/{}/{}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
point,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
38
server/src/apub/post.rs
Normal file
38
server/src/apub/post.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::apub::make_apub_endpoint;
|
||||||
|
use crate::db::post::Post;
|
||||||
|
use crate::to_datetime_utc;
|
||||||
|
use activitypub::{context, object::Page};
|
||||||
|
|
||||||
|
impl Post {
|
||||||
|
pub fn as_page(&self) -> Page {
|
||||||
|
let base_url = make_apub_endpoint("post", self.id);
|
||||||
|
let mut page = Page::default();
|
||||||
|
|
||||||
|
page.object_props.set_context_object(context()).ok();
|
||||||
|
page.object_props.set_id_string(base_url).ok();
|
||||||
|
page.object_props.set_name_string(self.name.to_owned()).ok();
|
||||||
|
|
||||||
|
if let Some(body) = &self.body {
|
||||||
|
page.object_props.set_content_string(body.to_owned()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = &self.url {
|
||||||
|
page.object_props.set_url_string(url.to_owned()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
//page.object_props.set_attributed_to_string
|
||||||
|
|
||||||
|
page
|
||||||
|
.object_props
|
||||||
|
.set_published_utctime(to_datetime_utc(self.published))
|
||||||
|
.ok();
|
||||||
|
if let Some(updated) = self.updated {
|
||||||
|
page
|
||||||
|
.object_props
|
||||||
|
.set_updated_utctime(to_datetime_utc(updated))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
page
|
||||||
|
}
|
||||||
|
}
|
79
server/src/apub/user.rs
Normal file
79
server/src/apub/user.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::apub::make_apub_endpoint;
|
||||||
|
use crate::constants::CACHE_INTERVAL_FEDERATION;
|
||||||
|
use crate::db::establish_connection;
|
||||||
|
use crate::db::user::User_;
|
||||||
|
use crate::to_datetime_utc;
|
||||||
|
use activitypub::{actor::Person, context};
|
||||||
|
use actix_web::body::Body;
|
||||||
|
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||||
|
use actix_web::web::Path;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
impl User_ {
|
||||||
|
pub fn as_person(&self) -> Person {
|
||||||
|
let base_url = make_apub_endpoint("u", &self.name);
|
||||||
|
let mut person = Person::default();
|
||||||
|
person.object_props.set_context_object(context()).ok();
|
||||||
|
person.object_props.set_id_string(base_url.to_string()).ok();
|
||||||
|
person
|
||||||
|
.object_props
|
||||||
|
.set_name_string(self.name.to_owned())
|
||||||
|
.ok();
|
||||||
|
person
|
||||||
|
.object_props
|
||||||
|
.set_published_utctime(to_datetime_utc(self.published))
|
||||||
|
.ok();
|
||||||
|
if let Some(updated) = self.updated {
|
||||||
|
person
|
||||||
|
.object_props
|
||||||
|
.set_updated_utctime(to_datetime_utc(updated))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
person
|
||||||
|
.ap_actor_props
|
||||||
|
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||||
|
.ok();
|
||||||
|
person
|
||||||
|
.ap_actor_props
|
||||||
|
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||||
|
.ok();
|
||||||
|
person
|
||||||
|
.ap_actor_props
|
||||||
|
.set_following_string(format!("{}/following", &base_url))
|
||||||
|
.ok();
|
||||||
|
person
|
||||||
|
.ap_actor_props
|
||||||
|
.set_liked_string(format!("{}/liked", &base_url))
|
||||||
|
.ok();
|
||||||
|
if let Some(i) = &self.preferred_username {
|
||||||
|
person
|
||||||
|
.ap_actor_props
|
||||||
|
.set_preferred_username_string(i.to_string())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
person
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UserQuery {
|
||||||
|
user_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
|
||||||
|
let connection = establish_connection();
|
||||||
|
|
||||||
|
if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||||
|
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||||
|
)]))
|
||||||
|
.body(serde_json::to_string(&user.as_person()).unwrap())
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().finish()
|
||||||
|
}
|
||||||
|
}
|
6
server/src/constants.rs
Normal file
6
server/src/constants.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use chrono::Duration;
|
||||||
|
|
||||||
|
// TODO: should all be 0 during debug
|
||||||
|
pub static CACHE_INTERVAL_FEEDS: Duration = Duration::minutes(15);
|
||||||
|
pub static CACHE_INTERVAL_FEDERATION: Duration = Duration::minutes(1);
|
||||||
|
pub static CACHE_INTERVAL_FRONTEND: Duration = Duration::hours(2);
|
|
@ -174,6 +174,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -182,6 +183,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -18,6 +18,7 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -46,6 +47,7 @@ pub struct CommentView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -226,6 +228,7 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -255,6 +258,7 @@ pub struct ReplyView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -368,6 +372,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -376,6 +381,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -447,6 +454,7 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
@ -470,6 +478,7 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
|
|
@ -68,6 +68,10 @@ impl Community {
|
||||||
.filter(name.eq(community_name))
|
.filter(name.eq(community_name))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_url(&self) -> String {
|
||||||
|
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||||
|
@ -216,6 +220,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -224,6 +229,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
nsfw -> Bool,
|
nsfw -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
category_name -> Varchar,
|
category_name -> Varchar,
|
||||||
number_of_subscribers -> BigInt,
|
number_of_subscribers -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
|
@ -33,6 +34,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +46,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +58,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +80,7 @@ pub struct CommunityView {
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub category_name: String,
|
pub category_name: String,
|
||||||
pub number_of_subscribers: i64,
|
pub number_of_subscribers: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
|
@ -119,7 +124,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_user_id<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
pub fn for_user<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
||||||
self.from_user_id = from_user_id.get_optional();
|
self.from_user_id = from_user_id.get_optional();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -224,6 +229,7 @@ pub struct CommunityModeratorView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +259,7 @@ pub struct CommunityFollowerView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +289,7 @@ pub struct CommunityUserBanView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::Settings;
|
extern crate lazy_static;
|
||||||
|
use crate::settings::Settings;
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
|
use diesel::r2d2::*;
|
||||||
use diesel::result::Error;
|
use diesel::result::Error;
|
||||||
use diesel::*;
|
use diesel::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -99,19 +101,29 @@ pub trait MaybeOptional<T> {
|
||||||
|
|
||||||
impl<T> MaybeOptional<T> for T {
|
impl<T> MaybeOptional<T> for T {
|
||||||
fn get_optional(self) -> Option<T> {
|
fn get_optional(self) -> Option<T> {
|
||||||
return Some(self);
|
Some(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MaybeOptional<T> for Option<T> {
|
impl<T> MaybeOptional<T> for Option<T> {
|
||||||
fn get_optional(self) -> Option<T> {
|
fn get_optional(self) -> Option<T> {
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn establish_connection() -> PgConnection {
|
lazy_static! {
|
||||||
let db_url = Settings::get().db_url;
|
static ref PG_POOL: Pool<ConnectionManager<PgConnection>> = {
|
||||||
PgConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url))
|
let db_url = Settings::get().get_database_url();
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(&db_url);
|
||||||
|
Pool::builder()
|
||||||
|
.max_size(Settings::get().database.pool_size)
|
||||||
|
.build(manager)
|
||||||
|
.unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn establish_connection() -> PooledConnection<ConnectionManager<PgConnection>> {
|
||||||
|
PG_POOL.get().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
|
|
|
@ -442,6 +442,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -450,6 +451,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
||||||
|
@ -460,6 +463,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -468,6 +472,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::schema::password_reset_request;
|
use crate::schema::password_reset_request;
|
||||||
use crate::schema::password_reset_request::dsl::*;
|
use crate::schema::password_reset_request::dsl::*;
|
||||||
use crypto::digest::Digest;
|
use sha2::{Digest, Sha256};
|
||||||
use crypto::sha2::Sha256;
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
||||||
#[table_name = "password_reset_request"]
|
#[table_name = "password_reset_request"]
|
||||||
|
@ -49,8 +48,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
|
||||||
impl PasswordResetRequest {
|
impl PasswordResetRequest {
|
||||||
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.input_str(token);
|
hasher.input(token);
|
||||||
let token_hash = hasher.result_str();
|
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||||
|
|
||||||
let form = PasswordResetRequestForm {
|
let form = PasswordResetRequestForm {
|
||||||
user_id: from_user_id,
|
user_id: from_user_id,
|
||||||
|
@ -61,13 +60,21 @@ impl PasswordResetRequest {
|
||||||
}
|
}
|
||||||
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.input_str(token);
|
hasher.input(token);
|
||||||
let token_hash = hasher.result_str();
|
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||||
password_reset_request
|
password_reset_request
|
||||||
.filter(token_encrypted.eq(token_hash))
|
.filter(token_encrypted.eq(token_hash))
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bytes_to_hex(bytes: Vec<u8>) -> String {
|
||||||
|
let mut str = String::new();
|
||||||
|
for byte in bytes {
|
||||||
|
str = format!("{}{:02x}", str, byte);
|
||||||
|
}
|
||||||
|
str
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -85,6 +92,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -93,27 +101,26 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
||||||
let new_password_reset_request = PasswordResetRequestForm {
|
let token = "nope";
|
||||||
user_id: inserted_user.id,
|
let token_encrypted_ = "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce";
|
||||||
token_encrypted: "no".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inserted_password_reset_request =
|
let inserted_password_reset_request =
|
||||||
PasswordResetRequest::create(&conn, &new_password_reset_request).unwrap();
|
PasswordResetRequest::create_token(&conn, inserted_user.id, token).unwrap();
|
||||||
|
|
||||||
let expected_password_reset_request = PasswordResetRequest {
|
let expected_password_reset_request = PasswordResetRequest {
|
||||||
id: inserted_password_reset_request.id,
|
id: inserted_password_reset_request.id,
|
||||||
user_id: inserted_user.id,
|
user_id: inserted_user.id,
|
||||||
token_encrypted: "no".into(),
|
token_encrypted: token_encrypted_.to_string(),
|
||||||
published: inserted_password_reset_request.published,
|
published: inserted_password_reset_request.published,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_password_reset_request =
|
let read_password_reset_request = PasswordResetRequest::read_from_token(&conn, token).unwrap();
|
||||||
PasswordResetRequest::read(&conn, inserted_password_reset_request.id).unwrap();
|
|
||||||
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
||||||
|
|
||||||
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
||||||
|
|
|
@ -187,6 +187,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -195,6 +196,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -21,6 +21,7 @@ table! {
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
stickied -> Bool,
|
stickied -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
community_removed -> Bool,
|
community_removed -> Bool,
|
||||||
community_deleted -> Bool,
|
community_deleted -> Bool,
|
||||||
|
@ -59,6 +60,7 @@ pub struct PostView {
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub stickied: bool,
|
pub stickied: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
pub community_removed: bool,
|
pub community_removed: bool,
|
||||||
pub community_deleted: bool,
|
pub community_deleted: bool,
|
||||||
|
@ -187,12 +189,9 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
|
|
||||||
let mut query = self.query;
|
let mut query = self.query;
|
||||||
|
|
||||||
match self.listing_type {
|
if let ListingType::Subscribed = self.listing_type {
|
||||||
ListingType::Subscribed => {
|
|
||||||
query = query.filter(subscribed.eq(true));
|
query = query.filter(subscribed.eq(true));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
query = match self.sort {
|
query = match self.sort {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
|
@ -303,6 +302,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
|
@ -311,6 +311,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -377,6 +379,7 @@ mod tests {
|
||||||
body: None,
|
body: None,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
@ -405,7 +408,7 @@ mod tests {
|
||||||
user_id: Some(inserted_user.id),
|
user_id: Some(inserted_user.id),
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
id: inserted_post.id,
|
id: inserted_post.id,
|
||||||
name: post_name.to_owned(),
|
name: post_name,
|
||||||
url: None,
|
url: None,
|
||||||
body: None,
|
body: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
@ -413,11 +416,12 @@ mod tests {
|
||||||
locked: false,
|
locked: false,
|
||||||
stickied: false,
|
stickied: false,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name,
|
||||||
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name: community_name.to_owned(),
|
community_name,
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
community_deleted: false,
|
community_deleted: false,
|
||||||
community_nsfw: false,
|
community_nsfw: false,
|
||||||
|
|
|
@ -12,6 +12,7 @@ table! {
|
||||||
open_registration -> Bool,
|
open_registration -> Bool,
|
||||||
enable_nsfw -> Bool,
|
enable_nsfw -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
number_of_users -> BigInt,
|
number_of_users -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
number_of_comments -> BigInt,
|
number_of_comments -> BigInt,
|
||||||
|
@ -34,6 +35,7 @@ pub struct SiteView {
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: bool,
|
pub enable_nsfw: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub number_of_users: i64,
|
pub number_of_users: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub number_of_comments: i64,
|
pub number_of_comments: i64,
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub struct User_ {
|
||||||
pub preferred_username: Option<String>,
|
pub preferred_username: Option<String>,
|
||||||
pub password_encrypted: String,
|
pub password_encrypted: String,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub icon: Option<Vec<u8>>,
|
pub avatar: Option<String>,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
|
@ -24,6 +24,8 @@ pub struct User_ {
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
@ -36,12 +38,15 @@ pub struct UserForm {
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<UserForm> for User_ {
|
impl Crud<UserForm> for User_ {
|
||||||
|
@ -74,14 +79,13 @@ impl User_ {
|
||||||
pub fn update_password(
|
pub fn update_password(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
form: &UserForm,
|
new_password: &str,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let mut edited_user = form.clone();
|
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
|
||||||
let password_hash =
|
|
||||||
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
|
|
||||||
edited_user.password_encrypted = password_hash;
|
|
||||||
|
|
||||||
Self::update(&conn, user_id, &edited_user)
|
diesel::update(user_.find(user_id))
|
||||||
|
.set(password_encrypted.eq(password_hash))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
|
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
|
||||||
|
@ -99,6 +103,8 @@ pub struct Claims {
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub show_avatars: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -123,6 +129,8 @@ impl User_ {
|
||||||
default_sort_type: self.default_sort_type,
|
default_sort_type: self.default_sort_type,
|
||||||
default_listing_type: self.default_listing_type,
|
default_listing_type: self.default_listing_type,
|
||||||
lang: self.lang.to_owned(),
|
lang: self.lang.to_owned(),
|
||||||
|
avatar: self.avatar.to_owned(),
|
||||||
|
show_avatars: self.show_avatars.to_owned(),
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
@ -132,23 +140,27 @@ impl User_ {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
|
||||||
|
user_.filter(name.eq(username)).first::<User_>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
||||||
|
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_email_or_username(
|
pub fn find_by_email_or_username(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
username_or_email: &str,
|
username_or_email: &str,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
if is_email_regex(username_or_email) {
|
if is_email_regex(username_or_email) {
|
||||||
user_
|
User_::find_by_email(conn, username_or_email)
|
||||||
.filter(email.eq(username_or_email))
|
|
||||||
.first::<User_>(conn)
|
|
||||||
} else {
|
} else {
|
||||||
user_
|
User_::find_by_username(conn, username_or_email)
|
||||||
.filter(name.eq(username_or_email))
|
|
||||||
.first::<User_>(conn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
pub fn get_profile_url(&self) -> String {
|
||||||
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
format!("https://{}/u/{}", Settings::get().hostname, self.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
||||||
|
@ -172,6 +184,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -180,6 +193,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -191,7 +206,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
icon: None,
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
published: inserted_user.published,
|
published: inserted_user.published,
|
||||||
|
@ -201,6 +216,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||||
|
|
|
@ -68,6 +68,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -76,6 +77,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -86,6 +89,7 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -94,6 +98,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||||
|
|
|
@ -20,6 +20,7 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -50,6 +51,7 @@ pub struct UserMentionView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -78,7 +80,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
UserMentionQueryBuilder {
|
UserMentionQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
for_user_id: for_user_id,
|
for_user_id,
|
||||||
sort: &SortType::New,
|
sort: &SortType::New,
|
||||||
unread_only: false,
|
unread_only: false,
|
||||||
page: None,
|
page: None,
|
||||||
|
|
|
@ -6,9 +6,13 @@ table! {
|
||||||
user_view (id) {
|
user_view (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
|
email -> Nullable<Text>,
|
||||||
fedi_name -> Varchar,
|
fedi_name -> Varchar,
|
||||||
admin -> Bool,
|
admin -> Bool,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
|
show_avatars -> Bool,
|
||||||
|
send_notifications_to_email -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
post_score -> BigInt,
|
post_score -> BigInt,
|
||||||
|
@ -24,9 +28,13 @@ table! {
|
||||||
pub struct UserView {
|
pub struct UserView {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
pub fedi_name: String,
|
pub fedi_name: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub post_score: i64,
|
pub post_score: i64,
|
||||||
|
|
|
@ -11,7 +11,6 @@ pub extern crate actix;
|
||||||
pub extern crate actix_web;
|
pub extern crate actix_web;
|
||||||
pub extern crate bcrypt;
|
pub extern crate bcrypt;
|
||||||
pub extern crate chrono;
|
pub extern crate chrono;
|
||||||
pub extern crate crypto;
|
|
||||||
pub extern crate dotenv;
|
pub extern crate dotenv;
|
||||||
pub extern crate jsonwebtoken;
|
pub extern crate jsonwebtoken;
|
||||||
pub extern crate lettre;
|
pub extern crate lettre;
|
||||||
|
@ -20,19 +19,21 @@ pub extern crate rand;
|
||||||
pub extern crate regex;
|
pub extern crate regex;
|
||||||
pub extern crate serde;
|
pub extern crate serde;
|
||||||
pub extern crate serde_json;
|
pub extern crate serde_json;
|
||||||
|
pub extern crate sha2;
|
||||||
pub extern crate strum;
|
pub extern crate strum;
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod apub;
|
pub mod apub;
|
||||||
|
pub mod constants;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod feeds;
|
pub mod routes;
|
||||||
pub mod nodeinfo;
|
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod settings;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use dotenv::dotenv;
|
|
||||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||||
use lettre::smtp::extension::ClientId;
|
use lettre::smtp::extension::ClientId;
|
||||||
use lettre::smtp::ConnectionReuseParameters;
|
use lettre::smtp::ConnectionReuseParameters;
|
||||||
|
@ -40,91 +41,7 @@ use lettre::{SmtpClient, Transport};
|
||||||
use lettre_email::Email;
|
use lettre_email::Email;
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use regex::Regex;
|
use regex::{Regex, RegexBuilder};
|
||||||
use std::env;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
pub struct Settings {
|
|
||||||
pub db_url: String,
|
|
||||||
pub hostname: String,
|
|
||||||
pub bind: IpAddr,
|
|
||||||
pub port: u16,
|
|
||||||
pub jwt_secret: String,
|
|
||||||
pub rate_limit_message: i32,
|
|
||||||
pub rate_limit_message_per_second: i32,
|
|
||||||
pub rate_limit_post: i32,
|
|
||||||
pub rate_limit_post_per_second: i32,
|
|
||||||
pub rate_limit_register: i32,
|
|
||||||
pub rate_limit_register_per_second: i32,
|
|
||||||
pub email_config: Option<EmailConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EmailConfig {
|
|
||||||
smtp_server: String,
|
|
||||||
smtp_login: String,
|
|
||||||
smtp_password: String,
|
|
||||||
smtp_from_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Settings {
|
|
||||||
pub fn get() -> Self {
|
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
let email_config =
|
|
||||||
if env::var("SMTP_SERVER").is_ok() && !env::var("SMTP_SERVER").unwrap().eq("") {
|
|
||||||
Some(EmailConfig {
|
|
||||||
smtp_server: env::var("SMTP_SERVER").expect("SMTP_SERVER must be set"),
|
|
||||||
smtp_login: env::var("SMTP_LOGIN").expect("SMTP_LOGIN must be set"),
|
|
||||||
smtp_password: env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD must be set"),
|
|
||||||
smtp_from_address: env::var("SMTP_FROM_ADDRESS").expect("SMTP_FROM_ADDRESS must be set"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Settings {
|
|
||||||
db_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
|
|
||||||
hostname: env::var("HOSTNAME").unwrap_or("rrr".to_string()),
|
|
||||||
bind: env::var("BIND")
|
|
||||||
.unwrap_or("0.0.0.0".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
port: env::var("PORT")
|
|
||||||
.unwrap_or("8536".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
jwt_secret: env::var("JWT_SECRET").unwrap_or("changeme".to_string()),
|
|
||||||
rate_limit_message: env::var("RATE_LIMIT_MESSAGE")
|
|
||||||
.unwrap_or("30".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_message_per_second: env::var("RATE_LIMIT_MESSAGE_PER_SECOND")
|
|
||||||
.unwrap_or("60".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_post: env::var("RATE_LIMIT_POST")
|
|
||||||
.unwrap_or("3".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_post_per_second: env::var("RATE_LIMIT_POST_PER_SECOND")
|
|
||||||
.unwrap_or("600".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_register: env::var("RATE_LIMIT_REGISTER")
|
|
||||||
.unwrap_or("1".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_register_per_second: env::var("RATE_LIMIT_REGISTER_PER_SECOND")
|
|
||||||
.unwrap_or("3600".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
email_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn api_endpoint(&self) -> String {
|
|
||||||
format!("{}/api/v1", self.hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||||
|
@ -174,13 +91,13 @@ pub fn send_email(
|
||||||
to_username: &str,
|
to_username: &str,
|
||||||
html: &str,
|
html: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let email_config = Settings::get().email_config.ok_or("no_email_setup")?;
|
let email_config = Settings::get().email.as_ref().ok_or("no_email_setup")?;
|
||||||
|
|
||||||
let email = Email::builder()
|
let email = Email::builder()
|
||||||
.to((to_email, to_username))
|
.to((to_email, to_username))
|
||||||
.from((
|
.from((
|
||||||
email_config.smtp_login.to_owned(),
|
email_config.smtp_login.to_owned(),
|
||||||
email_config.smtp_from_address,
|
email_config.smtp_from_address.to_owned(),
|
||||||
))
|
))
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.html(html)
|
.html(html)
|
||||||
|
@ -209,11 +126,7 @@ pub fn send_email(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs};
|
||||||
#[test]
|
|
||||||
fn test_api() {
|
|
||||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_email() {
|
fn test_email() {
|
||||||
|
@ -224,11 +137,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_slur_filter() {
|
fn test_slur_filter() {
|
||||||
let test =
|
let test =
|
||||||
"coons test dindu ladyboy tranny retardeds. This is a bunch of other safe text.".to_string();
|
"coons test dindu ladyboy tranny retardeds. Capitalized Nigger. This is a bunch of other safe text.".to_string();
|
||||||
let slur_free = "No slurs here";
|
let slur_free = "No slurs here";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
remove_slurs(&test),
|
remove_slurs(&test),
|
||||||
"*removed* test *removed* *removed* *removed* *removed*. This is a bunch of other safe text."
|
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
|
||||||
.to_string()
|
.to_string()
|
||||||
);
|
);
|
||||||
assert!(has_slurs(&test));
|
assert!(has_slurs(&test));
|
||||||
|
@ -251,6 +164,6 @@ mod tests {
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
|
||||||
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,260 +2,48 @@ extern crate lemmy_server;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_files::NamedFile;
|
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use actix_web_actors::ws;
|
|
||||||
use lemmy_server::db::establish_connection;
|
use lemmy_server::db::establish_connection;
|
||||||
use lemmy_server::feeds;
|
use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
|
||||||
use lemmy_server::nodeinfo;
|
use lemmy_server::settings::Settings;
|
||||||
use lemmy_server::websocket::server::*;
|
use std::io;
|
||||||
use lemmy_server::Settings;
|
|
||||||
use std::env;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
embed_migrations!();
|
embed_migrations!();
|
||||||
|
|
||||||
/// How often heartbeat pings are sent
|
#[actix_rt::main]
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
async fn main() -> io::Result<()> {
|
||||||
/// How long before lack of client response causes a timeout
|
env_logger::init();
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
/// Entry point for our route
|
|
||||||
fn chat_route(
|
|
||||||
req: HttpRequest,
|
|
||||||
stream: web::Payload,
|
|
||||||
chat_server: web::Data<Addr<ChatServer>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
ws::start(
|
|
||||||
WSSession {
|
|
||||||
cs_addr: chat_server.get_ref().to_owned(),
|
|
||||||
id: 0,
|
|
||||||
hb: Instant::now(),
|
|
||||||
ip: req
|
|
||||||
.connection_info()
|
|
||||||
.remote()
|
|
||||||
.unwrap_or("127.0.0.1:12345")
|
|
||||||
.split(":")
|
|
||||||
.next()
|
|
||||||
.unwrap_or("127.0.0.1")
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
&req,
|
|
||||||
stream,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WSSession {
|
|
||||||
cs_addr: Addr<ChatServer>,
|
|
||||||
/// unique session id
|
|
||||||
id: usize,
|
|
||||||
ip: String,
|
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
|
||||||
/// otherwise we drop connection.
|
|
||||||
hb: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for WSSession {
|
|
||||||
type Context = ws::WebsocketContext<Self>;
|
|
||||||
|
|
||||||
/// Method is called on actor start.
|
|
||||||
/// We register ws session with ChatServer
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
// we'll start heartbeat process on session start.
|
|
||||||
self.hb(ctx);
|
|
||||||
|
|
||||||
// register self in chat server. `AsyncContext::wait` register
|
|
||||||
// future within context, but context waits until this future resolves
|
|
||||||
// before processing any other events.
|
|
||||||
// across all routes within application
|
|
||||||
let addr = ctx.address();
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(Connect {
|
|
||||||
addr: addr.recipient(),
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, act, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => act.id = res,
|
|
||||||
// something is wrong with chat server
|
|
||||||
_ => ctx.stop(),
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
|
||||||
// notify chat server
|
|
||||||
self.cs_addr.do_send(Disconnect {
|
|
||||||
id: self.id,
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
});
|
|
||||||
Running::Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle messages from chat server, we simply send it to peer websocket
|
|
||||||
/// These are room messages, IE sent to others in the room
|
|
||||||
impl Handler<WSMessage> for WSSession {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
|
||||||
// println!("id: {} msg: {}", self.id, msg.0);
|
|
||||||
ctx.text(msg.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WebSocket message handler
|
|
||||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
|
||||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
|
||||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
|
||||||
match msg {
|
|
||||||
ws::Message::Ping(msg) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
ctx.pong(&msg);
|
|
||||||
}
|
|
||||||
ws::Message::Pong(_) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
}
|
|
||||||
ws::Message::Text(text) => {
|
|
||||||
let m = text.trim().to_owned();
|
|
||||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
|
||||||
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(StandardMessage {
|
|
||||||
id: self.id,
|
|
||||||
msg: m,
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, _, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => ctx.text(res),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", &e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
|
||||||
ws::Message::Close(_) => {
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WSSession {
|
|
||||||
/// helper method that sends ping to client every second.
|
|
||||||
///
|
|
||||||
/// also this method checks heartbeats from client
|
|
||||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
|
||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
|
||||||
// check client heartbeats
|
|
||||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
|
||||||
// heartbeat timed out
|
|
||||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
|
||||||
|
|
||||||
// notify chat server
|
|
||||||
act.cs_addr.do_send(Disconnect {
|
|
||||||
id: act.id,
|
|
||||||
ip: act.ip.to_owned(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// stop actor
|
|
||||||
ctx.stop();
|
|
||||||
|
|
||||||
// don't try to send a ping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ping("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _ = env_logger::init();
|
|
||||||
let sys = actix::System::new("lemmy");
|
|
||||||
|
|
||||||
// Run the migrations from code
|
// Run the migrations from code
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
embedded_migrations::run(&conn).unwrap();
|
embedded_migrations::run(&conn).unwrap();
|
||||||
|
|
||||||
// Start chat server actor in separate thread
|
|
||||||
let server = ChatServer::default().start();
|
|
||||||
|
|
||||||
let settings = Settings::get();
|
let settings = Settings::get();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Starting http server at {}:{}",
|
||||||
|
settings.bind, settings.port
|
||||||
|
);
|
||||||
|
|
||||||
// Create Http server with websocket support
|
// Create Http server with websocket support
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.data(server.clone())
|
.configure(federation::config)
|
||||||
// Front end routes
|
.configure(feeds::config)
|
||||||
.service(actix_files::Files::new("/static", front_end_dir()))
|
.configure(index::config)
|
||||||
.route("/", web::get().to(index))
|
.configure(nodeinfo::config)
|
||||||
.route(
|
.configure(webfinger::config)
|
||||||
"/home/type/{type}/sort/{sort}/page/{page}",
|
.configure(websocket::config)
|
||||||
web::get().to(index),
|
.service(actix_files::Files::new(
|
||||||
)
|
"/static",
|
||||||
.route("/login", web::get().to(index))
|
settings.front_end_dir.to_owned(),
|
||||||
.route("/create_post", web::get().to(index))
|
))
|
||||||
.route("/create_community", web::get().to(index))
|
.service(actix_files::Files::new(
|
||||||
.route("/communities/page/{page}", web::get().to(index))
|
"/docs",
|
||||||
.route("/communities", web::get().to(index))
|
settings.front_end_dir.to_owned() + "/documentation",
|
||||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
))
|
||||||
.route("/post/{id}", web::get().to(index))
|
|
||||||
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
|
||||||
.route("/c/{name}", web::get().to(index))
|
|
||||||
.route("/community/{id}", web::get().to(index))
|
|
||||||
.route(
|
|
||||||
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/u/{username}", web::get().to(index))
|
|
||||||
.route("/user/{id}", web::get().to(index))
|
|
||||||
.route("/inbox", web::get().to(index))
|
|
||||||
.route("/modlog/community/{community_id}", web::get().to(index))
|
|
||||||
.route("/modlog", web::get().to(index))
|
|
||||||
.route("/setup", web::get().to(index))
|
|
||||||
.route(
|
|
||||||
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/search", web::get().to(index))
|
|
||||||
.route("/sponsors", web::get().to(index))
|
|
||||||
.route("/password_change/{token}", web::get().to(index))
|
|
||||||
// Websocket
|
|
||||||
.service(web::resource("/api/v1/ws").to(chat_route))
|
|
||||||
// NodeInfo
|
|
||||||
.route("/nodeinfo/2.0.json", web::get().to(nodeinfo::node_info))
|
|
||||||
.route(
|
|
||||||
"/.well-known/nodeinfo",
|
|
||||||
web::get().to(nodeinfo::node_info_well_known),
|
|
||||||
)
|
|
||||||
// RSS
|
|
||||||
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
|
||||||
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
|
||||||
})
|
})
|
||||||
.bind((settings.bind, settings.port))
|
.bind((settings.bind, settings.port))?
|
||||||
.unwrap()
|
.run()
|
||||||
.start();
|
.await
|
||||||
|
|
||||||
println!("Started http server at {}:{}", settings.bind, settings.port);
|
|
||||||
let _ = sys.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
|
||||||
Ok(NamedFile::open(front_end_dir() + "/index.html")?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn front_end_dir() -> String {
|
|
||||||
env::var("LEMMY_FRONT_END_DIR").unwrap_or("../ui/dist".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
19
server/src/routes/federation.rs
Normal file
19
server/src/routes/federation.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use crate::apub;
|
||||||
|
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route(
|
||||||
|
"/federation/c/{community_name}",
|
||||||
|
web::get().to(apub::community::get_apub_community),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/federation/c/{community_name}/followers",
|
||||||
|
web::get().to(apub::community::get_apub_community_followers),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/federation/u/{user_name}",
|
||||||
|
web::get().to(apub::user::get_apub_user),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
extern crate rss;
|
extern crate rss;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::constants::CACHE_INTERVAL_FEEDS;
|
||||||
use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
|
use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
|
||||||
use crate::db::community::Community;
|
use crate::db::community::Community;
|
||||||
use crate::db::post_view::{PostQueryBuilder, PostView};
|
use crate::db::post_view::{PostQueryBuilder, PostView};
|
||||||
use crate::db::site_view::SiteView;
|
use crate::db::site_view::SiteView;
|
||||||
use crate::db::user::User_;
|
use crate::db::user::{Claims, User_};
|
||||||
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
||||||
use crate::db::{establish_connection, ListingType, SortType};
|
use crate::db::{establish_connection, ListingType, SortType};
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
|
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||||
use actix_web::{web, HttpResponse, Result};
|
use actix_web::{web, HttpResponse, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -29,7 +32,14 @@ enum RequestType {
|
||||||
Inbox,
|
Inbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
||||||
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
||||||
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
||||||
let sort_type = match get_sort_type(info) {
|
let sort_type = match get_sort_type(info) {
|
||||||
Ok(sort_type) => sort_type,
|
Ok(sort_type) => sort_type,
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
@ -45,7 +55,10 @@ pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
|
async fn get_feed(
|
||||||
|
path: web::Path<(String, String)>,
|
||||||
|
info: web::Query<Params>,
|
||||||
|
) -> HttpResponse<Body> {
|
||||||
let sort_type = match get_sort_type(info) {
|
let sort_type = match get_sort_type(info) {
|
||||||
Ok(sort_type) => sort_type,
|
Ok(sort_type) => sort_type,
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
@ -70,6 +83,9 @@ pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) ->
|
||||||
|
|
||||||
match feed_result {
|
match feed_result {
|
||||||
Ok(rss) => HttpResponse::Ok()
|
Ok(rss) => HttpResponse::Ok()
|
||||||
|
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||||
|
CACHE_INTERVAL_FEEDS.num_seconds() as u32,
|
||||||
|
)]))
|
||||||
.content_type("application/rss+xml")
|
.content_type("application/rss+xml")
|
||||||
.body(rss),
|
.body(rss),
|
||||||
Err(_) => HttpResponse::NotFound().finish(),
|
Err(_) => HttpResponse::NotFound().finish(),
|
||||||
|
@ -77,7 +93,10 @@ pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
|
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
|
||||||
let sort_query = info.sort.to_owned().unwrap_or(SortType::Hot.to_string());
|
let sort_query = info
|
||||||
|
.sort
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| SortType::Hot.to_string());
|
||||||
SortType::from_str(&sort_query)
|
SortType::from_str(&sort_query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,8 +129,8 @@ fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Erro
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user = User_::find_by_email_or_username(&conn, &user_name)?;
|
let user = User_::find_by_username(&conn, &user_name)?;
|
||||||
let user_url = format!("https://{}/u/{}", Settings::get().hostname, user.name);
|
let user_url = user.get_profile_url();
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::All)
|
.listing_type(ListingType::All)
|
||||||
|
@ -135,7 +154,7 @@ fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<St
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let community = Community::read_from_name(&conn, community_name)?;
|
let community = Community::read_from_name(&conn, community_name)?;
|
||||||
let community_url = format!("https://{}/c/{}", Settings::get().hostname, community.name);
|
let community_url = community.get_url();
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::All)
|
.listing_type(ListingType::All)
|
||||||
|
@ -162,7 +181,7 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::Subscribed)
|
.listing_type(ListingType::Subscribed)
|
||||||
|
@ -189,7 +208,7 @@ fn get_feed_inbox(jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let sort = SortType::New;
|
let sort = SortType::New;
|
||||||
|
|
||||||
|
@ -331,7 +350,7 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
|
||||||
"/c/{} <a href=\"{}\">(link)</a>",
|
"/c/{} <a href=\"{}\">(link)</a>",
|
||||||
p.community_name, community_url
|
p.community_name, community_url
|
||||||
))
|
))
|
||||||
.domain(Settings::get().hostname)
|
.domain(Settings::get().hostname.to_owned())
|
||||||
.build();
|
.build();
|
||||||
i.categories(vec![category.unwrap()]);
|
i.categories(vec![category.unwrap()]);
|
||||||
|
|
51
server/src/routes/index.rs
Normal file
51
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::constants::CACHE_INTERVAL_FRONTEND;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use actix::Response;
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::body::Body;
|
||||||
|
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||||
|
use actix_web::middleware;
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/home/type/{type}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/login", web::get().to(index))
|
||||||
|
.route("/create_post", web::get().to(index))
|
||||||
|
.route("/create_community", web::get().to(index))
|
||||||
|
.route("/communities/page/{page}", web::get().to(index))
|
||||||
|
.route("/communities", web::get().to(index))
|
||||||
|
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||||
|
.route("/post/{id}", web::get().to(index))
|
||||||
|
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
||||||
|
.route("/c/{name}", web::get().to(index))
|
||||||
|
.route("/community/{id}", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/u/{username}", web::get().to(index))
|
||||||
|
.route("/user/{id}", web::get().to(index))
|
||||||
|
.route("/inbox", web::get().to(index))
|
||||||
|
.route("/modlog/community/{community_id}", web::get().to(index))
|
||||||
|
.route("/modlog", web::get().to(index))
|
||||||
|
.route("/setup", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/search", web::get().to(index))
|
||||||
|
.route("/sponsors", web::get().to(index))
|
||||||
|
.route("/password_change/{token}", web::get().to(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index() -> Result<NamedFile, actix_web::error::Error> {
|
||||||
|
// TODO: figure out how to set cache-control header here
|
||||||
|
Ok(NamedFile::open(
|
||||||
|
Settings::get().front_end_dir.to_owned() + "/index.html",
|
||||||
|
)?)
|
||||||
|
}
|
6
server/src/routes/mod.rs
Normal file
6
server/src/routes/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod federation;
|
||||||
|
pub mod feeds;
|
||||||
|
pub mod index;
|
||||||
|
pub mod nodeinfo;
|
||||||
|
pub mod webfinger;
|
||||||
|
pub mod websocket;
|
|
@ -3,10 +3,17 @@ use crate::db::site_view::SiteView;
|
||||||
use crate::version;
|
use crate::version;
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
|
use actix_web::web;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub fn node_info_well_known() -> HttpResponse<Body> {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/nodeinfo/2.0.json", web::get().to(node_info))
|
||||||
|
.route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn node_info_well_known() -> HttpResponse<Body> {
|
||||||
let json = json!({
|
let json = json!({
|
||||||
"links": {
|
"links": {
|
||||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||||
|
@ -14,34 +21,39 @@ pub fn node_info_well_known() -> HttpResponse<Body> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.body(json.to_string());
|
.body(json.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn node_info() -> HttpResponse<Body> {
|
async fn node_info() -> HttpResponse<Body> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
let site_view = match SiteView::read(&conn) {
|
let site_view = match SiteView::read(&conn) {
|
||||||
Ok(site_view) => site_view,
|
Ok(site_view) => site_view,
|
||||||
Err(_e) => return HttpResponse::InternalServerError().finish(),
|
Err(_e) => return HttpResponse::InternalServerError().finish(),
|
||||||
};
|
};
|
||||||
|
let protocols = if Settings::get().federation_enabled {
|
||||||
|
vec!["activitypub"]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
let json = json!({
|
let json = json!({
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"software": {
|
"software": {
|
||||||
"name": "lemmy",
|
"name": "lemmy",
|
||||||
"version": version::VERSION,
|
"version": version::VERSION,
|
||||||
},
|
},
|
||||||
"protocols": [],
|
"protocols": protocols,
|
||||||
"usage": {
|
"usage": {
|
||||||
"users": {
|
"users": {
|
||||||
"total": site_view.number_of_users
|
"total": site_view.number_of_users
|
||||||
},
|
},
|
||||||
"localPosts": site_view.number_of_posts,
|
"localPosts": site_view.number_of_posts,
|
||||||
"localComments": site_view.number_of_comments,
|
"localComments": site_view.number_of_comments,
|
||||||
"openRegistrations": true,
|
"openRegistrations": site_view.open_registration,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.body(json.to_string());
|
.body(json.to_string())
|
||||||
}
|
}
|
90
server/src/routes/webfinger.rs
Normal file
90
server/src/routes/webfinger.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use crate::db::community::Community;
|
||||||
|
use crate::db::establish_connection;
|
||||||
|
use crate::Settings;
|
||||||
|
use actix_web::body::Body;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::web::Query;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Params {
|
||||||
|
resource: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
if Settings::get().federation_enabled {
|
||||||
|
cfg.route(
|
||||||
|
".well-known/webfinger",
|
||||||
|
web::get().to(get_webfinger_response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
|
||||||
|
"^group:([a-z0-9_]{{3, 20}})@{}$",
|
||||||
|
Settings::get().hostname
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responds to webfinger requests of the following format. There isn't any real documentation for
|
||||||
|
/// this, but it described in this blog post:
|
||||||
|
/// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social
|
||||||
|
///
|
||||||
|
/// You can also view the webfinger response that Mastodon sends:
|
||||||
|
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
|
||||||
|
async fn get_webfinger_response(info: Query<Params>) -> HttpResponse<Body> {
|
||||||
|
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
|
||||||
|
.captures(&info.resource)
|
||||||
|
.map(|c| c.get(1));
|
||||||
|
// TODO: replace this with .flatten() once we are running rust 1.40
|
||||||
|
let regex_parsed_flattened = match regex_parsed {
|
||||||
|
Some(s) => s,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let community_name = match regex_parsed_flattened {
|
||||||
|
Some(c) => c.as_str(),
|
||||||
|
None => return HttpResponse::NotFound().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure the requested community exists.
|
||||||
|
let conn = establish_connection();
|
||||||
|
let community = match Community::read_from_name(&conn, community_name.to_string()) {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(_) => return HttpResponse::NotFound().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let community_url = community.get_url();
|
||||||
|
|
||||||
|
let json = json!({
|
||||||
|
"subject": info.resource,
|
||||||
|
"aliases": [
|
||||||
|
community_url,
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": community_url
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
// Yes this is correct, this link doesn't include the `.json` extension
|
||||||
|
"href": community_url
|
||||||
|
}
|
||||||
|
// TODO: this also needs to return the subscribe link once that's implemented
|
||||||
|
//{
|
||||||
|
// "rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
|
||||||
|
//}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.body(json.to_string())
|
||||||
|
}
|
186
server/src/routes/websocket.rs
Normal file
186
server/src/routes/websocket.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
use crate::websocket::server::*;
|
||||||
|
use actix::prelude::*;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::*;
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
// Start chat server actor in separate thread
|
||||||
|
let server = ChatServer::default().start();
|
||||||
|
cfg
|
||||||
|
.data(server)
|
||||||
|
.service(web::resource("/api/v1/ws").to(chat_route));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How often heartbeat pings are sent
|
||||||
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
/// How long before lack of client response causes a timeout
|
||||||
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// Entry point for our route
|
||||||
|
async fn chat_route(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: web::Payload,
|
||||||
|
chat_server: web::Data<Addr<ChatServer>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
ws::start(
|
||||||
|
WSSession {
|
||||||
|
cs_addr: chat_server.get_ref().to_owned(),
|
||||||
|
id: 0,
|
||||||
|
hb: Instant::now(),
|
||||||
|
ip: req
|
||||||
|
.connection_info()
|
||||||
|
.remote()
|
||||||
|
.unwrap_or("127.0.0.1:12345")
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("127.0.0.1")
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
&req,
|
||||||
|
stream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WSSession {
|
||||||
|
cs_addr: Addr<ChatServer>,
|
||||||
|
/// unique session id
|
||||||
|
id: usize,
|
||||||
|
ip: String,
|
||||||
|
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||||
|
/// otherwise we drop connection.
|
||||||
|
hb: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for WSSession {
|
||||||
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
|
/// Method is called on actor start.
|
||||||
|
/// We register ws session with ChatServer
|
||||||
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
// we'll start heartbeat process on session start.
|
||||||
|
self.hb(ctx);
|
||||||
|
|
||||||
|
// register self in chat server. `AsyncContext::wait` register
|
||||||
|
// future within context, but context waits until this future resolves
|
||||||
|
// before processing any other events.
|
||||||
|
// across all routes within application
|
||||||
|
let addr = ctx.address();
|
||||||
|
self
|
||||||
|
.cs_addr
|
||||||
|
.send(Connect {
|
||||||
|
addr: addr.recipient(),
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
|
})
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, act, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(res) => act.id = res,
|
||||||
|
// something is wrong with chat server
|
||||||
|
_ => ctx.stop(),
|
||||||
|
}
|
||||||
|
actix::fut::ready(())
|
||||||
|
})
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
||||||
|
// notify chat server
|
||||||
|
self.cs_addr.do_send(Disconnect {
|
||||||
|
id: self.id,
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
|
});
|
||||||
|
Running::Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle messages from chat server, we simply send it to peer websocket
|
||||||
|
/// These are room messages, IE sent to others in the room
|
||||||
|
impl Handler<WSMessage> for WSSession {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
||||||
|
// println!("id: {} msg: {}", self.id, msg.0);
|
||||||
|
ctx.text(msg.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket message handler
|
||||||
|
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
||||||
|
fn handle(&mut self, result: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||||
|
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||||
|
let message = match result {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match message {
|
||||||
|
ws::Message::Ping(msg) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
ctx.pong(&msg);
|
||||||
|
}
|
||||||
|
ws::Message::Pong(_) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
}
|
||||||
|
ws::Message::Text(text) => {
|
||||||
|
let m = text.trim().to_owned();
|
||||||
|
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
||||||
|
|
||||||
|
self
|
||||||
|
.cs_addr
|
||||||
|
.send(StandardMessage {
|
||||||
|
id: self.id,
|
||||||
|
msg: m,
|
||||||
|
})
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, _, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(res) => ctx.text(res),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actix::fut::ready(())
|
||||||
|
})
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
||||||
|
ws::Message::Close(_) => {
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WSSession {
|
||||||
|
/// helper method that sends ping to client every second.
|
||||||
|
///
|
||||||
|
/// also this method checks heartbeats from client
|
||||||
|
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||||
|
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||||
|
// check client heartbeats
|
||||||
|
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||||
|
// heartbeat timed out
|
||||||
|
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||||
|
|
||||||
|
// notify chat server
|
||||||
|
act.cs_addr.do_send(Disconnect {
|
||||||
|
id: act.id,
|
||||||
|
ip: act.ip.to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// stop actor
|
||||||
|
ctx.stop();
|
||||||
|
|
||||||
|
// don't try to send a ping
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ping(b"");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -260,7 +260,7 @@ table! {
|
||||||
preferred_username -> Nullable<Varchar>,
|
preferred_username -> Nullable<Varchar>,
|
||||||
password_encrypted -> Text,
|
password_encrypted -> Text,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
icon -> Nullable<Bytea>,
|
avatar -> Nullable<Text>,
|
||||||
admin -> Bool,
|
admin -> Bool,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
|
@ -270,6 +270,8 @@ table! {
|
||||||
default_sort_type -> Int2,
|
default_sort_type -> Int2,
|
||||||
default_listing_type -> Int2,
|
default_listing_type -> Int2,
|
||||||
lang -> Varchar,
|
lang -> Varchar,
|
||||||
|
show_avatars -> Bool,
|
||||||
|
send_notifications_to_email -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
106
server/src/settings.rs
Normal file
106
server/src/settings.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
extern crate lazy_static;
|
||||||
|
use config::{Config, ConfigError, Environment, File};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::env;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
|
||||||
|
static CONFIG_FILE: &str = "config/config.hjson";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub database: Database,
|
||||||
|
pub hostname: String,
|
||||||
|
pub bind: IpAddr,
|
||||||
|
pub port: u16,
|
||||||
|
pub jwt_secret: String,
|
||||||
|
pub front_end_dir: String,
|
||||||
|
pub rate_limit: RateLimitConfig,
|
||||||
|
pub email: Option<EmailConfig>,
|
||||||
|
pub federation_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RateLimitConfig {
|
||||||
|
pub message: i32,
|
||||||
|
pub message_per_second: i32,
|
||||||
|
pub post: i32,
|
||||||
|
pub post_per_second: i32,
|
||||||
|
pub register: i32,
|
||||||
|
pub register_per_second: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct EmailConfig {
|
||||||
|
pub smtp_server: String,
|
||||||
|
pub smtp_login: String,
|
||||||
|
pub smtp_password: String,
|
||||||
|
pub smtp_from_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Database {
|
||||||
|
pub user: String,
|
||||||
|
pub password: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i32,
|
||||||
|
pub database: String,
|
||||||
|
pub pool_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SETTINGS: Settings = {
|
||||||
|
match Settings::init() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => panic!("{}", e),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// Reads config from the files and environment.
|
||||||
|
/// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten
|
||||||
|
/// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are
|
||||||
|
/// added to the config.
|
||||||
|
fn init() -> Result<Self, ConfigError> {
|
||||||
|
let mut s = Config::new();
|
||||||
|
|
||||||
|
s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
|
||||||
|
|
||||||
|
s.merge(File::with_name(CONFIG_FILE).required(false))?;
|
||||||
|
|
||||||
|
// Add in settings from the environment (with a prefix of LEMMY)
|
||||||
|
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
|
||||||
|
// Note: we need to use double underscore here, because otherwise variables containing
|
||||||
|
// underscore cant be set from environmnet.
|
||||||
|
// https://github.com/mehcode/config-rs/issues/73
|
||||||
|
s.merge(Environment::with_prefix("LEMMY").separator("__"))?;
|
||||||
|
|
||||||
|
s.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the config as a struct.
|
||||||
|
pub fn get() -> &'static Self {
|
||||||
|
&SETTINGS
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
|
||||||
|
/// otherwise the connection url is generated from the config.
|
||||||
|
pub fn get_database_url(&self) -> String {
|
||||||
|
match env::var("LEMMY_DATABASE_URL") {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(_) => format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
self.database.user,
|
||||||
|
self.database.password,
|
||||||
|
self.database.host,
|
||||||
|
self.database.port,
|
||||||
|
self.database.database
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_endpoint(&self) -> String {
|
||||||
|
format!("{}/api/v1", self.hostname)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
pub const VERSION: &'static str = "v0.5.0.3";
|
pub const VERSION: &str = "v0.5.17";
|
||||||
|
|
|
@ -21,6 +21,7 @@ use crate::Settings;
|
||||||
|
|
||||||
/// Chat server sends this messages to session
|
/// Chat server sends this messages to session
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
pub struct WSMessage(pub String);
|
pub struct WSMessage(pub String);
|
||||||
|
|
||||||
/// Message for chat server communications
|
/// Message for chat server communications
|
||||||
|
@ -35,6 +36,7 @@ pub struct Connect {
|
||||||
|
|
||||||
/// Session is disconnected
|
/// Session is disconnected
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
pub struct Disconnect {
|
pub struct Disconnect {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
|
@ -42,6 +44,7 @@ pub struct Disconnect {
|
||||||
|
|
||||||
/// Send message to specific room
|
/// Send message to specific room
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
pub struct ClientMessage {
|
pub struct ClientMessage {
|
||||||
/// Id of the client session
|
/// Id of the client session
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
@ -51,7 +54,8 @@ pub struct ClientMessage {
|
||||||
pub room: String,
|
pub room: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Message)]
|
||||||
|
#[rtype(String)]
|
||||||
pub struct StandardMessage {
|
pub struct StandardMessage {
|
||||||
/// Id of the client session
|
/// Id of the client session
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
@ -59,10 +63,6 @@ pub struct StandardMessage {
|
||||||
pub msg: String,
|
pub msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix::Message for StandardMessage {
|
|
||||||
type Result = String;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RateLimitBucket {
|
pub struct RateLimitBucket {
|
||||||
last_checked: SystemTime,
|
last_checked: SystemTime,
|
||||||
|
@ -91,7 +91,7 @@ impl Default for ChatServer {
|
||||||
ChatServer {
|
ChatServer {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
rate_limits: HashMap::new(),
|
rate_limits: HashMap::new(),
|
||||||
rooms: rooms,
|
rooms,
|
||||||
rng: rand::thread_rng(),
|
rng: rand::thread_rng(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,8 +99,8 @@ impl Default for ChatServer {
|
||||||
|
|
||||||
impl ChatServer {
|
impl ChatServer {
|
||||||
/// Send message to all users in the room
|
/// Send message to all users in the room
|
||||||
fn send_room_message(&self, room: &i32, message: &str, skip_id: usize) {
|
fn send_room_message(&self, room: i32, message: &str, skip_id: usize) {
|
||||||
if let Some(sessions) = self.rooms.get(room) {
|
if let Some(sessions) = self.rooms.get(&room) {
|
||||||
for id in sessions {
|
for id in sessions {
|
||||||
if *id != skip_id {
|
if *id != skip_id {
|
||||||
if let Some(info) = self.sessions.get(id) {
|
if let Some(info) = self.sessions.get(id) {
|
||||||
|
@ -113,7 +113,7 @@ impl ChatServer {
|
||||||
|
|
||||||
fn join_room(&mut self, room_id: i32, id: usize) {
|
fn join_room(&mut self, room_id: i32, id: usize) {
|
||||||
// remove session from all rooms
|
// remove session from all rooms
|
||||||
for (_n, sessions) in &mut self.rooms {
|
for sessions in self.rooms.values_mut() {
|
||||||
sessions.remove(&id);
|
sessions.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,12 +122,12 @@ impl ChatServer {
|
||||||
self.rooms.insert(room_id, HashSet::new());
|
self.rooms.insert(room_id, HashSet::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
&self.rooms.get_mut(&room_id).unwrap().insert(id);
|
self.rooms.get_mut(&room_id).unwrap().insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_community_message(
|
fn send_community_message(
|
||||||
&self,
|
&self,
|
||||||
community_id: &i32,
|
community_id: i32,
|
||||||
message: &str,
|
message: &str,
|
||||||
skip_id: usize,
|
skip_id: usize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
@ -138,12 +138,12 @@ impl ChatServer {
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::Community)
|
.listing_type(ListingType::Community)
|
||||||
.sort(&SortType::New)
|
.sort(&SortType::New)
|
||||||
.for_community_id(*community_id)
|
.for_community_id(community_id)
|
||||||
.limit(9999)
|
.limit(9999)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
self.send_room_message(&post.id, message, skip_id);
|
self.send_room_message(post.id, message, skip_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -152,27 +152,28 @@ impl ChatServer {
|
||||||
fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> {
|
fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> {
|
||||||
self.check_rate_limit_full(
|
self.check_rate_limit_full(
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit_register,
|
Settings::get().rate_limit.register,
|
||||||
Settings::get().rate_limit_register_per_second,
|
Settings::get().rate_limit.register_per_second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> {
|
fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> {
|
||||||
self.check_rate_limit_full(
|
self.check_rate_limit_full(
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit_post,
|
Settings::get().rate_limit.post,
|
||||||
Settings::get().rate_limit_post_per_second,
|
Settings::get().rate_limit.post_per_second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> {
|
fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> {
|
||||||
self.check_rate_limit_full(
|
self.check_rate_limit_full(
|
||||||
id,
|
id,
|
||||||
Settings::get().rate_limit_message,
|
Settings::get().rate_limit.message,
|
||||||
Settings::get().rate_limit_message_per_second,
|
Settings::get().rate_limit.message_per_second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> {
|
fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> {
|
||||||
if let Some(info) = self.sessions.get(&id) {
|
if let Some(info) = self.sessions.get(&id) {
|
||||||
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
||||||
|
@ -194,10 +195,13 @@ impl ChatServer {
|
||||||
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
||||||
&info.ip, time_passed, rate_limit.allowance
|
&info.ip, time_passed, rate_limit.allowance
|
||||||
);
|
);
|
||||||
Err(APIError {
|
Err(
|
||||||
|
APIError {
|
||||||
op: "Rate Limit".to_string(),
|
op: "Rate Limit".to_string(),
|
||||||
message: format!("Too many requests. {} per {} seconds", rate, per),
|
message: format!("Too many requests. {} per {} seconds", rate, per),
|
||||||
})?
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
rate_limit.allowance -= 1.0;
|
rate_limit.allowance -= 1.0;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -264,7 +268,7 @@ impl Handler<Disconnect> for ChatServer {
|
||||||
// remove address
|
// remove address
|
||||||
if self.sessions.remove(&msg.id).is_some() {
|
if self.sessions.remove(&msg.id).is_some() {
|
||||||
// remove session from all rooms
|
// remove session from all rooms
|
||||||
for (_id, sessions) in &mut self.rooms {
|
for sessions in self.rooms.values_mut() {
|
||||||
if sessions.remove(&msg.id) {
|
if sessions.remove(&msg.id) {
|
||||||
// rooms.push(*id);
|
// rooms.push(*id);
|
||||||
}
|
}
|
||||||
|
@ -292,7 +296,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let data = &json["data"].to_string();
|
let data = &json["data"].to_string();
|
||||||
let op = &json["op"].as_str().ok_or(APIError {
|
let op = &json["op"].as_str().ok_or(APIError {
|
||||||
op: "Unknown op type".to_string(),
|
op: "Unknown op type".to_string(),
|
||||||
message: format!("Unknown op type"),
|
message: "Unknown op type".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let user_operation: UserOperation = UserOperation::from_str(&op)?;
|
let user_operation: UserOperation = UserOperation::from_str(&op)?;
|
||||||
|
@ -374,7 +378,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
community_sent.community.user_id = None;
|
community_sent.community.user_id = None;
|
||||||
community_sent.community.subscribed = None;
|
community_sent.community.subscribed = None;
|
||||||
let community_sent_str = serde_json::to_string(&community_sent)?;
|
let community_sent_str = serde_json::to_string(&community_sent)?;
|
||||||
chat.send_community_message(&community_sent.community.id, &community_sent_str, msg.id)?;
|
chat.send_community_message(community_sent.community.id, &community_sent_str, msg.id)?;
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::FollowCommunity => {
|
UserOperation::FollowCommunity => {
|
||||||
|
@ -392,7 +396,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let community_id = ban_from_community.community_id;
|
let community_id = ban_from_community.community_id;
|
||||||
let res = Oper::new(user_operation, ban_from_community).perform()?;
|
let res = Oper::new(user_operation, ban_from_community).perform()?;
|
||||||
let res_str = serde_json::to_string(&res)?;
|
let res_str = serde_json::to_string(&res)?;
|
||||||
chat.send_community_message(&community_id, &res_str, msg.id)?;
|
chat.send_community_message(community_id, &res_str, msg.id)?;
|
||||||
Ok(res_str)
|
Ok(res_str)
|
||||||
}
|
}
|
||||||
UserOperation::AddModToCommunity => {
|
UserOperation::AddModToCommunity => {
|
||||||
|
@ -400,7 +404,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let community_id = mod_add_to_community.community_id;
|
let community_id = mod_add_to_community.community_id;
|
||||||
let res = Oper::new(user_operation, mod_add_to_community).perform()?;
|
let res = Oper::new(user_operation, mod_add_to_community).perform()?;
|
||||||
let res_str = serde_json::to_string(&res)?;
|
let res_str = serde_json::to_string(&res)?;
|
||||||
chat.send_community_message(&community_id, &res_str, msg.id)?;
|
chat.send_community_message(community_id, &res_str, msg.id)?;
|
||||||
Ok(res_str)
|
Ok(res_str)
|
||||||
}
|
}
|
||||||
UserOperation::ListCategories => {
|
UserOperation::ListCategories => {
|
||||||
|
@ -437,7 +441,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let mut post_sent = res.clone();
|
let mut post_sent = res.clone();
|
||||||
post_sent.post.my_vote = None;
|
post_sent.post.my_vote = None;
|
||||||
let post_sent_str = serde_json::to_string(&post_sent)?;
|
let post_sent_str = serde_json::to_string(&post_sent)?;
|
||||||
chat.send_room_message(&post_sent.post.id, &post_sent_str, msg.id);
|
chat.send_room_message(post_sent.post.id, &post_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::SavePost => {
|
UserOperation::SavePost => {
|
||||||
|
@ -454,7 +458,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::EditComment => {
|
UserOperation::EditComment => {
|
||||||
|
@ -465,7 +469,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::SaveComment => {
|
UserOperation::SaveComment => {
|
||||||
|
@ -482,7 +486,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::GetModlog => {
|
UserOperation::GetModlog => {
|
||||||
|
|
2
ui/.eslintignore
vendored
Normal file
2
ui/.eslintignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
fuse.js
|
||||||
|
translation_report.ts
|
10
ui/fuse.js
vendored
10
ui/fuse.js
vendored
|
@ -4,7 +4,7 @@ const {
|
||||||
EnvPlugin,
|
EnvPlugin,
|
||||||
CSSPlugin,
|
CSSPlugin,
|
||||||
WebIndexPlugin,
|
WebIndexPlugin,
|
||||||
QuantumPlugin
|
QuantumPlugin,
|
||||||
} = require('fuse-box');
|
} = require('fuse-box');
|
||||||
// const transformInferno = require('../../dist').default
|
// const transformInferno = require('../../dist').default
|
||||||
const transformInferno = require('ts-transform-inferno').default;
|
const transformInferno = require('ts-transform-inferno').default;
|
||||||
|
@ -25,7 +25,7 @@ Sparky.task('config', _ => {
|
||||||
before: [transformClasscat(), transformInferno()],
|
before: [transformClasscat(), transformInferno()],
|
||||||
},
|
},
|
||||||
alias: {
|
alias: {
|
||||||
'locale': 'moment/locale'
|
locale: 'moment/locale',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
|
EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
|
||||||
|
@ -33,7 +33,7 @@ Sparky.task('config', _ => {
|
||||||
WebIndexPlugin({
|
WebIndexPlugin({
|
||||||
title: 'Inferno Typescript FuseBox Example',
|
title: 'Inferno Typescript FuseBox Example',
|
||||||
template: 'src/index.html',
|
template: 'src/index.html',
|
||||||
path: isProduction ? "/static" : "/"
|
path: isProduction ? '/static' : '/',
|
||||||
}),
|
}),
|
||||||
isProduction &&
|
isProduction &&
|
||||||
QuantumPlugin({
|
QuantumPlugin({
|
||||||
|
@ -48,7 +48,9 @@ Sparky.task('config', _ => {
|
||||||
// Sparky.task('version', _ => setVersion());
|
// Sparky.task('version', _ => setVersion());
|
||||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||||
Sparky.task('env', _ => (isProduction = true));
|
Sparky.task('env', _ => (isProduction = true));
|
||||||
Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static'));
|
Sparky.task('copy-assets', () =>
|
||||||
|
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
|
||||||
|
);
|
||||||
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
||||||
fuse.dev();
|
fuse.dev();
|
||||||
app.hmr().watch();
|
app.hmr().watch();
|
||||||
|
|
18
ui/package.json
vendored
18
ui/package.json
vendored
|
@ -15,33 +15,32 @@
|
||||||
"@types/autosize": "^3.0.6",
|
"@types/autosize": "^3.0.6",
|
||||||
"@types/js-cookie": "^2.2.1",
|
"@types/js-cookie": "^2.2.1",
|
||||||
"@types/jwt-decode": "^2.2.1",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
"@types/markdown-it": "^0.0.7",
|
"@types/markdown-it": "^0.0.9",
|
||||||
"@types/markdown-it-container": "^2.0.2",
|
"@types/markdown-it-container": "^2.0.2",
|
||||||
"autosize": "^4.0.2",
|
"autosize": "^4.0.2",
|
||||||
"bootswatch": "^4.3.1",
|
"bootswatch": "^4.3.1",
|
||||||
"classcat": "^1.1.3",
|
"classcat": "^1.1.3",
|
||||||
"dotenv": "^6.1.0",
|
"dotenv": "^8.2.0",
|
||||||
"emoji-short-name": "^0.1.0",
|
"emoji-short-name": "^0.1.0",
|
||||||
"husky": "^3.0.9",
|
"husky": "^3.0.9",
|
||||||
"i18next": "^17.0.9",
|
"i18next": "^19.0.3",
|
||||||
"inferno": "^7.0.1",
|
"inferno": "^7.0.1",
|
||||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||||
"inferno-router": "^7.0.1",
|
"inferno-router": "^7.0.1",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"markdown-it": "^8.4.2",
|
"markdown-it": "^10.0.0",
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^2.0.0",
|
||||||
"markdown-it-emoji": "^1.4.0",
|
"markdown-it-emoji": "^1.4.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"rxjs": "^6.4.0",
|
"rxjs": "^6.4.0",
|
||||||
"terser": "^3.17.0",
|
"terser": "^4.6.0",
|
||||||
"tributejs": "3.7.2",
|
"tributejs": "^4.1.1",
|
||||||
"twemoji": "^12.1.2",
|
"twemoji": "^12.1.2",
|
||||||
"ws": "^7.0.0"
|
"ws": "^7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/i18next": "^12.1.0",
|
|
||||||
"eslint": "^6.5.1",
|
"eslint": "^6.5.1",
|
||||||
"eslint-plugin-inferno": "^7.14.3",
|
"eslint-plugin-inferno": "^7.14.3",
|
||||||
"eslint-plugin-jane": "^7.0.0",
|
"eslint-plugin-jane": "^7.0.0",
|
||||||
|
@ -58,7 +57,7 @@
|
||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"pre-commit": "lint-staged"
|
"pre-commit": "cargo fmt --manifest-path ../server/Cargo.toml && cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
@ -67,6 +66,9 @@
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"git add"
|
"git add"
|
||||||
],
|
],
|
||||||
|
"../server/src/**/*.rs": [
|
||||||
|
"git add"
|
||||||
|
],
|
||||||
"package.json": [
|
"package.json": [
|
||||||
"sortpack",
|
"sortpack",
|
||||||
"git add"
|
"git add"
|
||||||
|
|
4
ui/src/components/comment-form.tsx
vendored
4
ui/src/components/comment-form.tsx
vendored
|
@ -18,11 +18,11 @@ import {
|
||||||
markdownHelpUrl,
|
markdownHelpUrl,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import * as autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
import Tribute from 'tributejs/src/Tribute.js';
|
import Tribute from 'tributejs/src/Tribute.js';
|
||||||
import * as emojiShortName from 'emoji-short-name';
|
import emojiShortName from 'emoji-short-name';
|
||||||
|
|
||||||
interface CommentFormProps {
|
interface CommentFormProps {
|
||||||
postId?: number;
|
postId?: number;
|
||||||
|
|
172
ui/src/components/comment-node.tsx
vendored
172
ui/src/components/comment-node.tsx
vendored
|
@ -17,8 +17,15 @@ import {
|
||||||
BanType,
|
BanType,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
|
import {
|
||||||
import * as moment from 'moment';
|
mdToHtml,
|
||||||
|
getUnixTime,
|
||||||
|
canMod,
|
||||||
|
isMod,
|
||||||
|
pictshareAvatarThumbnail,
|
||||||
|
showAvatars,
|
||||||
|
} from '../utils';
|
||||||
|
import moment from 'moment';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { CommentForm } from './comment-form';
|
import { CommentForm } from './comment-form';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
|
@ -36,6 +43,8 @@ interface CommentNodeState {
|
||||||
banType: BanType;
|
banType: BanType;
|
||||||
showConfirmTransferSite: boolean;
|
showConfirmTransferSite: boolean;
|
||||||
showConfirmTransferCommunity: boolean;
|
showConfirmTransferCommunity: boolean;
|
||||||
|
showConfirmAppointAsMod: boolean;
|
||||||
|
showConfirmAppointAsAdmin: boolean;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
viewSource: boolean;
|
viewSource: boolean;
|
||||||
}
|
}
|
||||||
|
@ -65,6 +74,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
showConfirmTransferSite: false,
|
showConfirmTransferSite: false,
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
|
showConfirmAppointAsMod: false,
|
||||||
|
showConfirmAppointAsAdmin: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -128,7 +139,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
className="text-info"
|
className="text-info"
|
||||||
to={`/u/${node.comment.creator_name}`}
|
to={`/u/${node.comment.creator_name}`}
|
||||||
>
|
>
|
||||||
{node.comment.creator_name}
|
{node.comment.creator_avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
|
||||||
|
class="rounded-circle mr-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{node.comment.creator_name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{this.isMod && (
|
{this.isMod && (
|
||||||
|
@ -192,6 +211,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
|
{this.props.markable && (
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleMarkRead)}
|
||||||
|
>
|
||||||
|
{node.comment.read
|
||||||
|
? i18n.t('mark_as_unread')
|
||||||
|
: i18n.t('mark_as_read')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{UserService.Instance.user && !this.props.viewOnly && (
|
{UserService.Instance.user && !this.props.viewOnly && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
@ -232,13 +263,35 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span
|
||||||
|
className="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
|
>
|
||||||
|
<T i18nKey="view_source">#</T>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="link">#</T>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
{/* Admins and mods can remove comments */}
|
{/* Admins and mods can remove comments */}
|
||||||
{(this.canMod || this.canAdmin) && (
|
{(this.canMod || this.canAdmin) && (
|
||||||
|
<>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!node.comment.removed ? (
|
{!node.comment.removed ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModRemoveShow
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<T i18nKey="remove">#</T>
|
<T i18nKey="remove">#</T>
|
||||||
</span>
|
</span>
|
||||||
|
@ -254,6 +307,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{/* Mods can ban from community, and appoint as mods to community */}
|
{/* Mods can ban from community, and appoint as mods to community */}
|
||||||
{this.canMod && (
|
{this.canMod && (
|
||||||
|
@ -285,17 +339,43 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
{!node.comment.banned_from_community && (
|
{!node.comment.banned_from_community && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
{!this.state.showConfirmAppointAsMod ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
this.handleAddModToCommunity
|
this.handleShowConfirmAppointAsMod
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{this.isMod
|
{this.isMod
|
||||||
? i18n.t('remove_as_mod')
|
? i18n.t('remove_as_mod')
|
||||||
: i18n.t('appoint_as_mod')}
|
: i18n.t('appoint_as_mod')}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleAddModToCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelConfirmAppointAsMod
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -367,14 +447,40 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
{!node.comment.banned && (
|
{!node.comment.banned && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
{!this.state.showConfirmAppointAsAdmin ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleAddAdmin)}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleShowConfirmAppointAsAdmin
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{this.isAdmin
|
{this.isAdmin
|
||||||
? i18n.t('remove_as_admin')
|
? i18n.t('remove_as_admin')
|
||||||
: i18n.t('appoint_as_admin')}
|
: i18n.t('appoint_as_admin')}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelConfirmAppointAsAdmin
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -418,34 +524,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">
|
|
||||||
<span
|
|
||||||
className="pointer"
|
|
||||||
onClick={linkEvent(this, this.handleViewSource)}
|
|
||||||
>
|
|
||||||
<T i18nKey="view_source">#</T>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<Link
|
|
||||||
className="text-muted"
|
|
||||||
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
|
||||||
>
|
|
||||||
<T i18nKey="link">#</T>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{this.props.markable && (
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<span
|
|
||||||
class="pointer"
|
|
||||||
onClick={linkEvent(this, this.handleMarkRead)}
|
|
||||||
>
|
|
||||||
{node.comment.read
|
|
||||||
? i18n.t('mark_as_unread')
|
|
||||||
: i18n.t('mark_as_read')}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -711,13 +789,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModBanFromCommunityShow(i: CommentNode) {
|
handleModBanFromCommunityShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = !i.state.showBanDialog;
|
||||||
i.state.banType = BanType.Community;
|
i.state.banType = BanType.Community;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModBanShow(i: CommentNode) {
|
handleModBanShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = !i.state.showBanDialog;
|
||||||
i.state.banType = BanType.Site;
|
i.state.banType = BanType.Site;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -770,6 +848,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleShowConfirmAppointAsMod(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsMod = true;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelConfirmAppointAsMod(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsMod = false;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
handleAddModToCommunity(i: CommentNode) {
|
handleAddModToCommunity(i: CommentNode) {
|
||||||
let form: AddModToCommunityForm = {
|
let form: AddModToCommunityForm = {
|
||||||
user_id: i.props.node.comment.creator_id,
|
user_id: i.props.node.comment.creator_id,
|
||||||
|
@ -777,6 +865,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
added: !i.isMod,
|
added: !i.isMod,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.addModToCommunity(form);
|
WebSocketService.Instance.addModToCommunity(form);
|
||||||
|
i.state.showConfirmAppointAsMod = false;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowConfirmAppointAsAdmin(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsAdmin = true;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelConfirmAppointAsAdmin(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsAdmin = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -786,6 +885,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
added: !i.isAdmin,
|
added: !i.isAdmin,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.addAdmin(form);
|
WebSocketService.Instance.addAdmin(form);
|
||||||
|
i.state.showConfirmAppointAsAdmin = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
ui/src/components/community-form.tsx
vendored
10
ui/src/components/community-form.tsx
vendored
|
@ -7,6 +7,7 @@ import {
|
||||||
Category,
|
Category,
|
||||||
ListCategoriesResponse,
|
ListCategoriesResponse,
|
||||||
CommunityResponse,
|
CommunityResponse,
|
||||||
|
GetSiteResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { msgOp, capitalizeFirstLetter } from '../utils';
|
import { msgOp, capitalizeFirstLetter } from '../utils';
|
||||||
|
@ -27,6 +28,7 @@ interface CommunityFormState {
|
||||||
communityForm: CommunityFormI;
|
communityForm: CommunityFormI;
|
||||||
categories: Array<Category>;
|
categories: Array<Category>;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
enable_nsfw: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommunityForm extends Component<
|
export class CommunityForm extends Component<
|
||||||
|
@ -44,6 +46,7 @@ export class CommunityForm extends Component<
|
||||||
},
|
},
|
||||||
categories: [],
|
categories: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
enable_nsfw: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -79,6 +82,7 @@ export class CommunityForm extends Component<
|
||||||
);
|
);
|
||||||
|
|
||||||
WebSocketService.Instance.listCategories();
|
WebSocketService.Instance.listCategories();
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -157,7 +161,7 @@ export class CommunityForm extends Component<
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{WebSocketService.Instance.site.enable_nsfw && (
|
{this.state.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -267,6 +271,10 @@ export class CommunityForm extends Component<
|
||||||
let res: CommunityResponse = msg;
|
let res: CommunityResponse = msg;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.props.onEdit(res.community);
|
this.props.onEdit(res.community);
|
||||||
|
} else if (op == UserOperation.GetSite) {
|
||||||
|
let res: GetSiteResponse = msg;
|
||||||
|
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
ui/src/components/footer.tsx
vendored
4
ui/src/components/footer.tsx
vendored
|
@ -23,8 +23,8 @@ export class Footer extends Component<any, any> {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>
|
<a class="nav-link" href={'/docs/index.html'}>
|
||||||
<T i18nKey="api">#</T>
|
<T i18nKey="docs">#</T>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|
23
ui/src/components/login.tsx
vendored
23
ui/src/components/login.tsx
vendored
|
@ -7,6 +7,7 @@ import {
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
UserOperation,
|
UserOperation,
|
||||||
PasswordResetForm,
|
PasswordResetForm,
|
||||||
|
GetSiteResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { msgOp, validEmail } from '../utils';
|
import { msgOp, validEmail } from '../utils';
|
||||||
|
@ -18,6 +19,7 @@ interface State {
|
||||||
registerForm: RegisterForm;
|
registerForm: RegisterForm;
|
||||||
loginLoading: boolean;
|
loginLoading: boolean;
|
||||||
registerLoading: boolean;
|
registerLoading: boolean;
|
||||||
|
enable_nsfw: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Login extends Component<any, State> {
|
export class Login extends Component<any, State> {
|
||||||
|
@ -37,6 +39,7 @@ export class Login extends Component<any, State> {
|
||||||
},
|
},
|
||||||
loginLoading: false,
|
loginLoading: false,
|
||||||
registerLoading: false,
|
registerLoading: false,
|
||||||
|
enable_nsfw: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -58,18 +61,14 @@ export class Login extends Component<any, State> {
|
||||||
err => console.error(err),
|
err => console.error(err),
|
||||||
() => console.log('complete')
|
() => console.log('complete')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.title = `${i18n.t('login')} - ${
|
|
||||||
WebSocketService.Instance.site.name
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -205,7 +204,7 @@ export class Login extends Component<any, State> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{WebSocketService.Instance.site.enable_nsfw && (
|
{this.state.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -271,6 +270,9 @@ export class Login extends Component<any, State> {
|
||||||
|
|
||||||
handleRegisterEmailChange(i: Login, event: any) {
|
handleRegisterEmailChange(i: Login, event: any) {
|
||||||
i.state.registerForm.email = event.target.value;
|
i.state.registerForm.email = event.target.value;
|
||||||
|
if (i.state.registerForm.email == '') {
|
||||||
|
i.state.registerForm.email = undefined;
|
||||||
|
}
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,6 +321,13 @@ export class Login extends Component<any, State> {
|
||||||
this.props.history.push('/communities');
|
this.props.history.push('/communities');
|
||||||
} else if (op == UserOperation.PasswordReset) {
|
} else if (op == UserOperation.PasswordReset) {
|
||||||
alert(i18n.t('reset_password_mail_sent'));
|
alert(i18n.t('reset_password_mail_sent'));
|
||||||
|
} else if (op == UserOperation.GetSite) {
|
||||||
|
let res: GetSiteResponse = msg;
|
||||||
|
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||||
|
this.setState(this.state);
|
||||||
|
document.title = `${i18n.t('login')} - ${
|
||||||
|
WebSocketService.Instance.site.name
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
ui/src/components/main.tsx
vendored
15
ui/src/components/main.tsx
vendored
|
@ -31,6 +31,8 @@ import {
|
||||||
routeSortTypeToEnum,
|
routeSortTypeToEnum,
|
||||||
routeListingTypeToEnum,
|
routeListingTypeToEnum,
|
||||||
postRefetchSeconds,
|
postRefetchSeconds,
|
||||||
|
pictshareAvatarThumbnail,
|
||||||
|
showAvatars,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -65,6 +67,9 @@ export class Main extends Component<any, MainState> {
|
||||||
number_of_posts: null,
|
number_of_posts: null,
|
||||||
number_of_comments: null,
|
number_of_comments: null,
|
||||||
number_of_communities: null,
|
number_of_communities: null,
|
||||||
|
enable_downvotes: null,
|
||||||
|
open_registration: null,
|
||||||
|
enable_nsfw: null,
|
||||||
},
|
},
|
||||||
admins: [],
|
admins: [],
|
||||||
banned: [],
|
banned: [],
|
||||||
|
@ -341,7 +346,15 @@ export class Main extends Component<any, MainState> {
|
||||||
{this.state.site.admins.map(admin => (
|
{this.state.site.admins.map(admin => (
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<Link class="text-info" to={`/u/${admin.name}`}>
|
<Link class="text-info" to={`/u/${admin.name}`}>
|
||||||
{admin.name}
|
{admin.avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
src={pictshareAvatarThumbnail(admin.avatar)}
|
||||||
|
class="rounded-circle mr-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{admin.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
14
ui/src/components/navbar.tsx
vendored
14
ui/src/components/navbar.tsx
vendored
|
@ -13,7 +13,7 @@ import {
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
Comment,
|
Comment,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { msgOp } from '../utils';
|
import { msgOp, pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||||
import { version } from '../version';
|
import { version } from '../version';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -151,7 +151,19 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
to={`/u/${UserService.Instance.user.username}`}
|
to={`/u/${UserService.Instance.user.username}`}
|
||||||
>
|
>
|
||||||
|
<span>
|
||||||
|
{UserService.Instance.user.avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
src={pictshareAvatarThumbnail(
|
||||||
|
UserService.Instance.user.avatar
|
||||||
|
)}
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
class="rounded-circle mr-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{UserService.Instance.user.username}
|
{UserService.Instance.user.username}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
|
|
79
ui/src/components/post-form.tsx
vendored
79
ui/src/components/post-form.tsx
vendored
|
@ -15,6 +15,7 @@ import {
|
||||||
SearchForm,
|
SearchForm,
|
||||||
SearchType,
|
SearchType,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
GetSiteResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import {
|
import {
|
||||||
|
@ -26,8 +27,9 @@ import {
|
||||||
archiveUrl,
|
archiveUrl,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
debounce,
|
debounce,
|
||||||
|
isImage,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import * as autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
@ -48,6 +50,7 @@ interface PostFormState {
|
||||||
suggestedTitle: string;
|
suggestedTitle: string;
|
||||||
suggestedPosts: Array<Post>;
|
suggestedPosts: Array<Post>;
|
||||||
crossPosts: Array<Post>;
|
crossPosts: Array<Post>;
|
||||||
|
enable_nsfw: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
@ -69,10 +72,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
suggestedTitle: undefined,
|
suggestedTitle: undefined,
|
||||||
suggestedPosts: [],
|
suggestedPosts: [],
|
||||||
crossPosts: [],
|
crossPosts: [],
|
||||||
|
enable_nsfw: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
|
||||||
|
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
|
@ -80,7 +86,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.state.postForm = {
|
this.state.postForm = {
|
||||||
body: this.props.post.body,
|
body: this.props.post.body,
|
||||||
// NOTE: debouncing breaks both these for some reason, unless you use defaultValue
|
// NOTE: debouncing breaks both these for some reason, unless you use defaultValue
|
||||||
name: undefined,
|
name: this.props.post.name,
|
||||||
community_id: this.props.post.community_id,
|
community_id: this.props.post.community_id,
|
||||||
edit_id: this.props.post.id,
|
edit_id: this.props.post.id,
|
||||||
creator_id: this.props.post.creator_id,
|
creator_id: this.props.post.creator_id,
|
||||||
|
@ -101,14 +107,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
retryWhen(errors =>
|
|
||||||
errors.pipe(
|
|
||||||
delay(3000),
|
|
||||||
take(10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.subscribe(
|
.subscribe(
|
||||||
msg => this.parseMessage(msg),
|
msg => this.parseMessage(msg),
|
||||||
err => console.error(err),
|
err => console.error(err),
|
||||||
|
@ -121,6 +120,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
WebSocketService.Instance.getSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -143,7 +143,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
defaultValue={this.state.postForm.url}
|
value={this.state.postForm.url}
|
||||||
onInput={linkEvent(this, this.handlePostUrlChange)}
|
onInput={linkEvent(this, this.handlePostUrlChange)}
|
||||||
/>
|
/>
|
||||||
{this.state.suggestedTitle && (
|
{this.state.suggestedTitle && (
|
||||||
|
@ -193,6 +193,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
{isImage(this.state.postForm.url) && (
|
||||||
|
<img src={this.state.postForm.url} class="img-fluid" />
|
||||||
|
)}
|
||||||
{this.state.crossPosts.length > 0 && (
|
{this.state.crossPosts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div class="my-1 text-muted small font-weight-bold">
|
<div class="my-1 text-muted small font-weight-bold">
|
||||||
|
@ -209,10 +212,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea
|
<textarea
|
||||||
defaultValue={
|
|
||||||
this.props.post ? this.props.post.name : undefined
|
|
||||||
}
|
|
||||||
/* This needs to be undefined for some weird reason */
|
|
||||||
value={this.state.postForm.name}
|
value={this.state.postForm.name}
|
||||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@ -285,7 +284,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{WebSocketService.Instance.site.enable_nsfw && (
|
{this.state.enable_nsfw && (
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
@ -348,11 +347,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostUrlChange = debounce((i: PostForm, event: any) => {
|
handlePostUrlChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.url = event.target.value;
|
i.state.postForm.url = event.target.value;
|
||||||
if (validURL(i.state.postForm.url)) {
|
i.setState(i.state);
|
||||||
|
i.fetchPageTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPageTitle() {
|
||||||
|
if (validURL(this.state.postForm.url)) {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: i.state.postForm.url,
|
q: this.state.postForm.url,
|
||||||
type_: SearchType[SearchType.Url],
|
type_: SearchType[SearchType.Url],
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -362,37 +366,40 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
|
|
||||||
// Fetch the page title
|
// Fetch the page title
|
||||||
getPageTitle(i.state.postForm.url).then(d => {
|
getPageTitle(this.state.postForm.url).then(d => {
|
||||||
i.state.suggestedTitle = d;
|
this.state.suggestedTitle = d;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
i.state.suggestedTitle = undefined;
|
this.state.suggestedTitle = undefined;
|
||||||
i.state.crossPosts = [];
|
this.state.crossPosts = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i.setState(i.state);
|
handlePostNameChange(i: PostForm, event: any) {
|
||||||
});
|
|
||||||
|
|
||||||
handlePostNameChange = debounce((i: PostForm, event: any) => {
|
|
||||||
i.state.postForm.name = event.target.value;
|
i.state.postForm.name = event.target.value;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.fetchSimilarPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSimilarPosts() {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: i.state.postForm.name,
|
q: this.state.postForm.name,
|
||||||
type_: SearchType[SearchType.Posts],
|
type_: SearchType[SearchType.Posts],
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
community_id: i.state.postForm.community_id,
|
community_id: this.state.postForm.community_id,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (i.state.postForm.name !== '') {
|
if (this.state.postForm.name !== '') {
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
} else {
|
} else {
|
||||||
i.state.suggestedPosts = [];
|
this.state.suggestedPosts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
});
|
}
|
||||||
|
|
||||||
handlePostBodyChange(i: PostForm, event: any) {
|
handlePostBodyChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.body = event.target.value;
|
i.state.postForm.body = event.target.value;
|
||||||
|
@ -488,6 +495,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
this.state.crossPosts = res.posts;
|
this.state.crossPosts = res.posts;
|
||||||
}
|
}
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
} else if (op == UserOperation.GetSite) {
|
||||||
|
let res: GetSiteResponse = msg;
|
||||||
|
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
ui/src/components/post-listing.tsx
vendored
15
ui/src/components/post-listing.tsx
vendored
|
@ -25,6 +25,9 @@ import {
|
||||||
isImage,
|
isImage,
|
||||||
isVideo,
|
isVideo,
|
||||||
getUnixTime,
|
getUnixTime,
|
||||||
|
pictshareAvatarThumbnail,
|
||||||
|
showAvatars,
|
||||||
|
imageThumbnailer,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -135,7 +138,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="mx-2 mt-1 float-left img-fluid thumbnail rounded"
|
class="mx-2 mt-1 float-left img-fluid thumbnail rounded"
|
||||||
src={post.url}
|
src={imageThumbnailer(post.url)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -248,7 +251,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>{i18n.t('by')} </span>
|
<span>{i18n.t('by')} </span>
|
||||||
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
||||||
{post.creator_name}
|
{post.creator_avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
src={pictshareAvatarThumbnail(post.creator_avatar)}
|
||||||
|
class="rounded-circle mr-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{post.creator_name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
{this.isMod && (
|
{this.isMod && (
|
||||||
<span className="mx-1 badge badge-light">
|
<span className="mx-1 badge badge-light">
|
||||||
|
|
12
ui/src/components/post.tsx
vendored
12
ui/src/components/post.tsx
vendored
|
@ -34,7 +34,7 @@ import { PostListings } from './post-listings';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { CommentForm } from './comment-form';
|
import { CommentForm } from './comment-form';
|
||||||
import { CommentNodes } from './comment-nodes';
|
import { CommentNodes } from './comment-nodes';
|
||||||
import * as autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
@ -76,14 +76,7 @@ export class Post extends Component<any, PostState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
retryWhen(errors =>
|
|
||||||
errors.pipe(
|
|
||||||
delay(3000),
|
|
||||||
take(10)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.subscribe(
|
.subscribe(
|
||||||
msg => this.parseMessage(msg),
|
msg => this.parseMessage(msg),
|
||||||
err => console.error(err),
|
err => console.error(err),
|
||||||
|
@ -169,7 +162,6 @@ export class Post extends Component<any, PostState> {
|
||||||
post={this.state.post}
|
post={this.state.post}
|
||||||
showBody
|
showBody
|
||||||
showCommunity
|
showCommunity
|
||||||
editable
|
|
||||||
moderators={this.state.moderators}
|
moderators={this.state.moderators}
|
||||||
admins={this.state.admins}
|
admins={this.state.admins}
|
||||||
/>
|
/>
|
||||||
|
|
16
ui/src/components/search.tsx
vendored
16
ui/src/components/search.tsx
vendored
|
@ -19,6 +19,8 @@ import {
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
routeSearchTypeToEnum,
|
routeSearchTypeToEnum,
|
||||||
routeSortTypeToEnum,
|
routeSortTypeToEnum,
|
||||||
|
pictshareAvatarThumbnail,
|
||||||
|
showAvatars,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
@ -286,7 +288,19 @@ export class Search extends Component<any, SearchState> {
|
||||||
<Link
|
<Link
|
||||||
className="text-info"
|
className="text-info"
|
||||||
to={`/u/${(i.data as UserView).name}`}
|
to={`/u/${(i.data as UserView).name}`}
|
||||||
>{`/u/${(i.data as UserView).name}`}</Link>
|
>
|
||||||
|
{(i.data as UserView).avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
src={pictshareAvatarThumbnail(
|
||||||
|
(i.data as UserView).avatar
|
||||||
|
)}
|
||||||
|
class="rounded-circle mr-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{`/u/${(i.data as UserView).name}`}</span>
|
||||||
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span>{` - ${
|
<span>{` - ${
|
||||||
(i.data as UserView).comment_score
|
(i.data as UserView).comment_score
|
||||||
|
|
17
ui/src/components/sidebar.tsx
vendored
17
ui/src/components/sidebar.tsx
vendored
|
@ -8,7 +8,12 @@ import {
|
||||||
UserView,
|
UserView,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime } from '../utils';
|
import {
|
||||||
|
mdToHtml,
|
||||||
|
getUnixTime,
|
||||||
|
pictshareAvatarThumbnail,
|
||||||
|
showAvatars,
|
||||||
|
} from '../utils';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -194,7 +199,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
{this.props.moderators.map(mod => (
|
{this.props.moderators.map(mod => (
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
||||||
{mod.user_name}
|
{mod.avatar && showAvatars() && (
|
||||||
|
<img
|
||||||
|
height="32"
|
||||||
|
width="32"
|
||||||
|
src={pictshareAvatarThumbnail(mod.avatar)}
|
||||||
|
class="rounded-circle mr-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{mod.user_name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
2
ui/src/components/site-form.tsx
vendored
2
ui/src/components/site-form.tsx
vendored
|
@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno';
|
||||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { capitalizeFirstLetter } from '../utils';
|
import { capitalizeFirstLetter } from '../utils';
|
||||||
import * as autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue