forked from nutomic/lemmy
Compare commits
143 commits
Author | SHA1 | Date | |
---|---|---|---|
752318fdf3 | |||
baf77bb6be | |||
047ec97e18 | |||
2fb4900b0c | |||
cba8081579 | |||
d7285d8c25 | |||
415040a1e9 | |||
7a97c981a0 | |||
c41082f98f | |||
|
05f2bfc83c | ||
|
fb82a489d5 | ||
|
2af3f1d5cc | ||
|
b6aa9a30e8 | ||
|
676de4ab84 | ||
|
966f76f5cc | ||
|
f8e9578ff8 | ||
|
645fc9a620 | ||
|
a9c8127a69 | ||
|
5cf27d255a | ||
|
b14c8f1a46 | ||
|
d5af66c1b1 | ||
|
0457d4c8f1 | ||
|
69d816c865 | ||
|
24770126d4 | ||
|
318ce4a52a | ||
|
fc26a9a377 | ||
|
1e884c6969 | ||
|
04c7f99f67 | ||
|
efdc98dfa0 | ||
|
b0246a784b | ||
|
7e8c0b146b | ||
|
d762230f61 | ||
|
f8525b2474 | ||
|
48e221d06c | ||
|
6cd9156d3b | ||
|
655c5db59a | ||
|
10533ff005 | ||
|
0671390475 | ||
|
afdad2abc3 | ||
|
a2c469977c | ||
|
1cf97a8661 | ||
|
aaa64811f4 | ||
|
556016614d | ||
|
b0899cf55e | ||
|
cc11930bdd | ||
|
66f0683160 | ||
|
13a5c50c70 | ||
|
3f4cce99ed | ||
|
6260fea707 | ||
|
083fcb9c6c | ||
|
a06476fa96 | ||
|
aa502b687d | ||
|
5f4a35c80a | ||
|
a6d88fdfb0 | ||
|
7839eb6d40 | ||
|
d0de6552ab | ||
|
1707b19f80 | ||
|
ebaa96a9d6 | ||
|
33b602f353 | ||
|
7a82e9ffd2 | ||
|
ae02747ee0 | ||
|
34ddd62fd1 | ||
|
9755654734 | ||
|
38ba7dfb1a | ||
|
519a509412 | ||
|
dab6695ae2 | ||
|
a08d743747 | ||
|
ad2fc2e8d9 | ||
02bcbc42d6 | |||
|
8fe034c320 | ||
66c95993dc | |||
|
6d89f6f955 | ||
|
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 |
134 changed files with 7103 additions and 4218 deletions
1
.dockerignore
vendored
1
.dockerignore
vendored
|
@ -1,5 +1,4 @@
|
|||
ui/node_modules
|
||||
ui/dist
|
||||
server/target
|
||||
docs
|
||||
.git
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
|||
ansible/inventory
|
||||
ansible/passwords/
|
||||
docker/lemmy_mine.hjson
|
||||
docker/dev/env_deploy.sh
|
||||
build/
|
||||
.idea/
|
||||
|
|
26
.travis.yml
vendored
26
.travis.yml
vendored
|
@ -5,21 +5,31 @@ matrix:
|
|||
allow_failures:
|
||||
- rust: nightly
|
||||
fast_finish: true
|
||||
cache:
|
||||
directories:
|
||||
- /home/travis/.cargo
|
||||
cache: cargo
|
||||
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:
|
||||
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
||||
- psql -c 'create database rrr with owner rrr;' -U postgres
|
||||
- psql -c "create user lemmy with password 'password' superuser;" -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:
|
||||
- cd server
|
||||
script:
|
||||
- diesel migration run
|
||||
# Default checks, but fail if anything is detected
|
||||
- cargo build
|
||||
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
||||
- cargo install diesel_cli --no-default-features --features postgres --force
|
||||
- diesel migration run
|
||||
- cargo test
|
||||
env:
|
||||
- DATABASE_URL=postgres://rrr:rrr@localhost/rrr
|
||||
global:
|
||||
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
- RUST_TEST_THREADS=1
|
||||
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
|
4
CONTRIBUTING.md
vendored
Normal file
4
CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Contributing
|
||||
|
||||
See [here](https://dev.lemmy.ml/docs/contributing.html) for contributing Instructions.
|
||||
|
144
README.md
vendored
144
README.md
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
[![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)
|
||||
![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)
|
||||
[![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)
|
||||
|
@ -28,7 +28,7 @@
|
|||
<br>
|
||||
</p>
|
||||
|
||||
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
|
||||
[Lemmy Dev instance](https://dev.lemmy.ml) *This data is being backed up, and once federation is working, it will be the basis for a main instance.*
|
||||
|
||||
This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
|
||||
|
||||
|
@ -36,30 +36,19 @@ Front Page|Post
|
|||
---|---
|
||||
![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)
|
||||
- [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)
|
||||
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.
|
||||
|
||||
<!-- 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)
|
||||
- [Releases / Changelog](/RELEASES.md)
|
||||
- [Contributing](https://dev.lemmy.ml/docs/contributing.html)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -90,25 +79,13 @@ Front Page|Post
|
|||
- Front end is `~80kB` gzipped.
|
||||
- Supports arm64 / Raspberry Pi.
|
||||
|
||||
## About
|
||||
|
||||
[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?
|
||||
## 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/).
|
||||
|
||||
## Install
|
||||
|
||||
### Docker
|
||||
|
@ -119,8 +96,8 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
|||
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/prod/.env
|
||||
# Edit the .env if you want custom passwords
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||
# Edit lemmy.hjson to do more configuration
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
|
@ -156,79 +133,6 @@ nano inventory # enter your server, domain, contact email
|
|||
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
|
||||
|
||||
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,15 +151,15 @@ If you'd like to add translations, take a look a look at the [English translatio
|
|||
|
||||
lang | done | missing
|
||||
--- | --- | ---
|
||||
de | 100% |
|
||||
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
|
||||
es | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
fr | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
it | 96% | archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
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
|
||||
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
|
||||
sv | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
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
|
||||
de | 94% | avatar,upload_avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,email_already_exists
|
||||
eo | 81% | number_of_communities,preview,upload_image,avatar,upload_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,email_already_exists
|
||||
es | 89% | avatar,upload_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,email_already_exists
|
||||
fr | 89% | avatar,upload_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,email_already_exists
|
||||
it | 90% | avatar,upload_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,email_already_exists
|
||||
nl | 100% | email_already_exists
|
||||
ru | 77% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,upload_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,email_already_exists
|
||||
sv | 89% | avatar,upload_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,email_already_exists
|
||||
zh | 75% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,upload_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,email_already_exists
|
||||
|
||||
|
||||
If you'd like to update this report, run:
|
||||
|
|
22
RELEASES.md
vendored
Normal file
22
RELEASES.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Lemmy v0.6.0 Release (2020-01-16)
|
||||
|
||||
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/dessalines/lemmy/milestone/15?closed=1)
|
||||
|
||||
This is the biggest release by far:
|
||||
|
||||
- Avatars!
|
||||
- Optional Email notifications for username mentions, post and comment replies.
|
||||
- Ability to change your password and email address.
|
||||
- Can set a custom language.
|
||||
- Lemmy-wide settings to disable downvotes, and close registration.
|
||||
- A better documentation system, hosted in lemmy itself.
|
||||
- [Huge DB performance gains](https://github.com/dessalines/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
|
||||
- Fixed major issue with similar post URL and title searching.
|
||||
- Upgraded to Actix `2.0`
|
||||
- Faster comment / post voting.
|
||||
- Better small screen support.
|
||||
- Lots of bug fixes, refactoring of back end code.
|
||||
|
||||
Another major announcement is that Lemmy now has another lead developer besides me, [@felix@radical.town](https://radical.town/@felix). Theyve created a better documentation system, implemented RSS feeds, simplified docker and project configs, upgraded actix, working on federation, a whole lot else.
|
||||
|
||||
https://dev.lemmy.ml
|
2
ansible/inventory.example
vendored
2
ansible/inventory.example
vendored
|
@ -1,6 +1,6 @@
|
|||
[lemmy]
|
||||
# define the username and hostname that you use for ssh connection, and specify the domain
|
||||
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com
|
||||
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com smtp_server=smtp@example.com smtp_login=your@email.com smtp_password=pass smtp_from_address="Example.com Admin <notifications@example.com>"
|
||||
|
||||
[all:vars]
|
||||
ansible_connection=ssh
|
||||
|
|
10
ansible/lemmy.yml
vendored
10
ansible/lemmy.yml
vendored
|
@ -32,21 +32,13 @@
|
|||
- name: add all template files
|
||||
template: src={{item.src}} dest={{item.dest}}
|
||||
with_items:
|
||||
- { src: 'templates/env', dest: '/lemmy/.env' }
|
||||
- { 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' }
|
||||
vars:
|
||||
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres 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
|
||||
systemd:
|
||||
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 }}
|
12
ansible/templates/nginx.conf
vendored
12
ansible/templates/nginx.conf
vendored
|
@ -1,3 +1,5 @@
|
|||
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ domain }};
|
||||
|
@ -59,6 +61,13 @@ server {
|
|||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Proxy Cache
|
||||
proxy_cache lemmy_frontend_cache;
|
||||
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_min_uses 5;
|
||||
}
|
||||
|
||||
location /pictshare/ {
|
||||
|
@ -68,8 +77,7 @@ server {
|
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
|
||||
add_header Cache-Control "public";
|
||||
expires max;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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>
|
13
docker/dev/Dockerfile
vendored
13
docker/dev/Dockerfile
vendored
|
@ -10,7 +10,7 @@ RUN yarn install --pure-lockfile
|
|||
COPY ui /app/ui
|
||||
RUN yarn build
|
||||
|
||||
FROM ekidd/rust-musl-builder:1.38.0-openssl11 as rust
|
||||
FROM ekidd/rust-musl-builder:1.40.0-openssl11 as rust
|
||||
|
||||
# Cache deps
|
||||
WORKDIR /app
|
||||
|
@ -32,14 +32,25 @@ RUN cargo build --frozen --release
|
|||
# Get diesel-cli on there just in case
|
||||
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||
|
||||
|
||||
FROM ekidd/rust-musl-builder:1.40.0-openssl11 as docs
|
||||
WORKDIR /app
|
||||
COPY docs ./docs
|
||||
RUN sudo chown -R rust:rust .
|
||||
RUN mdbook build docs/
|
||||
|
||||
|
||||
FROM alpine:3.10
|
||||
|
||||
# Install libpq for postgres
|
||||
RUN apk add libpq
|
||||
|
||||
# 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=docs /app/docs/book/ /app/dist/documentation/
|
||||
COPY --from=node /app/ui/dist /app/dist
|
||||
|
||||
RUN addgroup -g 1000 lemmy
|
||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||
RUN chown lemmy:lemmy /app/lemmy
|
||||
|
|
3
docker/dev/Dockerfile.aarch64
vendored
3
docker/dev/Dockerfile.aarch64
vendored
|
@ -15,7 +15,7 @@ RUN yarn build
|
|||
FROM multiarch/qemu-user-static as qemu
|
||||
|
||||
|
||||
FROM arm64v8/rust:1.37-buster as rust
|
||||
FROM arm64v8/rust:1.40-buster as rust
|
||||
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
#COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
|||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
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
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
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
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
COPY --from=node /app/ui/dist /app/dist
|
||||
|
||||
RUN chown lemmy:lemmy /app/lemmy
|
||||
USER lemmy
|
||||
EXPOSE 8536
|
||||
|
|
20
docker/dev/deploy.sh
vendored
20
docker/dev/deploy.sh
vendored
|
@ -5,12 +5,14 @@ git checkout master
|
|||
new_tag="$1"
|
||||
git tag $new_tag
|
||||
|
||||
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||
|
||||
# Setting the version on the front end
|
||||
cd ../../
|
||||
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||
git add "ui/src/version.ts"
|
||||
# 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"
|
||||
|
||||
cd docker/dev
|
||||
|
@ -38,14 +40,22 @@ docker push dessalines/lemmy:x64-$new_tag
|
|||
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||
|
||||
# aarch64
|
||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||
docker push dessalines/lemmy:arm64-$new_tag
|
||||
# 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 tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||
docker push dessalines/lemmy:arm64-$new_tag
|
||||
fi
|
||||
|
||||
# Creating the manifest for the multi-arch build
|
||||
docker manifest create dessalines/lemmy:$new_tag \
|
||||
if [ $third_semver -eq 0 ]; then
|
||||
docker manifest create dessalines/lemmy:$new_tag \
|
||||
dessalines/lemmy:x64-$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
|
||||
|
||||
|
|
3
docker/dev/dev_deploy.sh
vendored
3
docker/dev/dev_deploy.sh
vendored
|
@ -7,3 +7,6 @@ git checkout dev
|
|||
docker-compose build
|
||||
docker tag dev_lemmy:latest dessalines/lemmy:dev
|
||||
docker push dessalines/lemmy:dev
|
||||
|
||||
# SSH and pull it
|
||||
ssh $LEMMY_USER@$LEMMY_HOST "cd ~/git/lemmy/docker/dev && docker pull dessalines/lemmy:dev && docker-compose up -d"
|
||||
|
|
19
docker/dev/docker-compose.yml
vendored
19
docker/dev/docker-compose.yml
vendored
|
@ -5,7 +5,7 @@ services:
|
|||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
|
@ -16,22 +16,9 @@ services:
|
|||
dockerfile: docker/dev/Dockerfile
|
||||
ports:
|
||||
- "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
|
||||
volumes:
|
||||
- ../lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
lemmy_pictshare:
|
||||
|
|
6
docker/k8s/lemmy.yml
vendored
6
docker/k8s/lemmy.yml
vendored
|
@ -14,13 +14,13 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: DATABASE_URL
|
||||
- name: LEMMY_DATABASE_URL
|
||||
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
||||
value: CHANGE_ME
|
||||
- name: HOSTNAME
|
||||
- name: LEMMY_HOSTNAME
|
||||
# example: 'lemmy.example.com'
|
||||
value: CHANGE_ME
|
||||
- name: JWT_SECRET
|
||||
- name: LEMMY_JWT_SECRET
|
||||
# example: 'very-super-good-secret'
|
||||
value: CHANGE_ME
|
||||
- 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: "my_domain"
|
||||
# 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
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
restart: always
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.5.0.3
|
||||
image: dessalines/lemmy:v0.6.0
|
||||
ports:
|
||||
- "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
|
||||
volumes:
|
||||
- ./lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
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.
|
32
docs/src/contributing.md
vendored
Normal file
32
docs/src/contributing.md
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Contributing
|
||||
|
||||
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.
|
||||
|
||||
## Translating
|
||||
|
||||
Go [here](https://github.com/dessalines/lemmy#translations) for translation instructions.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Front end
|
||||
|
||||
- The front end is written in `typescript`, using a react-like framework called [inferno](https://infernojs.org/). All UI elements are reusable `.tsx` components.
|
||||
- The main page and routing are in `ui/src/index.tsx`.
|
||||
- The components are located in `ui/src/components`.
|
||||
|
||||
### Back end
|
||||
|
||||
- The back end is written in `rust`, using `diesel`, and `actix`.
|
||||
- The server source code is split into main sections in `server/src`. These include:
|
||||
- `db` - The low level database actions.
|
||||
- Database additions are done using diesel migrations. Run `diesel migration generate xxxxx` to add new things.
|
||||
- `api` - The high level user interactions (things like `CreateComment`)
|
||||
- `routes` - The server endpoints .
|
||||
- `apub` - The activitypub conversions.
|
||||
- `websocket` - Creates the websocket server.
|
||||
|
||||
## Linting / Formatting
|
||||
|
||||
- Every front and back end commit is automatically formatted then linted using `husky`, and `lint-staged`.
|
||||
- Rust with `cargo fmt` and `cargo clippy`.
|
||||
- Typescript with `prettier` and `eslint`.
|
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
|
||||
```
|
2
server/.gitignore
vendored
2
server/.gitignore
vendored
|
@ -2,3 +2,5 @@
|
|||
.env
|
||||
.idea
|
||||
env_setup.sh
|
||||
query_testing/*.json
|
||||
query_testing/*.json.old
|
||||
|
|
1
server/.rustfmt.toml
vendored
1
server/.rustfmt.toml
vendored
|
@ -1 +1,2 @@
|
|||
tab_spaces = 2
|
||||
edition="2018"
|
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"
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono"] }
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.14.1"
|
||||
bcrypt = "0.5.0"
|
||||
activitypub = "0.1.5"
|
||||
dotenv = "0.15.0"
|
||||
bcrypt = "0.6.1"
|
||||
activitypub = "0.2.0"
|
||||
chrono = { version = "0.4.7", features = ["serde"] }
|
||||
failure = "0.1.5"
|
||||
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
||||
serde = { version = "1.0.94", features = ["derive"] }
|
||||
actix = "0.8.3"
|
||||
actix-web = "1.0"
|
||||
actix-files = "0.1.3"
|
||||
actix-web-actors = "1.0"
|
||||
env_logger = "0.6.2"
|
||||
actix = "0.9.0"
|
||||
actix-web = "2.0.0"
|
||||
actix-files = "0.2.1"
|
||||
actix-web-actors = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
env_logger = "0.7.1"
|
||||
rand = "0.7.0"
|
||||
strum = "0.15.0"
|
||||
strum_macros = "0.15.0"
|
||||
strum = "0.17.1"
|
||||
strum_macros = "0.17.1"
|
||||
jsonwebtoken = "6.0.1"
|
||||
regex = "1.1.9"
|
||||
lazy_static = "1.3.0"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
rust-crypto = "^0.2"
|
||||
sha2 = "0.8.0"
|
||||
rss = "1.8.0"
|
||||
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: "my_domain"
|
||||
# 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;
|
16
server/migrations/2020-01-11-012452_add_indexes/down.sql
vendored
Normal file
16
server/migrations/2020-01-11-012452_add_indexes/down.sql
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
drop index idx_post_creator;
|
||||
drop index idx_post_community;
|
||||
|
||||
drop index idx_post_like_post;
|
||||
drop index idx_post_like_user;
|
||||
|
||||
drop index idx_comment_creator;
|
||||
drop index idx_comment_parent;
|
||||
drop index idx_comment_post;
|
||||
|
||||
drop index idx_comment_like_comment;
|
||||
drop index idx_comment_like_user;
|
||||
drop index idx_comment_like_post;
|
||||
|
||||
drop index idx_community_creator;
|
||||
drop index idx_community_category;
|
17
server/migrations/2020-01-11-012452_add_indexes/up.sql
vendored
Normal file
17
server/migrations/2020-01-11-012452_add_indexes/up.sql
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
-- Go through all the tables joins, optimize every view, CTE, etc.
|
||||
create index idx_post_creator on post (creator_id);
|
||||
create index idx_post_community on post (community_id);
|
||||
|
||||
create index idx_post_like_post on post_like (post_id);
|
||||
create index idx_post_like_user on post_like (user_id);
|
||||
|
||||
create index idx_comment_creator on comment (creator_id);
|
||||
create index idx_comment_parent on comment (parent_id);
|
||||
create index idx_comment_post on comment (post_id);
|
||||
|
||||
create index idx_comment_like_comment on comment_like (comment_id);
|
||||
create index idx_comment_like_user on comment_like (user_id);
|
||||
create index idx_comment_like_post on comment_like (post_id);
|
||||
|
||||
create index idx_community_creator on community (creator_id);
|
||||
create index idx_community_category on community (category_id);
|
223
server/migrations/2020-01-13-025151_create_materialized_views/down.sql
vendored
Normal file
223
server/migrations/2020-01-13-025151_create_materialized_views/down.sql
vendored
Normal file
|
@ -0,0 +1,223 @@
|
|||
-- functions and triggers
|
||||
drop trigger refresh_user on user_;
|
||||
drop function refresh_user();
|
||||
drop trigger refresh_post on post;
|
||||
drop function refresh_post();
|
||||
drop trigger refresh_post_like on post_like;
|
||||
drop function refresh_post_like();
|
||||
drop trigger refresh_community on community;
|
||||
drop function refresh_community();
|
||||
drop trigger refresh_community_follower on community_follower;
|
||||
drop function refresh_community_follower();
|
||||
drop trigger refresh_community_user_ban on community_user_ban;
|
||||
drop function refresh_community_user_ban();
|
||||
drop trigger refresh_comment on comment;
|
||||
drop function refresh_comment();
|
||||
drop trigger refresh_comment_like on comment_like;
|
||||
drop function refresh_comment_like();
|
||||
|
||||
-- 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
|
||||
;
|
||||
|
||||
drop view post_mview;
|
||||
drop materialized view post_aggregates_mview;
|
||||
drop view post_aggregates_view;
|
||||
|
||||
-- user
|
||||
drop materialized view user_mview;
|
||||
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;
|
||||
|
||||
-- community
|
||||
drop view community_mview;
|
||||
drop materialized view community_aggregates_mview;
|
||||
drop view community_view;
|
||||
drop view community_aggregates_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;
|
||||
drop view comment_mview;
|
||||
drop materialized view comment_aggregates_mview;
|
||||
drop view comment_aggregates_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;
|
||||
|
437
server/migrations/2020-01-13-025151_create_materialized_views/up.sql
vendored
Normal file
437
server/migrations/2020-01-13-025151_create_materialized_views/up.sql
vendored
Normal file
|
@ -0,0 +1,437 @@
|
|||
-- post
|
||||
create view post_aggregates_view 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;
|
||||
|
||||
create materialized view post_aggregates_mview as select * from post_aggregates_view;
|
||||
|
||||
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
|
||||
|
||||
drop view post_view;
|
||||
create view post_view as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_view pa
|
||||
)
|
||||
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
|
||||
;
|
||||
|
||||
create view post_mview as
|
||||
with all_post as (
|
||||
select
|
||||
pa.*
|
||||
from post_aggregates_mview pa
|
||||
)
|
||||
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
|
||||
;
|
||||
|
||||
|
||||
-- user_view
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
u.email,
|
||||
u.fedi_name,
|
||||
u.admin,
|
||||
u.banned,
|
||||
u.show_avatars,
|
||||
u.send_notifications_to_email,
|
||||
u.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;
|
||||
|
||||
create materialized view user_mview as select * from user_view;
|
||||
|
||||
create unique index idx_user_mview_id on user_mview (id);
|
||||
|
||||
-- community
|
||||
create view community_aggregates_view as
|
||||
select c.*,
|
||||
(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;
|
||||
|
||||
create materialized view community_aggregates_mview as select * from community_aggregates_view;
|
||||
|
||||
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
|
||||
|
||||
drop view community_view;
|
||||
create view community_view as
|
||||
with all_community as
|
||||
(
|
||||
select
|
||||
ca.*
|
||||
from community_aggregates_view ca
|
||||
)
|
||||
|
||||
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
|
||||
;
|
||||
|
||||
create view community_mview as
|
||||
with all_community as
|
||||
(
|
||||
select
|
||||
ca.*
|
||||
from community_aggregates_mview ca
|
||||
)
|
||||
|
||||
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_aggregates_view 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;
|
||||
|
||||
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
|
||||
|
||||
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
|
||||
|
||||
drop view reply_view;
|
||||
drop view user_mention_view;
|
||||
drop view comment_view;
|
||||
|
||||
create view comment_view as
|
||||
with all_comment as
|
||||
(
|
||||
select
|
||||
ca.*
|
||||
from comment_aggregates_view ca
|
||||
)
|
||||
|
||||
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 comment_mview as
|
||||
with all_comment as
|
||||
(
|
||||
select
|
||||
ca.*
|
||||
from comment_aggregates_mview ca
|
||||
)
|
||||
|
||||
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;
|
||||
|
||||
-- user
|
||||
create or replace function refresh_user()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently user_mview;
|
||||
refresh materialized view concurrently comment_aggregates_mview; -- cause of bans
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_user
|
||||
after insert or update or delete or truncate
|
||||
on user_
|
||||
for each statement
|
||||
execute procedure refresh_user();
|
||||
|
||||
-- post
|
||||
create or replace function refresh_post()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
refresh materialized view concurrently user_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_post
|
||||
after insert or update or delete or truncate
|
||||
on post
|
||||
for each statement
|
||||
execute procedure refresh_post();
|
||||
|
||||
-- post_like
|
||||
create or replace function refresh_post_like()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
refresh materialized view concurrently user_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_post_like
|
||||
after insert or update or delete or truncate
|
||||
on post_like
|
||||
for each statement
|
||||
execute procedure refresh_post_like();
|
||||
|
||||
-- community
|
||||
create or replace function refresh_community()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
refresh materialized view concurrently community_aggregates_mview;
|
||||
refresh materialized view concurrently user_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_community
|
||||
after insert or update or delete or truncate
|
||||
on community
|
||||
for each statement
|
||||
execute procedure refresh_community();
|
||||
|
||||
-- community_follower
|
||||
create or replace function refresh_community_follower()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently community_aggregates_mview;
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_community_follower
|
||||
after insert or update or delete or truncate
|
||||
on community_follower
|
||||
for each statement
|
||||
execute procedure refresh_community_follower();
|
||||
|
||||
-- community_user_ban
|
||||
create or replace function refresh_community_user_ban()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently comment_aggregates_mview;
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_community_user_ban
|
||||
after insert or update or delete or truncate
|
||||
on community_user_ban
|
||||
for each statement
|
||||
execute procedure refresh_community_user_ban();
|
||||
|
||||
-- comment
|
||||
create or replace function refresh_comment()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_aggregates_mview;
|
||||
refresh materialized view concurrently comment_aggregates_mview;
|
||||
refresh materialized view concurrently community_aggregates_mview;
|
||||
refresh materialized view concurrently user_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_comment
|
||||
after insert or update or delete or truncate
|
||||
on comment
|
||||
for each statement
|
||||
execute procedure refresh_comment();
|
||||
|
||||
-- comment_like
|
||||
create or replace function refresh_comment_like()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently comment_aggregates_mview;
|
||||
refresh materialized view concurrently user_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_comment_like
|
||||
after insert or update or delete or truncate
|
||||
on comment_like
|
||||
for each statement
|
||||
execute procedure refresh_comment_like();
|
211
server/migrations_testing/2020-01-13-025151_create_materialized_views/down.sql
vendored
Normal file
211
server/migrations_testing/2020-01-13-025151_create_materialized_views/down.sql
vendored
Normal file
|
@ -0,0 +1,211 @@
|
|||
-- functions and triggers
|
||||
drop trigger refresh_user on user_;
|
||||
drop function refresh_user();
|
||||
drop trigger refresh_post on post;
|
||||
drop function refresh_post();
|
||||
drop trigger refresh_post_like on post_like;
|
||||
drop function refresh_post_like();
|
||||
drop trigger refresh_community on community;
|
||||
drop function refresh_community();
|
||||
drop trigger refresh_community_follower on community_follower;
|
||||
drop function refresh_community_follower();
|
||||
drop trigger refresh_comment on comment;
|
||||
drop function refresh_comment();
|
||||
drop trigger refresh_comment_like on comment_like;
|
||||
drop function refresh_comment_like();
|
||||
|
||||
-- post
|
||||
-- Recreate the view
|
||||
drop materialized 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
|
||||
;
|
||||
|
||||
|
||||
drop materialized 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;
|
||||
|
||||
|
||||
-- community
|
||||
drop materialized 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 materialized 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;
|
||||
|
324
server/migrations_testing/2020-01-13-025151_create_materialized_views/up.sql
vendored
Normal file
324
server/migrations_testing/2020-01-13-025151_create_materialized_views/up.sql
vendored
Normal file
|
@ -0,0 +1,324 @@
|
|||
-- post
|
||||
drop view post_view;
|
||||
create materialized 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
|
||||
with data
|
||||
;
|
||||
|
||||
create unique index idx_post_view_unique on post_view (id, user_id);
|
||||
create index idx_post_view_user_id on post_view (user_id);
|
||||
create index idx_post_view_hot_rank_published on post_view (hot_rank desc, published desc);
|
||||
create index idx_post_view_published on post_view (published desc);
|
||||
create index idx_post_view_score on post_view (score desc);
|
||||
|
||||
-- user_view
|
||||
drop view user_view;
|
||||
create materialized 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;
|
||||
|
||||
create unique index idx_user_view_unique on user_view (id);
|
||||
create index idx_user_view_comment_published on user_view (comment_score desc, published desc);
|
||||
create index idx_user_view_admin on user_view (admin);
|
||||
create index idx_user_view_banned on user_view (banned);
|
||||
|
||||
-- community
|
||||
drop view community_view;
|
||||
create materialized 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
|
||||
;
|
||||
|
||||
create unique index idx_community_view_unique on community_view (id, user_id);
|
||||
create index idx_community_view_user_id on community_view (user_id);
|
||||
create index idx_community_view_hot_rank_subscribed on community_view (hot_rank desc, number_of_subscribers desc);
|
||||
|
||||
|
||||
-- reply and comment view
|
||||
drop view reply_view;
|
||||
drop view user_mention_view;
|
||||
drop view comment_view;
|
||||
create materialized 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 unique index idx_comment_view_unique on comment_view (id, user_id);
|
||||
create index idx_comment_view_user_id on comment_view (user_id);
|
||||
create index idx_comment_view_creator_id on comment_view (creator_id);
|
||||
create index idx_comment_view_post_id on comment_view (post_id);
|
||||
create index idx_comment_view_score on comment_view (score desc);
|
||||
|
||||
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;
|
||||
|
||||
-- user
|
||||
create or replace function refresh_user()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently comment_view; -- cause of bans
|
||||
refresh materialized view concurrently post_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_user
|
||||
after insert or update or delete or truncate
|
||||
on user_
|
||||
for each statement
|
||||
execute procedure refresh_user();
|
||||
|
||||
-- post
|
||||
create or replace function refresh_post()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_post
|
||||
after insert or update or delete or truncate
|
||||
on post
|
||||
for each statement
|
||||
execute procedure refresh_post();
|
||||
|
||||
-- post_like
|
||||
create or replace function refresh_post_like()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_post_like
|
||||
after insert or update or delete or truncate
|
||||
on post_like
|
||||
for each statement
|
||||
execute procedure refresh_post_like();
|
||||
|
||||
-- community
|
||||
create or replace function refresh_community()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_view;
|
||||
refresh materialized view concurrently community_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_community
|
||||
after insert or update or delete or truncate
|
||||
on community
|
||||
for each statement
|
||||
execute procedure refresh_community();
|
||||
|
||||
-- community_follower
|
||||
create or replace function refresh_community_follower()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently community_view;
|
||||
refresh materialized view concurrently post_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_community_follower
|
||||
after insert or update or delete or truncate
|
||||
on community_follower
|
||||
for each statement
|
||||
execute procedure refresh_community_follower();
|
||||
|
||||
-- comment
|
||||
create or replace function refresh_comment()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently post_view;
|
||||
refresh materialized view concurrently comment_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_comment
|
||||
after insert or update or delete or truncate
|
||||
on comment
|
||||
for each statement
|
||||
execute procedure refresh_comment();
|
||||
|
||||
-- comment_like
|
||||
create or replace function refresh_comment_like()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently comment_view;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_comment_like
|
||||
after insert or update or delete or truncate
|
||||
on comment_like
|
||||
for each statement
|
||||
execute procedure refresh_comment_like();
|
25
server/query_testing/apache_bench_report.sh
vendored
Executable file
25
server/query_testing/apache_bench_report.sh
vendored
Executable file
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
|
||||
declare -a arr=(
|
||||
"https://mastodon.social/"
|
||||
"https://peertube.social/"
|
||||
"https://dev.lemmy.ml/"
|
||||
"https://dev.lemmy.ml/feeds/all.xml"
|
||||
"https://dev.lemmy.ml/.well-known/nodeinfo"
|
||||
"https://fediverse.blog/.well-known/nodeinfo"
|
||||
"https://torrents-csv.ml/service/search?q=wheel&page=1&type_=torrent"
|
||||
)
|
||||
|
||||
## now loop through the above array
|
||||
for i in "${arr[@]}"
|
||||
do
|
||||
ab -c 10 -t 10 "$i" > out.abtest
|
||||
grep "Server Hostname:" out.abtest
|
||||
grep "Document Path:" out.abtest
|
||||
grep "Requests per second" out.abtest
|
||||
grep "(mean, across all concurrent requests)" out.abtest
|
||||
grep "Transfer rate:" out.abtest
|
||||
echo "---"
|
||||
done
|
||||
|
||||
rm *.abtest
|
22
server/query_testing/generate_explain_reports.sh
vendored
Executable file
22
server/query_testing/generate_explain_reports.sh
vendored
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Do the views first
|
||||
|
||||
echo "explain (analyze, format json) select * from user_mview" > explain.sql
|
||||
psql -qAt -U lemmy -f explain.sql > user_view.json
|
||||
|
||||
echo "explain (analyze, format json) select * from post_mview where user_id is null order by hot_rank desc, published desc" > explain.sql
|
||||
psql -qAt -U lemmy -f explain.sql > post_view.json
|
||||
|
||||
echo "explain (analyze, format json) select * from comment_mview where user_id is null" > explain.sql
|
||||
psql -qAt -U lemmy -f explain.sql > comment_view.json
|
||||
|
||||
echo "explain (analyze, format json) select * from community_mview where user_id is null order by hot_rank desc" > explain.sql
|
||||
psql -qAt -U lemmy -f explain.sql > community_view.json
|
||||
|
||||
echo "explain (analyze, format json) select * from site_view limit 1" > explain.sql
|
||||
psql -qAt -U lemmy -f explain.sql > site_view.json
|
||||
|
||||
grep "Execution Time" *.json
|
||||
|
||||
rm explain.sql
|
|
@ -1,4 +1,7 @@
|
|||
use super::*;
|
||||
use crate::send_email;
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateComment {
|
||||
|
@ -32,7 +35,6 @@ pub struct SaveComment {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CommentResponse {
|
||||
op: String,
|
||||
pub comment: CommentView,
|
||||
}
|
||||
|
||||
|
@ -45,26 +47,27 @@ pub struct CreateCommentLike {
|
|||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||
fn perform(&self) -> Result<CommentResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
let data: &CreateComment = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
||||
// Check for a community ban
|
||||
let post = Post::read(&conn, data.post_id)?;
|
||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||
|
@ -82,24 +85,20 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
|
||||
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||
};
|
||||
|
||||
// Scan the comment for user mentions, add those rows
|
||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||
|
||||
for username_mention in &extracted_usernames {
|
||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
||||
|
||||
if mention_user.is_ok() {
|
||||
let mention_user_id = mention_user?.id;
|
||||
|
||||
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||
// You can't mention yourself
|
||||
// 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
|
||||
if mention_user_id != user_id {
|
||||
if mention_user.id != user_id {
|
||||
let user_mention_form = UserMentionForm {
|
||||
recipient_id: mention_user_id,
|
||||
recipient_id: mention_user.id,
|
||||
comment_id: inserted_comment.id,
|
||||
read: None,
|
||||
};
|
||||
|
@ -109,10 +108,79 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
match UserMention::create(&conn, &user_mention_form) {
|
||||
Ok(_mention) => (),
|
||||
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
|
||||
let like_form = CommentLikeForm {
|
||||
|
@ -124,26 +192,24 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
|
||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||
Ok(like) => like,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
|
||||
};
|
||||
|
||||
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
op: self.op.to_string(),
|
||||
comment: comment_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<EditComment> {
|
||||
fn perform(&self) -> Result<CommentResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
let data: &EditComment = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -163,17 +229,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||
|
||||
if !editors.contains(&user_id) {
|
||||
return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?;
|
||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,14 +262,14 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
|
||||
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
|
||||
// Scan the comment for user mentions, add those rows
|
||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||
|
||||
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() {
|
||||
let mention_user_id = mention_user?.id;
|
||||
|
@ -242,20 +308,18 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
op: self.op.to_string(),
|
||||
comment: comment_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||
fn perform(&self) -> Result<CommentResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
let data: &SaveComment = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -268,32 +332,30 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
|||
if data.save {
|
||||
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||
};
|
||||
} else {
|
||||
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||
};
|
||||
}
|
||||
|
||||
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
op: self.op.to_string(),
|
||||
comment: comment_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||
fn perform(&self) -> Result<CommentResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
let data: &CreateCommentLike = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -301,20 +363,20 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
// Don't do a downvote if site has downvotes disabled
|
||||
if data.score == -1 {
|
||||
let site = SiteView::read(&conn)?;
|
||||
if site.enable_downvotes == false {
|
||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
||||
if !site.enable_downvotes {
|
||||
return Err(APIError::err("downvotes_disabled").into());
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
let post = Post::read(&conn, data.post_id)?;
|
||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
let like_form = CommentLikeForm {
|
||||
|
@ -328,11 +390,11 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
CommentLike::remove(&conn, &like_form)?;
|
||||
|
||||
// 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 {
|
||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||
Ok(like) => like,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -340,7 +402,6 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
op: self.op.to_string(),
|
||||
comment: liked_comment,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -10,7 +11,6 @@ pub struct GetCommunity {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetCommunityResponse {
|
||||
op: String,
|
||||
community: CommunityView,
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
admins: Vec<UserView>,
|
||||
|
@ -28,7 +28,6 @@ pub struct CreateCommunity {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CommunityResponse {
|
||||
op: String,
|
||||
pub community: CommunityView,
|
||||
}
|
||||
|
||||
|
@ -42,7 +41,6 @@ pub struct ListCommunities {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListCommunitiesResponse {
|
||||
op: String,
|
||||
communities: Vec<CommunityView>,
|
||||
}
|
||||
|
||||
|
@ -58,7 +56,6 @@ pub struct BanFromCommunity {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BanFromCommunityResponse {
|
||||
op: String,
|
||||
user: UserView,
|
||||
banned: bool,
|
||||
}
|
||||
|
@ -73,7 +70,6 @@ pub struct AddModToCommunity {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AddModToCommunityResponse {
|
||||
op: String,
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
}
|
||||
|
||||
|
@ -106,7 +102,6 @@ pub struct GetFollowedCommunities {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetFollowedCommunitiesResponse {
|
||||
op: String,
|
||||
communities: Vec<CommunityFollowerView>,
|
||||
}
|
||||
|
||||
|
@ -118,9 +113,8 @@ pub struct TransferCommunity {
|
|||
}
|
||||
|
||||
impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||
fn perform(&self) -> Result<GetCommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||
let data: &GetCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
|
@ -136,21 +130,24 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
let community_id = match data.id {
|
||||
Some(id) => id,
|
||||
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,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
};
|
||||
|
||||
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
||||
Ok(moderators) => moderators,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
};
|
||||
|
||||
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||
|
@ -161,7 +158,6 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
|
||||
// Return the jwt
|
||||
Ok(GetCommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
community: community_view,
|
||||
moderators,
|
||||
admins,
|
||||
|
@ -170,27 +166,26 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||
fn perform(&self) -> Result<CommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
let data: &CreateCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
if has_slurs(&data.name)
|
||||
|| has_slurs(&data.title)
|
||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||
{
|
||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
||||
return Err(APIError::err("no_slurs").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// When you create a community, make sure the user becomes a moderator and a follower
|
||||
|
@ -208,7 +203,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
|||
|
||||
let inserted_community = match Community::create(&conn, &community_form) {
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("community_already_exists").into()),
|
||||
};
|
||||
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
|
@ -219,12 +214,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
|||
let _inserted_community_moderator =
|
||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"community_moderator_already_exists",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||
};
|
||||
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
|
@ -235,38 +225,35 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
|||
let _inserted_community_follower =
|
||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||
};
|
||||
|
||||
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
||||
|
||||
Ok(CommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
community: community_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||
fn perform(&self) -> Result<CommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
let data: &EditCommunity = &self.data;
|
||||
|
||||
if has_slurs(&data.name) || has_slurs(&data.title) {
|
||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
||||
return Err(APIError::err("no_slurs").into());
|
||||
}
|
||||
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
// Verify its a mod
|
||||
|
@ -279,7 +266,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
|||
);
|
||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||
if !editors.contains(&user_id) {
|
||||
return Err(APIError::err(&self.op, "no_community_edit_allowed"))?;
|
||||
return Err(APIError::err("no_community_edit_allowed").into());
|
||||
}
|
||||
|
||||
let community_form = CommunityForm {
|
||||
|
@ -296,7 +283,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
|||
|
||||
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
|
@ -318,16 +305,14 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
|||
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(CommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
community: community_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||
fn perform(&self) -> Result<ListCommunitiesResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<ListCommunitiesResponse, Error> {
|
||||
let data: &ListCommunities = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
|
@ -351,28 +336,24 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
|||
|
||||
let communities = CommunityQueryBuilder::create(&conn)
|
||||
.sort(&sort)
|
||||
.from_user_id(user_id)
|
||||
.for_user(user_id)
|
||||
.show_nsfw(show_nsfw)
|
||||
.page(data.page)
|
||||
.limit(data.limit)
|
||||
.list()?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(ListCommunitiesResponse {
|
||||
op: self.op.to_string(),
|
||||
communities,
|
||||
})
|
||||
Ok(ListCommunitiesResponse { communities })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||
fn perform(&self) -> Result<CommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
let data: &FollowCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -385,32 +366,30 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
|||
if data.follow {
|
||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||
};
|
||||
} else {
|
||||
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||
};
|
||||
}
|
||||
|
||||
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
|
||||
|
||||
Ok(CommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
community: community_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||
fn perform(&self) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||
let data: &GetFollowedCommunities = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -418,25 +397,21 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
|||
let communities: Vec<CommunityFollowerView> =
|
||||
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||
Ok(communities) => communities,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "system_err_login"))?,
|
||||
Err(_e) => return Err(APIError::err("system_err_login").into()),
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetFollowedCommunitiesResponse {
|
||||
op: self.op.to_string(),
|
||||
communities,
|
||||
})
|
||||
Ok(GetFollowedCommunitiesResponse { communities })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||
fn perform(&self) -> Result<BanFromCommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<BanFromCommunityResponse, Error> {
|
||||
let data: &BanFromCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -449,12 +424,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
|||
if data.ban {
|
||||
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
||||
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||
};
|
||||
} else {
|
||||
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
||||
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -477,7 +452,6 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
|||
let user_view = UserView::read(&conn, data.user_id)?;
|
||||
|
||||
Ok(BanFromCommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
user: user_view,
|
||||
banned: data.ban,
|
||||
})
|
||||
|
@ -485,13 +459,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
|||
}
|
||||
|
||||
impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||
fn perform(&self) -> Result<AddModToCommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<AddModToCommunityResponse, Error> {
|
||||
let data: &AddModToCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -504,22 +477,12 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
|||
if data.added {
|
||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"community_moderator_already_exists",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||
};
|
||||
} else {
|
||||
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"community_moderator_already_exists",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -534,21 +497,17 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
|||
|
||||
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
||||
|
||||
Ok(AddModToCommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
moderators,
|
||||
})
|
||||
Ok(AddModToCommunityResponse { moderators })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||
fn perform(&self) -> Result<GetCommunityResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||
let data: &TransferCommunity = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -562,14 +521,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
|||
admins.insert(0, creator_user);
|
||||
|
||||
// Make sure user is the creator, or an admin
|
||||
if user_id != read_community.creator_id
|
||||
&& !admins
|
||||
.iter()
|
||||
.map(|a| a.id)
|
||||
.collect::<Vec<i32>>()
|
||||
.contains(&user_id)
|
||||
{
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let community_form = CommunityForm {
|
||||
|
@ -586,7 +539,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
|||
|
||||
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||
};
|
||||
|
||||
// You also have to re-do the community_moderator table, reordering it.
|
||||
|
@ -609,12 +562,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
|||
let _inserted_community_moderator =
|
||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"community_moderator_already_exists",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -629,17 +577,16 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
|||
|
||||
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
||||
Ok(community) => community,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
};
|
||||
|
||||
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
||||
Ok(moderators) => moderators,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetCommunityResponse {
|
||||
op: self.op.to_string(),
|
||||
community: community_view,
|
||||
moderators,
|
||||
admins,
|
||||
|
|
|
@ -15,7 +15,8 @@ use crate::db::user_mention::*;
|
|||
use crate::db::user_mention_view::*;
|
||||
use crate::db::user_view::*;
|
||||
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 diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -25,78 +26,32 @@ pub mod post;
|
|||
pub mod site;
|
||||
pub mod user;
|
||||
|
||||
#[derive(EnumString, ToString, Debug)]
|
||||
pub enum UserOperation {
|
||||
Login,
|
||||
Register,
|
||||
CreateCommunity,
|
||||
CreatePost,
|
||||
ListCommunities,
|
||||
ListCategories,
|
||||
GetPost,
|
||||
GetCommunity,
|
||||
CreateComment,
|
||||
EditComment,
|
||||
SaveComment,
|
||||
CreateCommentLike,
|
||||
GetPosts,
|
||||
CreatePostLike,
|
||||
EditPost,
|
||||
SavePost,
|
||||
EditCommunity,
|
||||
FollowCommunity,
|
||||
GetFollowedCommunities,
|
||||
GetUserDetails,
|
||||
GetReplies,
|
||||
GetUserMentions,
|
||||
EditUserMention,
|
||||
GetModlog,
|
||||
BanFromCommunity,
|
||||
AddModToCommunity,
|
||||
CreateSite,
|
||||
EditSite,
|
||||
GetSite,
|
||||
AddAdmin,
|
||||
BanUser,
|
||||
Search,
|
||||
MarkAllAsRead,
|
||||
SaveUserSettings,
|
||||
TransferCommunity,
|
||||
TransferSite,
|
||||
DeleteAccount,
|
||||
PasswordReset,
|
||||
PasswordChange,
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display = "{{\"op\":\"{}\", \"error\":\"{}\"}}", op, message)]
|
||||
#[fail(display = "{{\"error\":\"{}\"}}", message)]
|
||||
pub struct APIError {
|
||||
pub op: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl APIError {
|
||||
pub fn err(op: &UserOperation, msg: &str) -> Self {
|
||||
pub fn err(msg: &str) -> Self {
|
||||
APIError {
|
||||
op: op.to_string(),
|
||||
message: msg.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Oper<T> {
|
||||
op: UserOperation,
|
||||
data: T,
|
||||
}
|
||||
|
||||
impl<T> Oper<T> {
|
||||
pub fn new(op: UserOperation, data: T) -> Oper<T> {
|
||||
Oper { op, data }
|
||||
pub fn new(data: T) -> Oper<T> {
|
||||
Oper { data }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Perform<T> {
|
||||
fn perform(&self) -> Result<T, Error>
|
||||
fn perform(&self, conn: &PgConnection) -> Result<T, Error>
|
||||
where
|
||||
T: Sized;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -13,7 +14,6 @@ pub struct CreatePost {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PostResponse {
|
||||
op: String,
|
||||
pub post: PostView,
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,6 @@ pub struct GetPost {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetPostResponse {
|
||||
op: String,
|
||||
post: PostView,
|
||||
comments: Vec<CommentView>,
|
||||
community: CommunityView,
|
||||
|
@ -45,7 +44,6 @@ pub struct GetPosts {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetPostsResponse {
|
||||
op: String,
|
||||
posts: Vec<PostView>,
|
||||
}
|
||||
|
||||
|
@ -58,7 +56,6 @@ pub struct CreatePostLike {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreatePostLikeResponse {
|
||||
op: String,
|
||||
post: PostView,
|
||||
}
|
||||
|
||||
|
@ -87,29 +84,28 @@ pub struct SavePost {
|
|||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<CreatePost> {
|
||||
fn perform(&self) -> Result<PostResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
let data: &CreatePost = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
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("no_slurs").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Check for a community ban
|
||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
let post_form = PostForm {
|
||||
|
@ -128,7 +124,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
|
||||
let inserted_post = match Post::create(&conn, &post_form) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||
};
|
||||
|
||||
// They like their own post by default
|
||||
|
@ -141,26 +137,22 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
// Only add the like if the score isnt 0
|
||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||
Ok(like) => like,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||
};
|
||||
|
||||
// Refetch the view
|
||||
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
};
|
||||
|
||||
Ok(PostResponse {
|
||||
op: self.op.to_string(),
|
||||
post: post_view,
|
||||
})
|
||||
Ok(PostResponse { post: post_view })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||
fn perform(&self) -> Result<GetPostResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetPostResponse, Error> {
|
||||
let data: &GetPost = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
|
@ -175,7 +167,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
|
||||
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
};
|
||||
|
||||
let comments = CommentQueryBuilder::create(&conn)
|
||||
|
@ -196,7 +188,6 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
|
||||
// Return the jwt
|
||||
Ok(GetPostResponse {
|
||||
op: self.op.to_string(),
|
||||
post: post_view,
|
||||
comments,
|
||||
community,
|
||||
|
@ -207,9 +198,8 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
}
|
||||
|
||||
impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||
fn perform(&self) -> Result<GetPostsResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetPostsResponse, Error> {
|
||||
let data: &GetPosts = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
|
@ -243,24 +233,20 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
|||
.list()
|
||||
{
|
||||
Ok(posts) => posts,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
|
||||
};
|
||||
|
||||
Ok(GetPostsResponse {
|
||||
op: self.op.to_string(),
|
||||
posts,
|
||||
})
|
||||
Ok(GetPostsResponse { posts })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||
fn perform(&self) -> Result<CreatePostLikeResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CreatePostLikeResponse, Error> {
|
||||
let data: &CreatePostLike = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -268,20 +254,20 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
|||
// Don't do a downvote if site has downvotes disabled
|
||||
if data.score == -1 {
|
||||
let site = SiteView::read(&conn)?;
|
||||
if site.enable_downvotes == false {
|
||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
||||
if !site.enable_downvotes {
|
||||
return Err(APIError::err("downvotes_disabled").into());
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
let post = Post::read(&conn, data.post_id)?;
|
||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
let like_form = PostLikeForm {
|
||||
|
@ -294,39 +280,34 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
|||
PostLike::remove(&conn, &like_form)?;
|
||||
|
||||
// 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 {
|
||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||
Ok(like) => like,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||
};
|
||||
}
|
||||
|
||||
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
};
|
||||
|
||||
// just output the score
|
||||
Ok(CreatePostLikeResponse {
|
||||
op: self.op.to_string(),
|
||||
post: post_view,
|
||||
})
|
||||
Ok(CreatePostLikeResponse { post: post_view })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<EditPost> {
|
||||
fn perform(&self) -> Result<PostResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
let data: &EditPost = &self.data;
|
||||
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("no_slurs").into());
|
||||
}
|
||||
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -341,17 +322,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
);
|
||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||
if !editors.contains(&user_id) {
|
||||
return Err(APIError::err(&self.op, "no_post_edit_allowed"))?;
|
||||
return Err(APIError::err("no_post_edit_allowed").into());
|
||||
}
|
||||
|
||||
// Check for a community ban
|
||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
||||
return Err(APIError::err("community_ban").into());
|
||||
}
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
||||
return Err(APIError::err("site_ban").into());
|
||||
}
|
||||
|
||||
let post_form = PostForm {
|
||||
|
@ -370,7 +351,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
|
||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
|
@ -404,21 +385,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
|
||||
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(PostResponse {
|
||||
op: self.op.to_string(),
|
||||
post: post_view,
|
||||
})
|
||||
Ok(PostResponse { post: post_view })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<SavePost> {
|
||||
fn perform(&self) -> Result<PostResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
let data: &SavePost = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -431,20 +408,17 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
|||
if data.save {
|
||||
match PostSaved::save(&conn, &post_saved_form) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||
};
|
||||
} else {
|
||||
match PostSaved::unsave(&conn, &post_saved_form) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||
};
|
||||
}
|
||||
|
||||
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
|
||||
|
||||
Ok(PostResponse {
|
||||
op: self.op.to_string(),
|
||||
post: post_view,
|
||||
})
|
||||
Ok(PostResponse { post: post_view })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -6,7 +7,6 @@ pub struct ListCategories;
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListCategoriesResponse {
|
||||
op: String,
|
||||
categories: Vec<Category>,
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,6 @@ pub struct Search {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
op: String,
|
||||
type_: String,
|
||||
comments: Vec<CommentView>,
|
||||
posts: Vec<PostView>,
|
||||
|
@ -40,7 +39,6 @@ pub struct GetModlog {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetModlogResponse {
|
||||
op: String,
|
||||
removed_posts: Vec<ModRemovePostView>,
|
||||
locked_posts: Vec<ModLockPostView>,
|
||||
stickied_posts: Vec<ModStickyPostView>,
|
||||
|
@ -77,13 +75,11 @@ pub struct GetSite;
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SiteResponse {
|
||||
op: String,
|
||||
site: SiteView,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSiteResponse {
|
||||
op: String,
|
||||
site: Option<SiteView>,
|
||||
admins: Vec<UserView>,
|
||||
banned: Vec<UserView>,
|
||||
|
@ -97,24 +93,19 @@ pub struct TransferSite {
|
|||
}
|
||||
|
||||
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
|
||||
fn perform(&self) -> Result<ListCategoriesResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
|
||||
let _data: &ListCategories = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let categories: Vec<Category> = Category::list_all(&conn)?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(ListCategoriesResponse {
|
||||
op: self.op.to_string(),
|
||||
categories,
|
||||
})
|
||||
Ok(ListCategoriesResponse { categories })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||
fn perform(&self) -> Result<GetModlogResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetModlogResponse, Error> {
|
||||
let data: &GetModlog = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let removed_posts = ModRemovePostView::list(
|
||||
&conn,
|
||||
|
@ -160,20 +151,18 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
|||
)?;
|
||||
|
||||
// These arrays are only for the full modlog, when a community isn't given
|
||||
let mut removed_communities = Vec::new();
|
||||
let mut banned = Vec::new();
|
||||
let mut added = Vec::new();
|
||||
|
||||
if data.community_id.is_none() {
|
||||
removed_communities =
|
||||
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
||||
banned = ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
||||
added = ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
||||
}
|
||||
let (removed_communities, banned, added) = if data.community_id.is_none() {
|
||||
(
|
||||
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetModlogResponse {
|
||||
op: self.op.to_string(),
|
||||
removed_posts,
|
||||
locked_posts,
|
||||
stickied_posts,
|
||||
|
@ -188,26 +177,25 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
|||
}
|
||||
|
||||
impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||
fn perform(&self) -> Result<SiteResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||
let data: &CreateSite = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
if has_slurs(&data.name)
|
||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||
{
|
||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
||||
return Err(APIError::err("no_slurs").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let site_form = SiteForm {
|
||||
|
@ -222,39 +210,35 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
|||
|
||||
match Site::create(&conn, &site_form) {
|
||||
Ok(site) => site,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("site_already_exists").into()),
|
||||
};
|
||||
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
|
||||
Ok(SiteResponse {
|
||||
op: self.op.to_string(),
|
||||
site: site_view,
|
||||
})
|
||||
Ok(SiteResponse { site: site_view })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<SiteResponse> for Oper<EditSite> {
|
||||
fn perform(&self) -> Result<SiteResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||
let data: &EditSite = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
if has_slurs(&data.name)
|
||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||
{
|
||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
||||
return Err(APIError::err("no_slurs").into());
|
||||
}
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
if UserView::read(&conn, user_id)?.admin == false {
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let found_site = Site::read(&conn, 1)?;
|
||||
|
@ -271,22 +255,18 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
|||
|
||||
match Site::update(&conn, 1, &site_form) {
|
||||
Ok(site) => site,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||
};
|
||||
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
|
||||
Ok(SiteResponse {
|
||||
op: self.op.to_string(),
|
||||
site: site_view,
|
||||
})
|
||||
Ok(SiteResponse { site: site_view })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetSiteResponse> for Oper<GetSite> {
|
||||
fn perform(&self) -> Result<GetSiteResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||
let _data: &GetSite = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
// It can return a null site in order to redirect
|
||||
let site_view = match Site::read(&conn, 1) {
|
||||
|
@ -305,7 +285,6 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
|||
let banned = UserView::banned(&conn)?;
|
||||
|
||||
Ok(GetSiteResponse {
|
||||
op: self.op.to_string(),
|
||||
site: site_view,
|
||||
admins,
|
||||
banned,
|
||||
|
@ -315,9 +294,8 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
|||
}
|
||||
|
||||
impl Perform<SearchResponse> for Oper<Search> {
|
||||
fn perform(&self) -> Result<SearchResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SearchResponse, Error> {
|
||||
let data: &Search = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
let type_ = SearchType::from_str(&data.type_)?;
|
||||
|
@ -409,7 +387,6 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
|
||||
// Return the jwt
|
||||
Ok(SearchResponse {
|
||||
op: self.op.to_string(),
|
||||
type_: data.type_.to_owned(),
|
||||
comments,
|
||||
posts,
|
||||
|
@ -420,13 +397,12 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
}
|
||||
|
||||
impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||
fn perform(&self) -> Result<GetSiteResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||
let data: &TransferSite = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -435,7 +411,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
|||
|
||||
// Make sure user is the creator
|
||||
if read_site.creator_id != user_id {
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let site_form = SiteForm {
|
||||
|
@ -450,7 +426,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
|||
|
||||
match Site::update(&conn, 1, &site_form) {
|
||||
Ok(site) => site,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
|
@ -475,7 +451,6 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
|||
let banned = UserView::banned(&conn)?;
|
||||
|
||||
Ok(GetSiteResponse {
|
||||
op: self.op.to_string(),
|
||||
site: Some(site_view),
|
||||
admins,
|
||||
banned,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use super::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::{generate_random_string, send_email};
|
||||
use bcrypt::verify;
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -26,12 +28,18 @@ pub struct SaveUserSettings {
|
|||
default_sort_type: i16,
|
||||
default_listing_type: i16,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LoginResponse {
|
||||
op: String,
|
||||
jwt: String,
|
||||
}
|
||||
|
||||
|
@ -49,7 +57,6 @@ pub struct GetUserDetails {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetUserDetailsResponse {
|
||||
op: String,
|
||||
user: UserView,
|
||||
follows: Vec<CommunityFollowerView>,
|
||||
moderates: Vec<CommunityModeratorView>,
|
||||
|
@ -60,13 +67,11 @@ pub struct GetUserDetailsResponse {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetRepliesResponse {
|
||||
op: String,
|
||||
replies: Vec<ReplyView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetUserMentionsResponse {
|
||||
op: String,
|
||||
mentions: Vec<UserMentionView>,
|
||||
}
|
||||
|
||||
|
@ -84,7 +89,6 @@ pub struct AddAdmin {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AddAdminResponse {
|
||||
op: String,
|
||||
admins: Vec<UserView>,
|
||||
}
|
||||
|
||||
|
@ -99,7 +103,6 @@ pub struct BanUser {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BanUserResponse {
|
||||
op: String,
|
||||
user: UserView,
|
||||
banned: bool,
|
||||
}
|
||||
|
@ -131,7 +134,6 @@ pub struct EditUserMention {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct UserMentionResponse {
|
||||
op: String,
|
||||
mention: UserMentionView,
|
||||
}
|
||||
|
||||
|
@ -147,9 +149,7 @@ pub struct PasswordReset {
|
|||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PasswordResetResponse {
|
||||
op: String,
|
||||
}
|
||||
pub struct PasswordResetResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PasswordChange {
|
||||
|
@ -159,66 +159,57 @@ pub struct PasswordChange {
|
|||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<Login> {
|
||||
fn perform(&self) -> Result<LoginResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &Login = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
// Fetch that username / email
|
||||
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"couldnt_find_that_username_or_email",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||
};
|
||||
|
||||
// Verify the password
|
||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||
if !valid {
|
||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
||||
return Err(APIError::err("password_incorrect").into());
|
||||
}
|
||||
|
||||
// Return the jwt
|
||||
Ok(LoginResponse {
|
||||
op: self.op.to_string(),
|
||||
jwt: user.jwt(),
|
||||
})
|
||||
Ok(LoginResponse { jwt: user.jwt() })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<Register> {
|
||||
fn perform(&self) -> Result<LoginResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &Register = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
// Make sure site has open registration
|
||||
if let Ok(site) = SiteView::read(&conn) {
|
||||
if !site.open_registration {
|
||||
return Err(APIError::err(&self.op, "registration_closed"))?;
|
||||
return Err(APIError::err("registration_closed").into());
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure passwords match
|
||||
if &data.password != &data.password_verify {
|
||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
||||
if data.password != data.password_verify {
|
||||
return Err(APIError::err("passwords_dont_match").into());
|
||||
}
|
||||
|
||||
if has_slurs(&data.username) {
|
||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
||||
return Err(APIError::err("no_slurs").into());
|
||||
}
|
||||
|
||||
// Make sure there are no admins
|
||||
if data.admin && UserView::admins(&conn)?.len() > 0 {
|
||||
return Err(APIError::err(&self.op, "admin_already_created"))?;
|
||||
if data.admin && !UserView::admins(&conn)?.is_empty() {
|
||||
return Err(APIError::err("admin_already_created").into());
|
||||
}
|
||||
|
||||
// Register the new user
|
||||
let user_form = UserForm {
|
||||
name: data.username.to_owned(),
|
||||
fedi_name: Settings::get().hostname.into(),
|
||||
fedi_name: Settings::get().hostname.to_owned(),
|
||||
email: data.email.to_owned(),
|
||||
avatar: None,
|
||||
password_encrypted: data.password.to_owned(),
|
||||
preferred_username: None,
|
||||
updated: None,
|
||||
|
@ -229,12 +220,24 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
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,
|
||||
};
|
||||
|
||||
// Create the user
|
||||
let inserted_user = match User_::register(&conn, &user_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?,
|
||||
Err(e) => {
|
||||
let err_type = if e.to_string()
|
||||
== "duplicate key value violates unique constraint \"user__email_key\""
|
||||
{
|
||||
"email_already_exists"
|
||||
} else {
|
||||
"user_already_exists"
|
||||
};
|
||||
|
||||
return Err(APIError::err(err_type).into());
|
||||
}
|
||||
};
|
||||
|
||||
// Create the main community if it doesn't exist
|
||||
|
@ -265,7 +268,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
let _inserted_community_follower =
|
||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
||||
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||
};
|
||||
|
||||
// If its an admin, add them as a mod and follower to main
|
||||
|
@ -278,42 +281,69 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
let _inserted_community_moderator =
|
||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"community_moderator_already_exists",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||
};
|
||||
}
|
||||
|
||||
// Return the jwt
|
||||
Ok(LoginResponse {
|
||||
op: self.op.to_string(),
|
||||
jwt: inserted_user.jwt(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||
fn perform(&self) -> Result<LoginResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &SaveUserSettings = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.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("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("password_incorrect").into());
|
||||
}
|
||||
User_::update_password(&conn, user_id, &new_password)?.password_encrypted
|
||||
}
|
||||
None => return Err(APIError::err("password_incorrect").into()),
|
||||
}
|
||||
}
|
||||
None => return Err(APIError::err("passwords_dont_match").into()),
|
||||
}
|
||||
}
|
||||
None => read_user.password_encrypted,
|
||||
};
|
||||
|
||||
let user_form = UserForm {
|
||||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
email,
|
||||
avatar: data.avatar.to_owned(),
|
||||
password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
admin: read_user.admin,
|
||||
|
@ -323,25 +353,35 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
default_sort_type: data.default_sort_type,
|
||||
default_listing_type: data.default_listing_type,
|
||||
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) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
||||
Err(e) => {
|
||||
let err_type = if e.to_string()
|
||||
== "duplicate key value violates unique constraint \"user__email_key\""
|
||||
{
|
||||
"email_already_exists"
|
||||
} else {
|
||||
"user_already_exists"
|
||||
};
|
||||
|
||||
return Err(APIError::err(err_type).into());
|
||||
}
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(LoginResponse {
|
||||
op: self.op.to_string(),
|
||||
jwt: updated_user.jwt(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||
fn perform(&self) -> Result<GetUserDetailsResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetUserDetailsResponse, Error> {
|
||||
let data: &GetUserDetails = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
|
@ -366,11 +406,16 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
let user_details_id = match data.user_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
User_::read_from_name(
|
||||
match User_::read_from_name(
|
||||
&conn,
|
||||
data.username.to_owned().unwrap_or("admin".to_string()),
|
||||
)?
|
||||
.id
|
||||
data
|
||||
.username
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| "admin".to_string()),
|
||||
) {
|
||||
Ok(user) => user.id,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -412,7 +457,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
|
||||
// Return the jwt
|
||||
Ok(GetUserDetailsResponse {
|
||||
op: self.op.to_string(),
|
||||
user: user_view,
|
||||
follows,
|
||||
moderates,
|
||||
|
@ -424,20 +468,19 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
}
|
||||
|
||||
impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||
fn perform(&self) -> Result<AddAdminResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<AddAdminResponse, Error> {
|
||||
let data: &AddAdmin = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
if UserView::read(&conn, user_id)?.admin == false {
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let read_user = User_::read(&conn, data.user_id)?;
|
||||
|
@ -446,6 +489,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
|
@ -456,11 +500,13 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
default_sort_type: read_user.default_sort_type,
|
||||
default_listing_type: read_user.default_listing_type,
|
||||
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) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
|
@ -478,28 +524,24 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
Ok(AddAdminResponse {
|
||||
op: self.op.to_string(),
|
||||
admins,
|
||||
})
|
||||
Ok(AddAdminResponse { admins })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||
fn perform(&self) -> Result<BanUserResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<BanUserResponse, Error> {
|
||||
let data: &BanUser = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
// Make sure user is an admin
|
||||
if UserView::read(&conn, user_id)?.admin == false {
|
||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let read_user = User_::read(&conn, data.user_id)?;
|
||||
|
@ -508,6 +550,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
|
@ -518,11 +561,13 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
default_sort_type: read_user.default_sort_type,
|
||||
default_listing_type: read_user.default_listing_type,
|
||||
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) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
|
@ -544,7 +589,6 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
let user_view = UserView::read(&conn, data.user_id)?;
|
||||
|
||||
Ok(BanUserResponse {
|
||||
op: self.op.to_string(),
|
||||
user: user_view,
|
||||
banned: data.ban,
|
||||
})
|
||||
|
@ -552,13 +596,12 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
}
|
||||
|
||||
impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetRepliesResponse, Error> {
|
||||
let data: &GetReplies = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -572,21 +615,17 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
|||
.limit(data.limit)
|
||||
.list()?;
|
||||
|
||||
Ok(GetRepliesResponse {
|
||||
op: self.op.to_string(),
|
||||
replies,
|
||||
})
|
||||
Ok(GetRepliesResponse { replies })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||
fn perform(&self) -> Result<GetUserMentionsResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetUserMentionsResponse, Error> {
|
||||
let data: &GetUserMentions = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -600,21 +639,17 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
|||
.limit(data.limit)
|
||||
.list()?;
|
||||
|
||||
Ok(GetUserMentionsResponse {
|
||||
op: self.op.to_string(),
|
||||
mentions,
|
||||
})
|
||||
Ok(GetUserMentionsResponse { mentions })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||
fn perform(&self) -> Result<UserMentionResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<UserMentionResponse, Error> {
|
||||
let data: &EditUserMention = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -630,26 +665,24 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
|||
let _updated_user_mention =
|
||||
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
|
||||
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||
|
||||
Ok(UserMentionResponse {
|
||||
op: self.op.to_string(),
|
||||
mention: user_mention_view,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||
fn perform(&self) -> Result<GetRepliesResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetRepliesResponse, Error> {
|
||||
let data: &MarkAllAsRead = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -674,7 +707,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
|||
|
||||
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -695,25 +728,21 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
|||
let _updated_mention =
|
||||
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||
Ok(mention) => mention,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(GetRepliesResponse {
|
||||
op: self.op.to_string(),
|
||||
replies: vec![],
|
||||
})
|
||||
Ok(GetRepliesResponse { replies: vec![] })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||
fn perform(&self) -> Result<LoginResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &DeleteAccount = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
Ok(claims) => claims.claims,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
@ -723,7 +752,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
// Verify the password
|
||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||
if !valid {
|
||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
||||
return Err(APIError::err("password_incorrect").into());
|
||||
}
|
||||
|
||||
// Comments
|
||||
|
@ -746,7 +775,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
|
||||
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
||||
Ok(comment) => comment,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -774,31 +803,24 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
|
||||
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(LoginResponse {
|
||||
op: self.op.to_string(),
|
||||
jwt: data.auth.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||
fn perform(&self) -> Result<PasswordResetResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PasswordResetResponse, Error> {
|
||||
let data: &PasswordReset = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
// Fetch that email
|
||||
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => {
|
||||
return Err(APIError::err(
|
||||
&self.op,
|
||||
"couldnt_find_that_username_or_email",
|
||||
))?
|
||||
}
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||
};
|
||||
|
||||
// Generate a random token
|
||||
|
@ -815,56 +837,33 @@ 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);
|
||||
match send_email(subject, user_email, &user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?,
|
||||
Err(_e) => return Err(APIError::err(&_e).into()),
|
||||
};
|
||||
|
||||
Ok(PasswordResetResponse {
|
||||
op: self.op.to_string(),
|
||||
})
|
||||
Ok(PasswordResetResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<PasswordChange> {
|
||||
fn perform(&self) -> Result<LoginResponse, Error> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
let data: &PasswordChange = &self.data;
|
||||
let conn = establish_connection();
|
||||
|
||||
// Fetch the user_id from the token
|
||||
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
||||
|
||||
// Make sure passwords match
|
||||
if &data.password != &data.password_verify {
|
||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
||||
if data.password != data.password_verify {
|
||||
return Err(APIError::err("passwords_dont_match").into());
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
let read_user = User_::read(&conn, user_id)?;
|
||||
|
||||
// Update the user with the new password
|
||||
let user_form = UserForm {
|
||||
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) {
|
||||
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
|
||||
Ok(user) => user,
|
||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
||||
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(LoginResponse {
|
||||
op: self.op.to_string(),
|
||||
jwt: updated_user.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);
|
||||
}
|
||||
}
|
109
server/src/apub/community.rs
Normal file
109
server/src/apub/community.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::community::Community;
|
||||
use crate::db::community_view::CommunityFollowerView;
|
||||
use crate::db::establish_unpooled_connection;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Group, collection::UnorderedCollection, context};
|
||||
use actix_web::body::Body;
|
||||
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_unpooled_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_unpooled_connection();
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.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_unpooled_connection();
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.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
|
||||
}
|
||||
}
|
74
server/src/apub/user.rs
Normal file
74
server/src/apub/user.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::establish_unpooled_connection;
|
||||
use crate::db::user::User_;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Person, context};
|
||||
use actix_web::body::Body;
|
||||
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_unpooled_connection();
|
||||
|
||||
if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.body(serde_json::to_string(&user.as_person()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let categories = Category::list_all(&conn).unwrap();
|
||||
let expected_first_category = Category {
|
||||
|
|
|
@ -166,7 +166,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "terry".into(),
|
||||
|
@ -174,6 +174,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -182,6 +183,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -18,6 +18,33 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
saved -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_mview (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
community_id -> Int4,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -46,6 +73,7 @@ pub struct CommentView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -56,7 +84,7 @@ pub struct CommentView {
|
|||
|
||||
pub struct CommentQueryBuilder<'a> {
|
||||
conn: &'a PgConnection,
|
||||
query: super::comment_view::comment_view::BoxedQuery<'a, Pg>,
|
||||
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
||||
sort: &'a SortType,
|
||||
for_post_id: Option<i32>,
|
||||
for_creator_id: Option<i32>,
|
||||
|
@ -69,9 +97,9 @@ pub struct CommentQueryBuilder<'a> {
|
|||
|
||||
impl<'a> CommentQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection) -> Self {
|
||||
use super::comment_view::comment_view::dsl::*;
|
||||
use super::comment_view::comment_mview::dsl::*;
|
||||
|
||||
let query = comment_view.into_boxed();
|
||||
let query = comment_mview.into_boxed();
|
||||
|
||||
CommentQueryBuilder {
|
||||
conn,
|
||||
|
@ -128,7 +156,7 @@ impl<'a> CommentQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<CommentView>, Error> {
|
||||
use super::comment_view::comment_view::dsl::*;
|
||||
use super::comment_view::comment_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
|
@ -226,6 +254,7 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -255,6 +284,7 @@ pub struct ReplyView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -360,7 +390,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "timmy".into(),
|
||||
|
@ -368,6 +398,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -376,6 +407,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -447,6 +480,7 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
upvotes: 1,
|
||||
|
@ -470,6 +504,7 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
upvotes: 1,
|
||||
|
|
|
@ -68,6 +68,10 @@ impl Community {
|
|||
.filter(name.eq(community_name))
|
||||
.first::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> String {
|
||||
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||
|
@ -208,7 +212,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "bobbee".into(),
|
||||
|
@ -216,6 +220,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -224,6 +229,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::community_view::community_view::BoxedQuery;
|
||||
use super::community_view::community_mview::BoxedQuery;
|
||||
use super::*;
|
||||
use diesel::pg::Pg;
|
||||
|
||||
|
@ -16,6 +16,32 @@ table! {
|
|||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
number_of_comments -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
community_mview (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
title -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Int4,
|
||||
creator_id -> Int4,
|
||||
removed -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
|
@ -33,6 +59,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +71,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +83,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +105,7 @@ pub struct CommunityView {
|
|||
pub deleted: bool,
|
||||
pub nsfw: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub category_name: String,
|
||||
pub number_of_subscribers: i64,
|
||||
pub number_of_posts: i64,
|
||||
|
@ -98,9 +128,9 @@ pub struct CommunityQueryBuilder<'a> {
|
|||
|
||||
impl<'a> CommunityQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection) -> Self {
|
||||
use super::community_view::community_view::dsl::*;
|
||||
use super::community_view::community_mview::dsl::*;
|
||||
|
||||
let query = community_view.into_boxed();
|
||||
let query = community_mview.into_boxed();
|
||||
|
||||
CommunityQueryBuilder {
|
||||
conn,
|
||||
|
@ -119,7 +149,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
|||
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
|
||||
}
|
||||
|
@ -145,7 +175,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<CommunityView>, Error> {
|
||||
use super::community_view::community_view::dsl::*;
|
||||
use super::community_view::community_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
|
@ -224,6 +254,7 @@ pub struct CommunityModeratorView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
@ -253,6 +284,7 @@ pub struct CommunityFollowerView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
@ -282,6 +314,7 @@ pub struct CommunityUserBanView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::Settings;
|
||||
extern crate lazy_static;
|
||||
use crate::settings::Settings;
|
||||
use diesel::dsl::*;
|
||||
use diesel::result::Error;
|
||||
use diesel::*;
|
||||
|
@ -99,19 +100,19 @@ pub trait MaybeOptional<T> {
|
|||
|
||||
impl<T> MaybeOptional<T> for T {
|
||||
fn get_optional(self) -> Option<T> {
|
||||
return Some(self);
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> MaybeOptional<T> for Option<T> {
|
||||
fn get_optional(self) -> Option<T> {
|
||||
return self;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn establish_connection() -> PgConnection {
|
||||
let db_url = Settings::get().db_url;
|
||||
PgConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url))
|
||||
pub fn establish_unpooled_connection() -> PgConnection {
|
||||
let db_url = Settings::get().get_database_url();
|
||||
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||
}
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -434,7 +434,7 @@ mod tests {
|
|||
// use Crud;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_mod = UserForm {
|
||||
name: "the mod".into(),
|
||||
|
@ -442,6 +442,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -450,6 +451,8 @@ mod tests {
|
|||
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 inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
||||
|
@ -460,6 +463,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -468,6 +472,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use super::*;
|
||||
use crate::schema::password_reset_request;
|
||||
use crate::schema::password_reset_request::dsl::*;
|
||||
use crypto::digest::Digest;
|
||||
use crypto::sha2::Sha256;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
||||
#[table_name = "password_reset_request"]
|
||||
|
@ -49,8 +48,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
|
|||
impl PasswordResetRequest {
|
||||
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.input_str(token);
|
||||
let token_hash = hasher.result_str();
|
||||
hasher.input(token);
|
||||
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||
|
||||
let form = PasswordResetRequestForm {
|
||||
user_id: from_user_id,
|
||||
|
@ -61,13 +60,21 @@ impl PasswordResetRequest {
|
|||
}
|
||||
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.input_str(token);
|
||||
let token_hash = hasher.result_str();
|
||||
hasher.input(token);
|
||||
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||
password_reset_request
|
||||
.filter(token_encrypted.eq(token_hash))
|
||||
.filter(published.gt(now - 1.days()))
|
||||
.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)]
|
||||
|
@ -77,7 +84,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "thommy prw".into(),
|
||||
|
@ -85,6 +92,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -93,27 +101,26 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
||||
let new_password_reset_request = PasswordResetRequestForm {
|
||||
user_id: inserted_user.id,
|
||||
token_encrypted: "no".into(),
|
||||
};
|
||||
let token = "nope";
|
||||
let token_encrypted_ = "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce";
|
||||
|
||||
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 {
|
||||
id: inserted_password_reset_request.id,
|
||||
user_id: inserted_user.id,
|
||||
token_encrypted: "no".into(),
|
||||
token_encrypted: token_encrypted_.to_string(),
|
||||
published: inserted_password_reset_request.published,
|
||||
};
|
||||
|
||||
let read_password_reset_request =
|
||||
PasswordResetRequest::read(&conn, inserted_password_reset_request.id).unwrap();
|
||||
let read_password_reset_request = PasswordResetRequest::read_from_token(&conn, token).unwrap();
|
||||
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
||||
|
||||
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
||||
|
|
|
@ -179,7 +179,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "jim".into(),
|
||||
|
@ -187,6 +187,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -195,6 +196,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::post_view::post_view::BoxedQuery;
|
||||
use super::post_view::post_mview::BoxedQuery;
|
||||
use super::*;
|
||||
use diesel::pg::Pg;
|
||||
|
||||
|
@ -21,6 +21,7 @@ table! {
|
|||
banned_from_community -> Bool,
|
||||
stickied -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
|
@ -59,6 +60,7 @@ pub struct PostView {
|
|||
pub banned_from_community: bool,
|
||||
pub stickied: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
pub community_removed: bool,
|
||||
pub community_deleted: bool,
|
||||
|
@ -75,6 +77,43 @@ pub struct PostView {
|
|||
pub saved: Option<bool>,
|
||||
}
|
||||
|
||||
// The faked schema since diesel doesn't do views
|
||||
table! {
|
||||
post_mview (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
url -> Nullable<Text>,
|
||||
body -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Bool,
|
||||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
stickied -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
community_nsfw -> Bool,
|
||||
number_of_comments -> BigInt,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
read -> Nullable<Bool>,
|
||||
saved -> Nullable<Bool>,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostQueryBuilder<'a> {
|
||||
conn: &'a PgConnection,
|
||||
query: BoxedQuery<'a, Pg>,
|
||||
|
@ -91,9 +130,9 @@ pub struct PostQueryBuilder<'a> {
|
|||
|
||||
impl<'a> PostQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection) -> Self {
|
||||
use super::post_view::post_view::dsl::*;
|
||||
use super::post_view::post_mview::dsl::*;
|
||||
|
||||
let query = post_view.into_boxed();
|
||||
let query = post_mview.into_boxed();
|
||||
|
||||
PostQueryBuilder {
|
||||
conn,
|
||||
|
@ -121,7 +160,7 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
||||
use super::post_view::post_view::dsl::*;
|
||||
use super::post_view::post_mview::dsl::*;
|
||||
if let Some(for_community_id) = for_community_id.get_optional() {
|
||||
self.query = self.query.filter(community_id.eq(for_community_id));
|
||||
self.query = self.query.then_order_by(stickied.desc());
|
||||
|
@ -137,7 +176,7 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||
use super::post_view::post_view::dsl::*;
|
||||
use super::post_view::post_mview::dsl::*;
|
||||
if let Some(search_term) = search_term.get_optional() {
|
||||
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
||||
}
|
||||
|
@ -145,7 +184,7 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
|
||||
use super::post_view::post_view::dsl::*;
|
||||
use super::post_view::post_mview::dsl::*;
|
||||
if let Some(url_search) = url_search.get_optional() {
|
||||
self.query = self.query.filter(url.eq(url_search));
|
||||
}
|
||||
|
@ -183,16 +222,13 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<PostView>, Error> {
|
||||
use super::post_view::post_view::dsl::*;
|
||||
use super::post_view::post_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
match self.listing_type {
|
||||
ListingType::Subscribed => {
|
||||
if let ListingType::Subscribed = self.listing_type {
|
||||
query = query.filter(subscribed.eq(true));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
query = match self.sort {
|
||||
SortType::Hot => query
|
||||
|
@ -291,7 +327,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let user_name = "tegan".to_string();
|
||||
let community_name = "test_community_3".to_string();
|
||||
|
@ -303,6 +339,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
updated: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -311,6 +348,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -377,6 +416,7 @@ mod tests {
|
|||
body: None,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
community_id: inserted_community.id,
|
||||
|
@ -405,7 +445,7 @@ mod tests {
|
|||
user_id: Some(inserted_user.id),
|
||||
my_vote: Some(1),
|
||||
id: inserted_post.id,
|
||||
name: post_name.to_owned(),
|
||||
name: post_name,
|
||||
url: None,
|
||||
body: None,
|
||||
removed: false,
|
||||
|
@ -413,11 +453,12 @@ mod tests {
|
|||
locked: false,
|
||||
stickied: false,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_name: user_name,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
community_id: inserted_community.id,
|
||||
community_name: community_name.to_owned(),
|
||||
community_name,
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
community_nsfw: false,
|
||||
|
|
|
@ -12,6 +12,7 @@ table! {
|
|||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
number_of_users -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
number_of_comments -> BigInt,
|
||||
|
@ -34,6 +35,7 @@ pub struct SiteView {
|
|||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub number_of_users: i64,
|
||||
pub number_of_posts: i64,
|
||||
pub number_of_comments: i64,
|
||||
|
|
|
@ -14,7 +14,7 @@ pub struct User_ {
|
|||
pub preferred_username: Option<String>,
|
||||
pub password_encrypted: String,
|
||||
pub email: Option<String>,
|
||||
pub icon: Option<Vec<u8>>,
|
||||
pub avatar: Option<String>,
|
||||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
|
@ -24,6 +24,8 @@ pub struct User_ {
|
|||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -36,12 +38,15 @@ pub struct UserForm {
|
|||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub email: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
}
|
||||
|
||||
impl Crud<UserForm> for User_ {
|
||||
|
@ -74,14 +79,13 @@ impl User_ {
|
|||
pub fn update_password(
|
||||
conn: &PgConnection,
|
||||
user_id: i32,
|
||||
form: &UserForm,
|
||||
new_password: &str,
|
||||
) -> Result<Self, Error> {
|
||||
let mut edited_user = form.clone();
|
||||
let password_hash =
|
||||
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
|
||||
edited_user.password_encrypted = password_hash;
|
||||
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
|
||||
|
||||
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> {
|
||||
|
@ -99,6 +103,8 @@ pub struct Claims {
|
|||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub avatar: Option<String>,
|
||||
pub show_avatars: bool,
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
|
@ -123,6 +129,8 @@ impl User_ {
|
|||
default_sort_type: self.default_sort_type,
|
||||
default_listing_type: self.default_listing_type,
|
||||
lang: self.lang.to_owned(),
|
||||
avatar: self.avatar.to_owned(),
|
||||
show_avatars: self.show_avatars.to_owned(),
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
|
@ -132,23 +140,27 @@ impl User_ {
|
|||
.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(
|
||||
conn: &PgConnection,
|
||||
username_or_email: &str,
|
||||
) -> Result<Self, Error> {
|
||||
if is_email_regex(username_or_email) {
|
||||
user_
|
||||
.filter(email.eq(username_or_email))
|
||||
.first::<User_>(conn)
|
||||
User_::find_by_email(conn, username_or_email)
|
||||
} else {
|
||||
user_
|
||||
.filter(name.eq(username_or_email))
|
||||
.first::<User_>(conn)
|
||||
User_::find_by_username(conn, username_or_email)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
||||
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
||||
pub fn get_profile_url(&self) -> String {
|
||||
format!("https://{}/u/{}", Settings::get().hostname, self.name)
|
||||
}
|
||||
|
||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
||||
|
@ -164,7 +176,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "thommy".into(),
|
||||
|
@ -172,6 +184,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -180,6 +193,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -191,7 +206,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
icon: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
published: inserted_user.published,
|
||||
|
@ -201,6 +216,8 @@ mod tests {
|
|||
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 read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||
|
|
|
@ -60,7 +60,7 @@ mod tests {
|
|||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_connection();
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "terrylake".into(),
|
||||
|
@ -68,6 +68,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -76,6 +77,8 @@ mod tests {
|
|||
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 inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -86,6 +89,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -94,6 +98,8 @@ mod tests {
|
|||
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 inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||
|
|
|
@ -20,6 +20,7 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -50,6 +51,7 @@ pub struct UserMentionView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -78,7 +80,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
|||
UserMentionQueryBuilder {
|
||||
conn,
|
||||
query,
|
||||
for_user_id: for_user_id,
|
||||
for_user_id,
|
||||
sort: &SortType::New,
|
||||
unread_only: false,
|
||||
page: None,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::user_view::user_view::BoxedQuery;
|
||||
use super::user_view::user_mview::BoxedQuery;
|
||||
use super::*;
|
||||
use diesel::pg::Pg;
|
||||
|
||||
|
@ -6,9 +6,32 @@ table! {
|
|||
user_view (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
fedi_name -> Varchar,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
published -> Timestamp,
|
||||
number_of_posts -> BigInt,
|
||||
post_score -> BigInt,
|
||||
number_of_comments -> BigInt,
|
||||
comment_score -> BigInt,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
user_mview (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
fedi_name -> Varchar,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
published -> Timestamp,
|
||||
number_of_posts -> BigInt,
|
||||
post_score -> BigInt,
|
||||
|
@ -24,9 +47,13 @@ table! {
|
|||
pub struct UserView {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub fedi_name: String,
|
||||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub number_of_posts: i64,
|
||||
pub post_score: i64,
|
||||
|
@ -44,9 +71,9 @@ pub struct UserQueryBuilder<'a> {
|
|||
|
||||
impl<'a> UserQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection) -> Self {
|
||||
use super::user_view::user_view::dsl::*;
|
||||
use super::user_view::user_mview::dsl::*;
|
||||
|
||||
let query = user_view.into_boxed();
|
||||
let query = user_mview.into_boxed();
|
||||
|
||||
UserQueryBuilder {
|
||||
conn,
|
||||
|
@ -63,7 +90,7 @@ impl<'a> UserQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||
use super::user_view::user_view::dsl::*;
|
||||
use super::user_view::user_mview::dsl::*;
|
||||
if let Some(search_term) = search_term.get_optional() {
|
||||
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
||||
}
|
||||
|
@ -81,7 +108,7 @@ impl<'a> UserQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<UserView>, Error> {
|
||||
use super::user_view::user_view::dsl::*;
|
||||
use super::user_view::user_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
|
@ -120,12 +147,12 @@ impl UserView {
|
|||
}
|
||||
|
||||
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||
use super::user_view::user_view::dsl::*;
|
||||
user_view.filter(admin.eq(true)).load::<Self>(conn)
|
||||
use super::user_view::user_mview::dsl::*;
|
||||
user_mview.filter(admin.eq(true)).load::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||
use super::user_view::user_view::dsl::*;
|
||||
user_view.filter(banned.eq(true)).load::<Self>(conn)
|
||||
use super::user_view::user_mview::dsl::*;
|
||||
user_mview.filter(banned.eq(true)).load::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ pub extern crate actix;
|
|||
pub extern crate actix_web;
|
||||
pub extern crate bcrypt;
|
||||
pub extern crate chrono;
|
||||
pub extern crate crypto;
|
||||
pub extern crate dotenv;
|
||||
pub extern crate jsonwebtoken;
|
||||
pub extern crate lettre;
|
||||
|
@ -20,19 +19,20 @@ pub extern crate rand;
|
|||
pub extern crate regex;
|
||||
pub extern crate serde;
|
||||
pub extern crate serde_json;
|
||||
pub extern crate sha2;
|
||||
pub extern crate strum;
|
||||
|
||||
pub mod api;
|
||||
pub mod apub;
|
||||
pub mod db;
|
||||
pub mod feeds;
|
||||
pub mod nodeinfo;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
pub mod settings;
|
||||
pub mod version;
|
||||
pub mod websocket;
|
||||
|
||||
use crate::settings::Settings;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use dotenv::dotenv;
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
|
@ -40,91 +40,7 @@ use lettre::{SmtpClient, Transport};
|
|||
use lettre_email::Email;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use regex::Regex;
|
||||
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)
|
||||
}
|
||||
}
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||
|
@ -174,13 +90,13 @@ pub fn send_email(
|
|||
to_username: &str,
|
||||
html: &str,
|
||||
) -> 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()
|
||||
.to((to_email, to_username))
|
||||
.from((
|
||||
email_config.smtp_login.to_owned(),
|
||||
email_config.smtp_from_address,
|
||||
email_config.smtp_from_address.to_owned(),
|
||||
))
|
||||
.subject(subject)
|
||||
.html(html)
|
||||
|
@ -209,11 +125,7 @@ pub fn send_email(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
||||
#[test]
|
||||
fn test_api() {
|
||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||
}
|
||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs};
|
||||
|
||||
#[test]
|
||||
fn test_email() {
|
||||
|
@ -224,11 +136,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_slur_filter() {
|
||||
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";
|
||||
assert_eq!(
|
||||
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()
|
||||
);
|
||||
assert!(has_slurs(&test));
|
||||
|
@ -251,6 +163,6 @@ mod tests {
|
|||
|
||||
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 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();
|
||||
}
|
||||
|
|
|
@ -3,259 +3,65 @@ extern crate lemmy_server;
|
|||
extern crate diesel_migrations;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::*;
|
||||
use actix_web_actors::ws;
|
||||
use lemmy_server::db::establish_connection;
|
||||
use lemmy_server::feeds;
|
||||
use lemmy_server::nodeinfo;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket};
|
||||
use lemmy_server::settings::Settings;
|
||||
use lemmy_server::websocket::server::*;
|
||||
use lemmy_server::Settings;
|
||||
use std::env;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::io;
|
||||
|
||||
embed_migrations!();
|
||||
|
||||
/// 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);
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init();
|
||||
let settings = Settings::get();
|
||||
|
||||
/// 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");
|
||||
// Set up the r2d2 connection pool
|
||||
let manager = ConnectionManager::<PgConnection>::new(&settings.get_database_url());
|
||||
let pool = Pool::builder()
|
||||
.max_size(settings.database.pool_size)
|
||||
.build(manager)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url()));
|
||||
|
||||
// Run the migrations from code
|
||||
let conn = establish_connection();
|
||||
let conn = pool.get().unwrap();
|
||||
embedded_migrations::run(&conn).unwrap();
|
||||
|
||||
// Start chat server actor in separate thread
|
||||
let server = ChatServer::default().start();
|
||||
// Set up websocket server
|
||||
let server = ChatServer::startup(pool.clone()).start();
|
||||
|
||||
let settings = Settings::get();
|
||||
println!(
|
||||
"Starting http server at {}:{}",
|
||||
settings.bind, settings.port
|
||||
);
|
||||
|
||||
// Create Http server with websocket support
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.data(pool.clone())
|
||||
.data(server.clone())
|
||||
// Front end routes
|
||||
.service(actix_files::Files::new("/static", front_end_dir()))
|
||||
.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))
|
||||
// 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))
|
||||
// The routes
|
||||
.configure(api::config)
|
||||
.configure(federation::config)
|
||||
.configure(feeds::config)
|
||||
.configure(index::config)
|
||||
.configure(nodeinfo::config)
|
||||
.configure(webfinger::config)
|
||||
.configure(websocket::config)
|
||||
// static files
|
||||
.service(actix_files::Files::new(
|
||||
"/static",
|
||||
settings.front_end_dir.to_owned(),
|
||||
))
|
||||
.service(actix_files::Files::new(
|
||||
"/docs",
|
||||
settings.front_end_dir.to_owned() + "/documentation",
|
||||
))
|
||||
})
|
||||
.bind((settings.bind, settings.port))
|
||||
.unwrap()
|
||||
.start();
|
||||
|
||||
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())
|
||||
.bind((settings.bind, settings.port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
use crate::db::establish_connection;
|
||||
use crate::db::site_view::SiteView;
|
||||
use crate::version;
|
||||
use crate::Settings;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::HttpResponse;
|
||||
use serde_json::json;
|
||||
|
||||
pub fn node_info_well_known() -> HttpResponse<Body> {
|
||||
let json = json!({
|
||||
"links": {
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": format!("https://{}/nodeinfo/2.0.json", Settings::get().hostname),
|
||||
}
|
||||
});
|
||||
|
||||
return HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(json.to_string());
|
||||
}
|
||||
|
||||
pub fn node_info() -> HttpResponse<Body> {
|
||||
let conn = establish_connection();
|
||||
let site_view = match SiteView::read(&conn) {
|
||||
Ok(site_view) => site_view,
|
||||
Err(_e) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
let json = json!({
|
||||
"version": "2.0",
|
||||
"software": {
|
||||
"name": "lemmy",
|
||||
"version": version::VERSION,
|
||||
},
|
||||
"protocols": [],
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": site_view.number_of_users
|
||||
},
|
||||
"localPosts": site_view.number_of_posts,
|
||||
"localComments": site_view.number_of_comments,
|
||||
"openRegistrations": true,
|
||||
}
|
||||
});
|
||||
return HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(json.to_string());
|
||||
}
|
103
server/src/routes/api.rs
Normal file
103
server/src/routes/api.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use crate::api::comment::*;
|
||||
use crate::api::community::*;
|
||||
use crate::api::post::*;
|
||||
use crate::api::site::*;
|
||||
use crate::api::user::*;
|
||||
use crate::api::{Oper, Perform};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::Serialize;
|
||||
|
||||
type DbParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
// Site
|
||||
.route("/api/v1/site", web::get().to(route_get::<GetSite, GetSiteResponse>))
|
||||
.route("/api/v1/categories", web::get().to(route_get::<ListCategories, ListCategoriesResponse>))
|
||||
.route("/api/v1/modlog", web::get().to(route_get::<GetModlog, GetModlogResponse>))
|
||||
.route("/api/v1/search", web::get().to(route_get::<Search, SearchResponse>))
|
||||
// Community
|
||||
.route("/api/v1/community", web::post().to(route_post::<CreateCommunity, CommunityResponse>))
|
||||
.route("/api/v1/community", web::get().to(route_get::<GetCommunity, GetCommunityResponse>))
|
||||
.route("/api/v1/community", web::put().to(route_post::<EditCommunity, CommunityResponse>))
|
||||
.route("/api/v1/community/list", web::get().to(route_get::<ListCommunities, ListCommunitiesResponse>))
|
||||
.route("/api/v1/community/follow", web::post().to(route_post::<FollowCommunity, CommunityResponse>))
|
||||
// Post
|
||||
.route("/api/v1/post", web::post().to(route_post::<CreatePost, PostResponse>))
|
||||
.route("/api/v1/post", web::put().to(route_post::<EditPost, PostResponse>))
|
||||
.route("/api/v1/post", web::get().to(route_get::<GetPost, GetPostResponse>))
|
||||
.route("/api/v1/post/list", web::get().to(route_get::<GetPosts, GetPostsResponse>))
|
||||
.route("/api/v1/post/like", web::post().to(route_post::<CreatePostLike, CreatePostLikeResponse>))
|
||||
.route("/api/v1/post/save", web::put().to(route_post::<SavePost, PostResponse>))
|
||||
// Comment
|
||||
.route("/api/v1/comment", web::post().to(route_post::<CreateComment, CommentResponse>))
|
||||
.route("/api/v1/comment", web::put().to(route_post::<EditComment, CommentResponse>))
|
||||
.route("/api/v1/comment/like", web::post().to(route_post::<CreateCommentLike, CommentResponse>))
|
||||
.route("/api/v1/comment/save", web::put().to(route_post::<SaveComment, CommentResponse>))
|
||||
// User
|
||||
.route("/api/v1/user", web::get().to(route_get::<GetUserDetails, GetUserDetailsResponse>))
|
||||
.route("/api/v1/user/mention", web::get().to(route_get::<GetUserMentions, GetUserMentionsResponse>))
|
||||
.route("/api/v1/user/mention", web::put().to(route_post::<EditUserMention, UserMentionResponse>))
|
||||
.route("/api/v1/user/replies", web::get().to(route_get::<GetReplies, GetRepliesResponse>))
|
||||
.route("/api/v1/user/followed_communities", web::get().to(route_get::<GetFollowedCommunities, GetFollowedCommunitiesResponse>))
|
||||
// Mod actions
|
||||
.route("/api/v1/community/transfer", web::post().to(route_post::<TransferCommunity, GetCommunityResponse>))
|
||||
.route("/api/v1/community/ban_user", web::post().to(route_post::<BanFromCommunity, BanFromCommunityResponse>))
|
||||
.route("/api/v1/community/mod", web::post().to(route_post::<AddModToCommunity, AddModToCommunityResponse>))
|
||||
// Admin actions
|
||||
.route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
|
||||
.route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
|
||||
.route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
|
||||
.route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
|
||||
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
|
||||
// User account actions
|
||||
.route("/api/v1/user/login", web::post().to(route_post::<Login, LoginResponse>))
|
||||
.route("/api/v1/user/register", web::post().to(route_post::<Register, LoginResponse>))
|
||||
.route("/api/v1/user/delete_account", web::post().to(route_post::<DeleteAccount, LoginResponse>))
|
||||
.route("/api/v1/user/password_reset", web::post().to(route_post::<PasswordReset, PasswordResetResponse>))
|
||||
.route("/api/v1/user/password_change", web::post().to(route_post::<PasswordChange, LoginResponse>))
|
||||
.route("/api/v1/user/mark_all_as_read", web::post().to(route_post::<MarkAllAsRead, GetRepliesResponse>))
|
||||
.route("/api/v1/user/save_user_settings", web::put().to(route_post::<SaveUserSettings, LoginResponse>));
|
||||
}
|
||||
|
||||
fn perform<Request, Response>(data: Request, db: DbParam) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Response: Serialize,
|
||||
Oper<Request>: Perform<Response>,
|
||||
{
|
||||
let conn = match db.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(format_err!("{}", e)),
|
||||
};
|
||||
let oper: Oper<Request> = Oper::new(data);
|
||||
let response = oper.perform(&conn);
|
||||
Ok(HttpResponse::Ok().json(response?))
|
||||
}
|
||||
|
||||
async fn route_get<Data, Response>(
|
||||
data: web::Query<Data>,
|
||||
db: DbParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Data: Serialize,
|
||||
Response: Serialize,
|
||||
Oper<Data>: Perform<Response>,
|
||||
{
|
||||
perform::<Data, Response>(data.0, db)
|
||||
}
|
||||
|
||||
async fn route_post<Data, Response>(
|
||||
data: web::Json<Data>,
|
||||
db: DbParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Data: Serialize,
|
||||
Response: Serialize,
|
||||
Oper<Data>: Perform<Response>,
|
||||
{
|
||||
perform::<Data, Response>(data.0, db)
|
||||
}
|
18
server/src/routes/federation.rs
Normal file
18
server/src/routes/federation.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::apub;
|
||||
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),
|
||||
);
|
||||
}
|
|
@ -5,12 +5,14 @@ use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
|
|||
use crate::db::community::Community;
|
||||
use crate::db::post_view::{PostQueryBuilder, PostView};
|
||||
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::{establish_connection, ListingType, SortType};
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::Settings;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||
use serde::Deserialize;
|
||||
|
@ -29,61 +31,79 @@ enum RequestType {
|
|||
Inbox,
|
||||
}
|
||||
|
||||
pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
||||
let sort_type = match get_sort_type(info) {
|
||||
Ok(sort_type) => sort_type,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
let feed_result = get_feed_all_data(&sort_type);
|
||||
|
||||
match feed_result {
|
||||
Ok(rss) => HttpResponse::Ok()
|
||||
.content_type("application/rss+xml")
|
||||
.body(rss),
|
||||
Err(_) => HttpResponse::NotFound().finish(),
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
|
||||
let sort_type = match get_sort_type(info) {
|
||||
Ok(sort_type) => sort_type,
|
||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
async fn get_all_feed(
|
||||
info: web::Query<Params>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
|
||||
let sort_type = get_sort_type(info)?;
|
||||
get_feed_all_data(&conn, &sort_type)
|
||||
})
|
||||
.await
|
||||
.map(|rss| {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/rss+xml")
|
||||
.body(rss)
|
||||
})
|
||||
.map_err(|_| HttpResponse::InternalServerError())?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_feed(
|
||||
path: web::Path<(String, String)>,
|
||||
info: web::Query<Params>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
|
||||
let sort_type = get_sort_type(info)?;
|
||||
|
||||
let request_type = match path.0.as_ref() {
|
||||
"u" => RequestType::User,
|
||||
"c" => RequestType::Community,
|
||||
"front" => RequestType::Front,
|
||||
"inbox" => RequestType::Inbox,
|
||||
_ => return HttpResponse::NotFound().finish(),
|
||||
_ => return Err(format_err!("wrong_type")),
|
||||
};
|
||||
|
||||
let param = path.1.to_owned();
|
||||
|
||||
let feed_result = match request_type {
|
||||
RequestType::User => get_feed_user(&sort_type, param),
|
||||
RequestType::Community => get_feed_community(&sort_type, param),
|
||||
RequestType::Front => get_feed_front(&sort_type, param),
|
||||
RequestType::Inbox => get_feed_inbox(param),
|
||||
};
|
||||
|
||||
match feed_result {
|
||||
Ok(rss) => HttpResponse::Ok()
|
||||
.content_type("application/rss+xml")
|
||||
.body(rss),
|
||||
Err(_) => HttpResponse::NotFound().finish(),
|
||||
match request_type {
|
||||
RequestType::User => get_feed_user(&conn, &sort_type, param),
|
||||
RequestType::Community => get_feed_community(&conn, &sort_type, param),
|
||||
RequestType::Front => get_feed_front(&conn, &sort_type, param),
|
||||
RequestType::Inbox => get_feed_inbox(&conn, param),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(|rss| {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/rss+xml")
|
||||
.body(rss)
|
||||
})
|
||||
.map_err(|_| HttpResponse::InternalServerError())?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn get_feed_all_data(sort_type: &SortType) -> Result<String, Error> {
|
||||
let conn = establish_connection();
|
||||
|
||||
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
|
@ -106,12 +126,14 @@ fn get_feed_all_data(sort_type: &SortType) -> Result<String, Error> {
|
|||
Ok(channel_builder.build().unwrap().to_string())
|
||||
}
|
||||
|
||||
fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Error> {
|
||||
let conn = establish_connection();
|
||||
|
||||
fn get_feed_user(
|
||||
conn: &PgConnection,
|
||||
sort_type: &SortType,
|
||||
user_name: String,
|
||||
) -> Result<String, Error> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
let user = User_::find_by_email_or_username(&conn, &user_name)?;
|
||||
let user_url = format!("https://{}/u/{}", Settings::get().hostname, user.name);
|
||||
let user = User_::find_by_username(&conn, &user_name)?;
|
||||
let user_url = user.get_profile_url();
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
.listing_type(ListingType::All)
|
||||
|
@ -130,12 +152,14 @@ fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Erro
|
|||
Ok(channel_builder.build().unwrap().to_string())
|
||||
}
|
||||
|
||||
fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<String, Error> {
|
||||
let conn = establish_connection();
|
||||
|
||||
fn get_feed_community(
|
||||
conn: &PgConnection,
|
||||
sort_type: &SortType,
|
||||
community_name: String,
|
||||
) -> Result<String, Error> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
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)
|
||||
.listing_type(ListingType::All)
|
||||
|
@ -158,11 +182,9 @@ fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<St
|
|||
Ok(channel_builder.build().unwrap().to_string())
|
||||
}
|
||||
|
||||
fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||
let conn = establish_connection();
|
||||
|
||||
fn get_feed_front(conn: &PgConnection, sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||
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)
|
||||
.listing_type(ListingType::Subscribed)
|
||||
|
@ -185,11 +207,9 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
|||
Ok(channel_builder.build().unwrap().to_string())
|
||||
}
|
||||
|
||||
fn get_feed_inbox(jwt: String) -> Result<String, Error> {
|
||||
let conn = establish_connection();
|
||||
|
||||
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<String, Error> {
|
||||
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;
|
||||
|
||||
|
@ -331,7 +351,7 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
|
|||
"/c/{} <a href=\"{}\">(link)</a>",
|
||||
p.community_name, community_url
|
||||
))
|
||||
.domain(Settings::get().hostname)
|
||||
.domain(Settings::get().hostname.to_owned())
|
||||
.build();
|
||||
i.categories(vec![category.unwrap()]);
|
||||
|
45
server/src/routes/index.rs
Normal file
45
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use crate::settings::Settings;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::web;
|
||||
|
||||
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> {
|
||||
Ok(NamedFile::open(
|
||||
Settings::get().front_end_dir.to_owned() + "/index.html",
|
||||
)?)
|
||||
}
|
7
server/src/routes/mod.rs
Normal file
7
server/src/routes/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod api;
|
||||
pub mod federation;
|
||||
pub mod feeds;
|
||||
pub mod index;
|
||||
pub mod nodeinfo;
|
||||
pub mod webfinger;
|
||||
pub mod websocket;
|
63
server/src/routes/nodeinfo.rs
Normal file
63
server/src/routes/nodeinfo.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::db::site_view::SiteView;
|
||||
use crate::version;
|
||||
use crate::Settings;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web;
|
||||
use actix_web::HttpResponse;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use serde_json::json;
|
||||
|
||||
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!({
|
||||
"links": {
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
"href": format!("https://{}/nodeinfo/2.0.json", Settings::get().hostname),
|
||||
}
|
||||
});
|
||||
|
||||
HttpResponse::Ok().json(json)
|
||||
}
|
||||
|
||||
async fn node_info(
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
let site_view = match SiteView::read(&conn) {
|
||||
Ok(site_view) => site_view,
|
||||
Err(_) => return Err(format_err!("not_found")),
|
||||
};
|
||||
let protocols = if Settings::get().federation_enabled {
|
||||
vec!["activitypub"]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
Ok(json!({
|
||||
"version": "2.0",
|
||||
"software": {
|
||||
"name": "lemmy",
|
||||
"version": version::VERSION,
|
||||
},
|
||||
"protocols": protocols,
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": site_view.number_of_users
|
||||
},
|
||||
"localPosts": site_view.number_of_posts,
|
||||
"localComments": site_view.number_of_comments,
|
||||
"openRegistrations": site_view.open_registration,
|
||||
}
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map(|json| HttpResponse::Ok().json(json))
|
||||
.map_err(|_| HttpResponse::InternalServerError())?;
|
||||
Ok(res)
|
||||
}
|
97
server/src/routes/webfinger.rs
Normal file
97
server/src/routes/webfinger.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use crate::db::community::Community;
|
||||
use crate::Settings;
|
||||
use actix_web::web;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::HttpResponse;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
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>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
|
||||
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 Err(format_err!("not_found")),
|
||||
};
|
||||
|
||||
// Make sure the requested community exists.
|
||||
let community = match Community::read_from_name(&conn, community_name.to_string()) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return Err(format_err!("not_found")),
|
||||
};
|
||||
|
||||
let community_url = community.get_url();
|
||||
|
||||
Ok(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}"
|
||||
//}
|
||||
]
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map(|json| HttpResponse::Ok().json(json))
|
||||
.map_err(|_| HttpResponse::InternalServerError())?;
|
||||
Ok(res)
|
||||
}
|
184
server/src/routes/websocket.rs
Normal file
184
server/src/routes/websocket.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
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) {
|
||||
cfg.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> {
|
||||
// TODO not sure if the blocking should be here or not
|
||||
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,
|
||||
// db: Pool<ConnectionManager<PgConnection>>,
|
||||
}
|
||||
|
||||
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>,
|
||||
password_encrypted -> Text,
|
||||
email -> Nullable<Text>,
|
||||
icon -> Nullable<Bytea>,
|
||||
avatar -> Nullable<Text>,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
published -> Timestamp,
|
||||
|
@ -270,6 +270,8 @@ table! {
|
|||
default_sort_type -> Int2,
|
||||
default_listing_type -> Int2,
|
||||
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.6.0";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue