Compare commits
202 commits
master
...
proxy-pict
Author | SHA1 | Date | |
---|---|---|---|
a0e180994e | |||
|
3b12f92752 | ||
|
2998957617 | ||
|
c5fc5cc9d0 | ||
|
7bbb071b0b | ||
|
d6d060f7ab | ||
|
61a5bcaf04 | ||
|
c83dc4f311 | ||
|
9140faded0 | ||
|
7be3cff714 | ||
|
4dfd96ce8c | ||
|
349751f143 | ||
|
bfc45aa9bc | ||
|
9024809a5c | ||
|
197bd67601 | ||
|
ada50fc3de | ||
|
351cd84ab8 | ||
|
d458571f13 | ||
|
58af4355c5 | ||
|
937489ad51 | ||
|
ba16e36202 | ||
|
c3eaa2273a | ||
|
f5b75f342b | ||
|
bd1fc2b80b | ||
|
69389f61c9 | ||
|
7fdcae4f07 | ||
752318fdf3 | |||
|
9ccff18f23 | ||
|
5197407dd2 | ||
|
58f673ab78 | ||
|
bacb9ac59e | ||
|
10c6505968 | ||
|
7d3adda0cd | ||
|
759453772d | ||
|
2b4bacaa10 | ||
|
dbe9ad0998 | ||
|
fc86b83e36 | ||
|
dc35c7b126 | ||
|
5fec981674 | ||
|
572b3b876f | ||
|
7145dde79f | ||
|
365f81b699 | ||
|
5dc0d947e9 | ||
|
6312ff333b | ||
|
2394993dd4 | ||
|
66adf67661 | ||
|
7b7fb0f5d2 | ||
|
7f0e69e54c | ||
|
3b8a2f61fc | ||
|
20c9c54806 | ||
|
dc84ccaac9 | ||
|
3edd75ed43 | ||
|
6c61dd266b | ||
e518954bca | |||
0a409bc9be | |||
|
c5eecd055e | ||
|
0c5eb47135 | ||
|
9e60e76a8c | ||
e859080632 | |||
|
126e2085fd | ||
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 |
153 changed files with 10485 additions and 5493 deletions
1
.dockerignore
vendored
1
.dockerignore
vendored
|
@ -1,5 +1,4 @@
|
||||||
ui/node_modules
|
ui/node_modules
|
||||||
ui/dist
|
ui/dist
|
||||||
server/target
|
server/target
|
||||||
docs
|
|
||||||
.git
|
.git
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
||||||
ansible/inventory
|
ansible/inventory
|
||||||
ansible/passwords/
|
ansible/passwords/
|
||||||
|
docker/lemmy_mine.hjson
|
||||||
|
docker/dev/env_deploy.sh
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
26
.travis.yml
vendored
26
.travis.yml
vendored
|
@ -5,21 +5,31 @@ matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rust: nightly
|
- rust: nightly
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
cache:
|
cache: cargo
|
||||||
directories:
|
|
||||||
- /home/travis/.cargo
|
|
||||||
before_cache:
|
before_cache:
|
||||||
- rm -rf /home/travis/.cargo/registry
|
- rm -rfv target/debug/incremental/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/.fingerprint/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/build/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/deps/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/lemmy_server.d
|
||||||
|
- cargo clean
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
- psql -c 'create database rrr with owner rrr;' -U postgres
|
- psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
|
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
|
||||||
before_install:
|
before_install:
|
||||||
- cd server
|
- cd server
|
||||||
script:
|
script:
|
||||||
- diesel migration run
|
# Default checks, but fail if anything is detected
|
||||||
- cargo build
|
- 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
|
- cargo test
|
||||||
env:
|
env:
|
||||||
- DATABASE_URL=postgres://rrr:rrr@localhost/rrr
|
global:
|
||||||
|
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
|
- RUST_TEST_THREADS=1
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.4"
|
||||||
|
|
35
CODE_OF_CONDUCT.md
vendored
Normal file
35
CODE_OF_CONDUCT.md
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
- We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
|
||||||
|
- Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
|
||||||
|
- Please be kind and courteous. There’s no need to be mean or rude.
|
||||||
|
- Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
|
||||||
|
- Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
|
||||||
|
- We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the Citizen Code of Conduct; if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups.
|
||||||
|
- Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or any of the Lemmy moderation team immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back.
|
||||||
|
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
|
||||||
|
|
||||||
|
[**Message the Moderation Team on Mastodon**](https://mastodon.social/@LemmyDev)
|
||||||
|
|
||||||
|
[**Email The Moderation Team**](mailto:contact@lemmy.ml)
|
||||||
|
|
||||||
|
## Moderation
|
||||||
|
|
||||||
|
These are the policies for upholding our community’s standards of conduct. If you feel that a thread needs moderation, please contact the Lemmy moderation team .
|
||||||
|
|
||||||
|
1. Remarks that violate the Lemmy standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.)
|
||||||
|
2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed.
|
||||||
|
3. Moderators will first respond to such remarks with a warning.
|
||||||
|
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
|
||||||
|
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
|
||||||
|
6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
|
||||||
|
7. If a moderator bans someone and you think it was unjustified, please take it up with that moderator, or with a different moderator, in private. Complaints about bans in-channel are not allowed.
|
||||||
|
8. Moderators are held to a higher standard than other community members. If a moderator creates an inappropriate situation, they should expect less leeway than others.
|
||||||
|
|
||||||
|
In the Lemmy community we strive to go the extra step to look out for each other. Don’t just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if they’re off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely.
|
||||||
|
|
||||||
|
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
|
||||||
|
|
||||||
|
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/dessalines/lemmy](https://github.com/dessalines/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
|
||||||
|
|
||||||
|
Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).
|
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.
|
||||||
|
|
171
README.md
vendored
171
README.md
vendored
|
@ -7,10 +7,8 @@
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[![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)
|
|
||||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||||
|
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
|
||||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||||
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
|
||||||
|
@ -28,7 +26,7 @@
|
||||||
<br>
|
<br>
|
||||||
</p>
|
</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.
|
This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
|
||||||
|
|
||||||
|
@ -36,30 +34,25 @@ Front Page|Post
|
||||||
---|---
|
---|---
|
||||||
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
||||||
|
|
||||||
## 📝 Table of Contents
|
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||||
|
|
||||||
<!-- toc -->
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
- [Features](#features)
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
- [About](#about)
|
|
||||||
* [Why's it called Lemmy?](#whys-it-called-lemmy)
|
|
||||||
- [Install](#install)
|
|
||||||
* [Docker](#docker)
|
|
||||||
+ [Updating](#updating)
|
|
||||||
* [Ansible](#ansible)
|
|
||||||
* [Kubernetes](#kubernetes)
|
|
||||||
- [Develop](#develop)
|
|
||||||
* [Docker Development](#docker-development)
|
|
||||||
* [Local Development](#local-development)
|
|
||||||
+ [Requirements](#requirements)
|
|
||||||
+ [Set up Postgres DB](#set-up-postgres-db)
|
|
||||||
+ [Running](#running)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [Support](#support)
|
|
||||||
- [Translations](#translations)
|
|
||||||
- [Credits](#credits)
|
|
||||||
|
|
||||||
<!-- tocstop -->
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://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)
|
||||||
|
|
||||||
|
## Repository Mirrors
|
||||||
|
|
||||||
|
- [GitHub](https://github.com/dessalines/lemmy)
|
||||||
|
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
|
||||||
|
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -67,18 +60,24 @@ Front Page|Post
|
||||||
- Self hostable, easy to deploy.
|
- Self hostable, easy to deploy.
|
||||||
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||||
- Clean, mobile-friendly interface.
|
- Clean, mobile-friendly interface.
|
||||||
|
- Only a minimum of a username and password is required to sign up!
|
||||||
|
- User avatar support.
|
||||||
- Live-updating Comment threads.
|
- Live-updating Comment threads.
|
||||||
- Full vote scores `(+/-)` like old reddit.
|
- Full vote scores `(+/-)` like old reddit.
|
||||||
- Themes, including light, dark, and solarized.
|
- Themes, including light, dark, and solarized.
|
||||||
- Emojis with autocomplete support. Start typing `:`
|
- Emojis with autocomplete support. Start typing `:`
|
||||||
- User tagging using `@`, Community tagging using `#`.
|
- User tagging using `@`, Community tagging using `#`.
|
||||||
|
- Integrated image uploading in both posts and comments.
|
||||||
|
- A post can consist of a title and any combination of self text, a URL, or nothing else.
|
||||||
- Notifications, on comment replies and when you're tagged.
|
- Notifications, on comment replies and when you're tagged.
|
||||||
|
- Notifications can be sent via email.
|
||||||
- i18n / internationalization support.
|
- i18n / internationalization support.
|
||||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||||
- Cross-posting support.
|
- Cross-posting support.
|
||||||
- A *similar post search* when creating new posts. Great for question / answer communities.
|
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||||
- Moderation abilities.
|
- Moderation abilities.
|
||||||
- Public Moderation Logs.
|
- Public Moderation Logs.
|
||||||
|
- Can sticky posts to the top of communities.
|
||||||
- Both site admins, and community moderators, who can appoint other moderators.
|
- Both site admins, and community moderators, who can appoint other moderators.
|
||||||
- Can lock, remove, and restore posts and comments.
|
- Can lock, remove, and restore posts and comments.
|
||||||
- Can ban and unban users from communities and the site.
|
- Can ban and unban users from communities and the site.
|
||||||
|
@ -90,25 +89,13 @@ Front Page|Post
|
||||||
- Front end is `~80kB` gzipped.
|
- Front end is `~80kB` gzipped.
|
||||||
- Supports arm64 / Raspberry Pi.
|
- Supports arm64 / Raspberry Pi.
|
||||||
|
|
||||||
## About
|
## Why's it called Lemmy?
|
||||||
|
|
||||||
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
|
||||||
|
|
||||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
|
||||||
|
|
||||||
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
|
||||||
|
|
||||||
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
|
||||||
|
|
||||||
### Why's it called Lemmy?
|
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
@ -119,8 +106,8 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||||
mkdir lemmy/
|
mkdir lemmy/
|
||||||
cd lemmy/
|
cd lemmy/
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/.env
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
# Edit the .env if you want custom passwords
|
# Edit lemmy.hjson to do more configuration
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -156,85 +143,16 @@ nano inventory # enter your server, domain, contact email
|
||||||
ansible-playbook lemmy.yml --become
|
ansible-playbook lemmy.yml --become
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kubernetes
|
## Support / Donate
|
||||||
|
|
||||||
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.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
|
||||||
- [Support on Patreon](https://www.patreon.com/dessalines).
|
- [Support on Patreon](https://www.patreon.com/dessalines).
|
||||||
- [Sponsor List](https://dev.lemmy.ml/sponsors).
|
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
||||||
|
- Soon to add either liberapay or opencollective.
|
||||||
|
|
||||||
|
### Crypto
|
||||||
|
|
||||||
- bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
|
- bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
|
||||||
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
||||||
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
|
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
|
||||||
|
@ -245,24 +163,27 @@ If you'd like to add translations, take a look a look at the [English translatio
|
||||||
|
|
||||||
- Languages supported: English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`), Italian (`it`).
|
- Languages supported: English (`en`), Chinese (`zh`), Dutch (`nl`), Esperanto (`eo`), French (`fr`), Spanish (`es`), Swedish (`sv`), German (`de`), Russian (`ru`), Italian (`it`).
|
||||||
|
|
||||||
|
<!-- translations -->
|
||||||
|
|
||||||
lang | done | missing
|
lang | done | missing
|
||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
de | 100% |
|
de | 88% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
eo | 86% | number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme,are_you_sure,yes,no
|
eo | 76% | number_of_communities,create_private_message,send_secure_message,send_message,message,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,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
es | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
es | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
fr | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
fr | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
it | 96% | archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
it | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
nl | 88% | preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme
|
nl | 93% | create_private_message,send_secure_message,send_message,message,message_sent,messages,matrix_user_id,private_message_disclaimer,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
ru | 82% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
ru | 72% | cross_posts,cross_post,number_of_communities,create_private_message,send_secure_message,send_message,message,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,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
sv | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
sv | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
zh | 80% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
zh | 70% | cross_posts,cross_post,users,number_of_communities,create_private_message,send_secure_message,send_message,message,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,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message
|
||||||
|
|
||||||
|
<!-- translationsstop -->
|
||||||
|
|
||||||
If you'd like to update this report, run:
|
If you'd like to update this report, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ui
|
cd ui
|
||||||
ts-node translation_report.ts > tmp # And replace the text above.
|
ts-node translation_report.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
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]
|
[lemmy]
|
||||||
# define the username and hostname that you use for ssh connection, and specify the domain
|
# 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]
|
[all:vars]
|
||||||
ansible_connection=ssh
|
ansible_connection=ssh
|
||||||
|
|
10
ansible/lemmy.yml
vendored
10
ansible/lemmy.yml
vendored
|
@ -32,21 +32,13 @@
|
||||||
- name: add all template files
|
- name: add all template files
|
||||||
template: src={{item.src}} dest={{item.dest}}
|
template: src={{item.src}} dest={{item.dest}}
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/env', dest: '/lemmy/.env' }
|
|
||||||
- { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
|
- { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
|
||||||
|
- { src: 'templates/config.hjson', dest: '/lemmy/lemmy.hjson' }
|
||||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
|
||||||
vars:
|
vars:
|
||||||
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
||||||
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
||||||
|
|
||||||
- name: set env file permissions
|
|
||||||
file:
|
|
||||||
path: "/lemmy/.env"
|
|
||||||
state: touch
|
|
||||||
mode: 0600
|
|
||||||
access_time: preserve
|
|
||||||
modification_time: preserve
|
|
||||||
|
|
||||||
- name: enable and start docker service
|
- name: enable and start docker service
|
||||||
systemd:
|
systemd:
|
||||||
name: docker
|
name: docker
|
||||||
|
|
15
ansible/templates/config.hjson
vendored
Normal file
15
ansible/templates/config.hjson
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
database: {
|
||||||
|
password: "{{ postgres_password }}"
|
||||||
|
host: "lemmy_db"
|
||||||
|
}
|
||||||
|
hostname: "{{ domain }}"
|
||||||
|
jwt_secret: "{{ jwt_password }}"
|
||||||
|
front_end_dir: "/app/dist"
|
||||||
|
email: {
|
||||||
|
smtp_server: "{{ smtp_server }}"
|
||||||
|
smtp_login: "{{ smtp_login }}"
|
||||||
|
smtp_password: "{{ smtp_password }}"
|
||||||
|
smtp_from_address: "{{ smtp_from_address }}"
|
||||||
|
}
|
||||||
|
}
|
14
ansible/templates/env
vendored
14
ansible/templates/env
vendored
|
@ -1,14 +0,0 @@
|
||||||
DOMAIN={{ domain }}
|
|
||||||
DATABASE_PASSWORD={{ postgres_password }}
|
|
||||||
DATABASE_URL=postgres://lemmy:{{ postgres_password }}@lemmy_db:5432/lemmy
|
|
||||||
JWT_SECRET={{ jwt_password }}
|
|
||||||
RATE_LIMIT_MESSAGE=30
|
|
||||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
|
||||||
RATE_LIMIT_POST=3
|
|
||||||
RATE_LIMIT_POST_PER_SECOND=600
|
|
||||||
RATE_LIMIT_REGISTER=3
|
|
||||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
|
||||||
SMTP_SERVER={{ smtp_server }}
|
|
||||||
SMTP_LOGIN={{ smtp_login }}
|
|
||||||
SMTP_PASSWORD={{ smtp_password }}
|
|
||||||
SMTP_FROM_ADDRESS={{ smtp_from_address }}
|
|
13
ansible/templates/nginx.conf
vendored
13
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 {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name {{ domain }};
|
server_name {{ domain }};
|
||||||
|
@ -59,8 +61,16 @@ server {
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Proxy Cache
|
||||||
|
proxy_cache lemmy_frontend_cache;
|
||||||
|
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_revalidate on;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
proxy_cache_min_uses 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TODO: probably remove this (at least the proxy_pass)
|
||||||
location /pictshare/ {
|
location /pictshare/ {
|
||||||
proxy_pass http://0.0.0.0:8537/;
|
proxy_pass http://0.0.0.0:8537/;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
@ -68,8 +78,7 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
|
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
|
||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
expires max;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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
|
COPY ui /app/ui
|
||||||
RUN yarn build
|
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
|
# Cache deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -32,14 +32,25 @@ RUN cargo build --frozen --release
|
||||||
# Get diesel-cli on there just in case
|
# Get diesel-cli on there just in case
|
||||||
# RUN cargo install diesel_cli --no-default-features --features postgres
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
|
||||||
|
FROM ekidd/rust-musl-builder:1.40.0-openssl11 as docs
|
||||||
|
WORKDIR /app
|
||||||
|
COPY docs ./docs
|
||||||
|
RUN sudo chown -R rust:rust .
|
||||||
|
RUN mdbook build docs/
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.10
|
FROM alpine:3.10
|
||||||
|
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
|
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
|
||||||
|
COPY --from=docs /app/docs/book/ /app/dist/documentation/
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
RUN addgroup -g 1000 lemmy
|
RUN addgroup -g 1000 lemmy
|
||||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
RUN chown lemmy:lemmy /app/lemmy
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
|
|
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 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-aarch64-static /usr/bin
|
||||||
#COPY --from=qemu /usr/bin/qemu-arm-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
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
|
1
docker/dev/Dockerfile.armv7hf
vendored
1
docker/dev/Dockerfile.armv7hf
vendored
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
||||||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
|
2
docker/dev/Dockerfile.libc
vendored
2
docker/dev/Dockerfile.libc
vendored
|
@ -65,8 +65,10 @@ RUN addgroup --gid 1000 lemmy
|
||||||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/ready /app/lemmy
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
RUN chown lemmy:lemmy /app/lemmy
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
USER lemmy
|
USER lemmy
|
||||||
EXPOSE 8536
|
EXPOSE 8536
|
||||||
|
|
18
docker/dev/deploy.sh
vendored
18
docker/dev/deploy.sh
vendored
|
@ -5,12 +5,14 @@ git checkout master
|
||||||
new_tag="$1"
|
new_tag="$1"
|
||||||
git tag $new_tag
|
git tag $new_tag
|
||||||
|
|
||||||
|
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||||
|
|
||||||
# Setting the version on the front end
|
# Setting the version on the front end
|
||||||
cd ../../
|
cd ../../
|
||||||
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||||
git add "ui/src/version.ts"
|
git add "ui/src/version.ts"
|
||||||
# Setting the version on the backend
|
# Setting the version on the backend
|
||||||
echo "pub const VERSION: &'static str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||||
git add "server/src/version.rs"
|
git add "server/src/version.rs"
|
||||||
|
|
||||||
cd docker/dev
|
cd docker/dev
|
||||||
|
@ -22,9 +24,6 @@ git add ../prod/docker-compose.yml
|
||||||
# The commit
|
# The commit
|
||||||
git commit -m"Version $new_tag"
|
git commit -m"Version $new_tag"
|
||||||
|
|
||||||
# Registering qemu binaries
|
|
||||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
|
||||||
|
|
||||||
# Rebuilding docker
|
# Rebuilding docker
|
||||||
docker-compose build
|
docker-compose build
|
||||||
docker tag dev_lemmy:latest dessalines/lemmy:x64-$new_tag
|
docker tag dev_lemmy:latest dessalines/lemmy:x64-$new_tag
|
||||||
|
@ -38,14 +37,25 @@ docker push dessalines/lemmy:x64-$new_tag
|
||||||
# docker push dessalines/lemmy:armv7hf-$new_tag
|
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||||
|
|
||||||
# aarch64
|
# aarch64
|
||||||
|
# Only do this on major releases (IE the third semver is 0)
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
|
# Registering qemu binaries
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||||
|
|
||||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||||
docker push dessalines/lemmy:arm64-$new_tag
|
docker push dessalines/lemmy:arm64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
# Creating the manifest for the multi-arch build
|
# Creating the manifest for the multi-arch build
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
docker manifest create dessalines/lemmy:$new_tag \
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
dessalines/lemmy:x64-$new_tag \
|
dessalines/lemmy:x64-$new_tag \
|
||||||
dessalines/lemmy:arm64-$new_tag
|
dessalines/lemmy:arm64-$new_tag
|
||||||
|
else
|
||||||
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
|
dessalines/lemmy:x64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
docker manifest push dessalines/lemmy:$new_tag
|
docker manifest push dessalines/lemmy:$new_tag
|
||||||
|
|
||||||
|
|
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-compose build
|
||||||
docker tag dev_lemmy:latest dessalines/lemmy:dev
|
docker tag dev_lemmy:latest dessalines/lemmy:dev
|
||||||
docker push 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"
|
||||||
|
|
26
docker/dev/docker-compose.yml
vendored
26
docker/dev/docker-compose.yml
vendored
|
@ -5,7 +5,7 @@ services:
|
||||||
image: postgres:12-alpine
|
image: postgres:12-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=lemmy
|
- POSTGRES_USER=lemmy
|
||||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
- POSTGRES_PASSWORD=password
|
||||||
- POSTGRES_DB=lemmy
|
- POSTGRES_DB=lemmy
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
|
@ -14,30 +14,18 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ../../
|
context: ../../
|
||||||
dockerfile: docker/dev/Dockerfile
|
dockerfile: docker/dev/Dockerfile
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=actix_web=info
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
environment:
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- HOSTNAME=${DOMAIN}
|
|
||||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
|
||||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
|
||||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
|
||||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
|
||||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
|
||||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
|
||||||
- SMTP_SERVER=${SMTP_SERVER}
|
|
||||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ../lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- lemmy_db
|
- lemmy_db
|
||||||
lemmy_pictshare:
|
- pictshare
|
||||||
|
pictshare:
|
||||||
image: shtripok/pictshare:latest
|
image: shtripok/pictshare:latest
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8537:80"
|
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
6
docker/k8s/lemmy.yml
vendored
6
docker/k8s/lemmy.yml
vendored
|
@ -14,13 +14,13 @@ spec:
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- env:
|
- env:
|
||||||
- name: DATABASE_URL
|
- name: LEMMY_DATABASE_URL
|
||||||
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: HOSTNAME
|
- name: LEMMY_HOSTNAME
|
||||||
# example: 'lemmy.example.com'
|
# example: 'lemmy.example.com'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: JWT_SECRET
|
- name: LEMMY_JWT_SECRET
|
||||||
# example: 'very-super-good-secret'
|
# example: 'very-super-good-secret'
|
||||||
value: CHANGE_ME
|
value: CHANGE_ME
|
||||||
- name: LEMMY_FRONT_END_DIR
|
- name: LEMMY_FRONT_END_DIR
|
||||||
|
|
58
docker/lemmy.hjson
vendored
Normal file
58
docker/lemmy.hjson
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
# The url where pictshare is available (this should only be exposed to lemmy, not to the outside)
|
||||||
|
pictshare_url: "http://pictshare:80"
|
||||||
|
# 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>
|
|
26
docker/prod/docker-compose.yml
vendored
26
docker/prod/docker-compose.yml
vendored
|
@ -5,37 +5,23 @@ services:
|
||||||
image: postgres:12-alpine
|
image: postgres:12-alpine
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=lemmy
|
- POSTGRES_USER=lemmy
|
||||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
- POSTGRES_PASSWORD=password
|
||||||
- POSTGRES_DB=lemmy
|
- POSTGRES_DB=lemmy
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.5.0.3
|
image: dessalines/lemmy:v0.6.4
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
environment:
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- HOSTNAME=${DOMAIN}
|
|
||||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
|
||||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
|
||||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
|
||||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
|
||||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
|
||||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
|
||||||
- SMTP_SERVER=${SMTP_SERVER}
|
|
||||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
|
||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
|
||||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
|
||||||
restart: always
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- lemmy_db
|
- lemmy_db
|
||||||
lemmy_pictshare:
|
- pictshare
|
||||||
|
pictshare:
|
||||||
image: shtripok/pictshare:latest
|
image: shtripok/pictshare:latest
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8537:80"
|
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
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/HTTP API](contributing_websocket_http_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://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
|
||||||
|
```
|
|
@ -5,126 +5,171 @@
|
||||||
|
|
||||||
- [Data types](#data-types)
|
- [Data types](#data-types)
|
||||||
- [Basic usage](#basic-usage)
|
- [Basic usage](#basic-usage)
|
||||||
* [WebSocket Endpoint](#websocket-endpoint)
|
* [WebSocket](#websocket)
|
||||||
* [Testing with Websocat](#testing-with-websocat)
|
+ [Testing with Websocat](#testing-with-websocat)
|
||||||
* [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api)
|
+ [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api)
|
||||||
|
* [HTTP](#http)
|
||||||
|
+ [Testing with Curl](#testing-with-curl)
|
||||||
|
- [Get Example](#get-example)
|
||||||
|
- [Post Example](#post-example)
|
||||||
- [Rate limits](#rate-limits)
|
- [Rate limits](#rate-limits)
|
||||||
- [Errors](#errors)
|
- [Errors](#errors)
|
||||||
- [API documentation](#api-documentation)
|
- [API documentation](#api-documentation)
|
||||||
* [Sort Types](#sort-types)
|
* [Sort Types](#sort-types)
|
||||||
|
* [Websocket vs HTTP](#websocket-vs-http)
|
||||||
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
|
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
|
||||||
+ [Login](#login)
|
+ [Login](#login)
|
||||||
- [Request](#request)
|
- [Request](#request)
|
||||||
- [Response](#response)
|
- [Response](#response)
|
||||||
|
- [HTTP](#http-1)
|
||||||
+ [Register](#register)
|
+ [Register](#register)
|
||||||
- [Request](#request-1)
|
- [Request](#request-1)
|
||||||
- [Response](#response-1)
|
- [Response](#response-1)
|
||||||
|
- [HTTP](#http-2)
|
||||||
+ [Get User Details](#get-user-details)
|
+ [Get User Details](#get-user-details)
|
||||||
- [Request](#request-2)
|
- [Request](#request-2)
|
||||||
- [Response](#response-2)
|
- [Response](#response-2)
|
||||||
|
- [HTTP](#http-3)
|
||||||
+ [Save User Settings](#save-user-settings)
|
+ [Save User Settings](#save-user-settings)
|
||||||
- [Request](#request-3)
|
- [Request](#request-3)
|
||||||
- [Response](#response-3)
|
- [Response](#response-3)
|
||||||
|
- [HTTP](#http-4)
|
||||||
+ [Get Replies / Inbox](#get-replies--inbox)
|
+ [Get Replies / Inbox](#get-replies--inbox)
|
||||||
- [Request](#request-4)
|
- [Request](#request-4)
|
||||||
- [Response](#response-4)
|
- [Response](#response-4)
|
||||||
|
- [HTTP](#http-5)
|
||||||
+ [Get User Mentions](#get-user-mentions)
|
+ [Get User Mentions](#get-user-mentions)
|
||||||
- [Request](#request-5)
|
- [Request](#request-5)
|
||||||
- [Response](#response-5)
|
- [Response](#response-5)
|
||||||
+ [Mark All As Read](#mark-all-as-read)
|
- [HTTP](#http-6)
|
||||||
|
+ [Edit User Mention](#edit-user-mention)
|
||||||
- [Request](#request-6)
|
- [Request](#request-6)
|
||||||
- [Response](#response-6)
|
- [Response](#response-6)
|
||||||
+ [Delete Account](#delete-account)
|
- [HTTP](#http-7)
|
||||||
|
+ [Mark All As Read](#mark-all-as-read)
|
||||||
- [Request](#request-7)
|
- [Request](#request-7)
|
||||||
- [Response](#response-7)
|
- [Response](#response-7)
|
||||||
+ [Add admin](#add-admin)
|
- [HTTP](#http-8)
|
||||||
|
+ [Delete Account](#delete-account)
|
||||||
- [Request](#request-8)
|
- [Request](#request-8)
|
||||||
- [Response](#response-8)
|
- [Response](#response-8)
|
||||||
+ [Ban user](#ban-user)
|
- [HTTP](#http-9)
|
||||||
|
+ [Add admin](#add-admin)
|
||||||
- [Request](#request-9)
|
- [Request](#request-9)
|
||||||
- [Response](#response-9)
|
- [Response](#response-9)
|
||||||
* [Site](#site)
|
- [HTTP](#http-10)
|
||||||
+ [List Categories](#list-categories)
|
+ [Ban user](#ban-user)
|
||||||
- [Request](#request-10)
|
- [Request](#request-10)
|
||||||
- [Response](#response-10)
|
- [Response](#response-10)
|
||||||
+ [Search](#search)
|
- [HTTP](#http-11)
|
||||||
|
* [Site](#site)
|
||||||
|
+ [List Categories](#list-categories)
|
||||||
- [Request](#request-11)
|
- [Request](#request-11)
|
||||||
- [Response](#response-11)
|
- [Response](#response-11)
|
||||||
+ [Get Modlog](#get-modlog)
|
- [HTTP](#http-12)
|
||||||
|
+ [Search](#search)
|
||||||
- [Request](#request-12)
|
- [Request](#request-12)
|
||||||
- [Response](#response-12)
|
- [Response](#response-12)
|
||||||
+ [Create Site](#create-site)
|
- [HTTP](#http-13)
|
||||||
|
+ [Get Modlog](#get-modlog)
|
||||||
- [Request](#request-13)
|
- [Request](#request-13)
|
||||||
- [Response](#response-13)
|
- [Response](#response-13)
|
||||||
+ [Edit Site](#edit-site)
|
- [HTTP](#http-14)
|
||||||
|
+ [Create Site](#create-site)
|
||||||
- [Request](#request-14)
|
- [Request](#request-14)
|
||||||
- [Response](#response-14)
|
- [Response](#response-14)
|
||||||
+ [Get Site](#get-site)
|
- [HTTP](#http-15)
|
||||||
|
+ [Edit Site](#edit-site)
|
||||||
- [Request](#request-15)
|
- [Request](#request-15)
|
||||||
- [Response](#response-15)
|
- [Response](#response-15)
|
||||||
+ [Transfer Site](#transfer-site)
|
- [HTTP](#http-16)
|
||||||
|
+ [Get Site](#get-site)
|
||||||
- [Request](#request-16)
|
- [Request](#request-16)
|
||||||
- [Response](#response-16)
|
- [Response](#response-16)
|
||||||
* [Community](#community)
|
- [HTTP](#http-17)
|
||||||
+ [Get Community](#get-community)
|
+ [Transfer Site](#transfer-site)
|
||||||
- [Request](#request-17)
|
- [Request](#request-17)
|
||||||
- [Response](#response-17)
|
- [Response](#response-17)
|
||||||
+ [Create Community](#create-community)
|
- [HTTP](#http-18)
|
||||||
|
* [Community](#community)
|
||||||
|
+ [Get Community](#get-community)
|
||||||
- [Request](#request-18)
|
- [Request](#request-18)
|
||||||
- [Response](#response-18)
|
- [Response](#response-18)
|
||||||
+ [List Communities](#list-communities)
|
- [HTTP](#http-19)
|
||||||
|
+ [Create Community](#create-community)
|
||||||
- [Request](#request-19)
|
- [Request](#request-19)
|
||||||
- [Response](#response-19)
|
- [Response](#response-19)
|
||||||
+ [Ban from Community](#ban-from-community)
|
- [HTTP](#http-20)
|
||||||
|
+ [List Communities](#list-communities)
|
||||||
- [Request](#request-20)
|
- [Request](#request-20)
|
||||||
- [Response](#response-20)
|
- [Response](#response-20)
|
||||||
+ [Add Mod to Community](#add-mod-to-community)
|
- [HTTP](#http-21)
|
||||||
|
+ [Ban from Community](#ban-from-community)
|
||||||
- [Request](#request-21)
|
- [Request](#request-21)
|
||||||
- [Response](#response-21)
|
- [Response](#response-21)
|
||||||
+ [Edit Community](#edit-community)
|
- [HTTP](#http-22)
|
||||||
|
+ [Add Mod to Community](#add-mod-to-community)
|
||||||
- [Request](#request-22)
|
- [Request](#request-22)
|
||||||
- [Response](#response-22)
|
- [Response](#response-22)
|
||||||
+ [Follow Community](#follow-community)
|
- [HTTP](#http-23)
|
||||||
|
+ [Edit Community](#edit-community)
|
||||||
- [Request](#request-23)
|
- [Request](#request-23)
|
||||||
- [Response](#response-23)
|
- [Response](#response-23)
|
||||||
+ [Get Followed Communities](#get-followed-communities)
|
- [HTTP](#http-24)
|
||||||
|
+ [Follow Community](#follow-community)
|
||||||
- [Request](#request-24)
|
- [Request](#request-24)
|
||||||
- [Response](#response-24)
|
- [Response](#response-24)
|
||||||
+ [Transfer Community](#transfer-community)
|
- [HTTP](#http-25)
|
||||||
|
+ [Get Followed Communities](#get-followed-communities)
|
||||||
- [Request](#request-25)
|
- [Request](#request-25)
|
||||||
- [Response](#response-25)
|
- [Response](#response-25)
|
||||||
* [Post](#post)
|
- [HTTP](#http-26)
|
||||||
+ [Create Post](#create-post)
|
+ [Transfer Community](#transfer-community)
|
||||||
- [Request](#request-26)
|
- [Request](#request-26)
|
||||||
- [Response](#response-26)
|
- [Response](#response-26)
|
||||||
+ [Get Post](#get-post)
|
- [HTTP](#http-27)
|
||||||
|
* [Post](#post)
|
||||||
|
+ [Create Post](#create-post)
|
||||||
- [Request](#request-27)
|
- [Request](#request-27)
|
||||||
- [Response](#response-27)
|
- [Response](#response-27)
|
||||||
+ [Get Posts](#get-posts)
|
- [HTTP](#http-28)
|
||||||
|
+ [Get Post](#get-post)
|
||||||
- [Request](#request-28)
|
- [Request](#request-28)
|
||||||
- [Response](#response-28)
|
- [Response](#response-28)
|
||||||
+ [Create Post Like](#create-post-like)
|
- [HTTP](#http-29)
|
||||||
|
+ [Get Posts](#get-posts)
|
||||||
- [Request](#request-29)
|
- [Request](#request-29)
|
||||||
- [Response](#response-29)
|
- [Response](#response-29)
|
||||||
+ [Edit Post](#edit-post)
|
- [HTTP](#http-30)
|
||||||
|
+ [Create Post Like](#create-post-like)
|
||||||
- [Request](#request-30)
|
- [Request](#request-30)
|
||||||
- [Response](#response-30)
|
- [Response](#response-30)
|
||||||
+ [Save Post](#save-post)
|
- [HTTP](#http-31)
|
||||||
|
+ [Edit Post](#edit-post)
|
||||||
- [Request](#request-31)
|
- [Request](#request-31)
|
||||||
- [Response](#response-31)
|
- [Response](#response-31)
|
||||||
* [Comment](#comment)
|
- [HTTP](#http-32)
|
||||||
+ [Create Comment](#create-comment)
|
+ [Save Post](#save-post)
|
||||||
- [Request](#request-32)
|
- [Request](#request-32)
|
||||||
- [Response](#response-32)
|
- [Response](#response-32)
|
||||||
+ [Edit Comment](#edit-comment)
|
- [HTTP](#http-33)
|
||||||
|
* [Comment](#comment)
|
||||||
|
+ [Create Comment](#create-comment)
|
||||||
- [Request](#request-33)
|
- [Request](#request-33)
|
||||||
- [Response](#response-33)
|
- [Response](#response-33)
|
||||||
+ [Save Comment](#save-comment)
|
- [HTTP](#http-34)
|
||||||
|
+ [Edit Comment](#edit-comment)
|
||||||
- [Request](#request-34)
|
- [Request](#request-34)
|
||||||
- [Response](#response-34)
|
- [Response](#response-34)
|
||||||
+ [Create Comment Like](#create-comment-like)
|
- [HTTP](#http-35)
|
||||||
|
+ [Save Comment](#save-comment)
|
||||||
- [Request](#request-35)
|
- [Request](#request-35)
|
||||||
- [Response](#response-35)
|
- [Response](#response-35)
|
||||||
|
- [HTTP](#http-36)
|
||||||
|
+ [Create Comment Like](#create-comment-like)
|
||||||
|
- [Request](#request-36)
|
||||||
|
- [Response](#response-36)
|
||||||
|
- [HTTP](#http-37)
|
||||||
* [RSS / Atom feeds](#rss--atom-feeds)
|
* [RSS / Atom feeds](#rss--atom-feeds)
|
||||||
+ [All](#all)
|
+ [All](#all)
|
||||||
+ [Community](#community-1)
|
+ [Community](#community-1)
|
||||||
|
@ -144,13 +189,13 @@
|
||||||
|
|
||||||
Request and response strings are in [JSON format](https://www.json.org).
|
Request and response strings are in [JSON format](https://www.json.org).
|
||||||
|
|
||||||
### WebSocket Endpoint
|
### WebSocket
|
||||||
|
|
||||||
Connect to <code>ws://***host***/api/v1/ws</code> to get started.
|
Connect to <code>ws://***host***/api/v1/ws</code> to get started.
|
||||||
|
|
||||||
If the ***`host`*** supports secure connections, you can use <code>wss://***host***/api/v1/ws</code>.
|
If the ***`host`*** supports secure connections, you can use <code>wss://***host***/api/v1/ws</code>.
|
||||||
|
|
||||||
### Testing with Websocat
|
#### Testing with Websocat
|
||||||
|
|
||||||
[Websocat link](https://github.com/vi/websocat)
|
[Websocat link](https://github.com/vi/websocat)
|
||||||
|
|
||||||
|
@ -159,7 +204,7 @@ If the ***`host`*** supports secure connections, you can use <code>wss://***host
|
||||||
A simple test command:
|
A simple test command:
|
||||||
`{"op": "ListCategories"}`
|
`{"op": "ListCategories"}`
|
||||||
|
|
||||||
### Testing with the WebSocket JavaScript API
|
#### Testing with the WebSocket JavaScript API
|
||||||
|
|
||||||
[WebSocket JavaScript API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
|
[WebSocket JavaScript API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
|
||||||
```javascript
|
```javascript
|
||||||
|
@ -171,6 +216,32 @@ ws.onopen = function () {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
### HTTP
|
||||||
|
|
||||||
|
Endpoints are at <code>http://***host***/api/v1/***endpoint***</code>. They'll be listed below for each action.
|
||||||
|
|
||||||
|
#### Testing with Curl
|
||||||
|
|
||||||
|
##### Get Example
|
||||||
|
|
||||||
|
```
|
||||||
|
curl /community/list?sort=Hot
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Post Example
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -i -H \
|
||||||
|
"Content-Type: application/json" \
|
||||||
|
-X POST \
|
||||||
|
-d '{
|
||||||
|
"comment_id": X,
|
||||||
|
"post_id": X,
|
||||||
|
"score": X,
|
||||||
|
"auth": "..."
|
||||||
|
}' \
|
||||||
|
/comment/like
|
||||||
|
```
|
||||||
|
|
||||||
## Rate limits
|
## Rate limits
|
||||||
|
|
||||||
|
@ -201,6 +272,11 @@ These go wherever there is a `sort` field. The available sort types are:
|
||||||
- `TopYear` - the most upvoted posts/communities of the current year.
|
- `TopYear` - the most upvoted posts/communities of the current year.
|
||||||
- `TopAll` - the most upvoted posts/communities on the current instance.
|
- `TopAll` - the most upvoted posts/communities on the current instance.
|
||||||
|
|
||||||
|
### Websocket vs HTTP
|
||||||
|
|
||||||
|
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
|
||||||
|
- For example, an http login will be a `POST` `{username_or_email: X, password: X}`
|
||||||
|
|
||||||
### User / Authentication / Admin actions
|
### User / Authentication / Admin actions
|
||||||
|
|
||||||
#### Login
|
#### Login
|
||||||
|
@ -220,13 +296,19 @@ The `jwt` string should be stored and used anywhere `auth` is called for.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "Login",
|
||||||
jwt: String
|
data: {
|
||||||
|
jwt: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /user/login`
|
||||||
|
|
||||||
#### Register
|
#### Register
|
||||||
|
|
||||||
Only the first user will be able to be the admin.
|
Only the first user will be able to be the admin.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -245,11 +327,17 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "Register",
|
||||||
jwt: String
|
data: {
|
||||||
|
jwt: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /user/register`
|
||||||
|
|
||||||
#### Get User Details
|
#### Get User Details
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -270,14 +358,20 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetUserDetails",
|
||||||
|
data: {
|
||||||
user: UserView,
|
user: UserView,
|
||||||
follows: Vec<CommunityFollowerView>,
|
follows: Vec<CommunityFollowerView>,
|
||||||
moderates: Vec<CommunityModeratorView>,
|
moderates: Vec<CommunityModeratorView>,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /user`
|
||||||
|
|
||||||
#### Save User Settings
|
#### Save User Settings
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -295,10 +389,16 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "SaveUserSettings",
|
||||||
|
data: {
|
||||||
jwt: String
|
jwt: String
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /save_user_settings`
|
||||||
|
|
||||||
#### Get Replies / Inbox
|
#### Get Replies / Inbox
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -316,10 +416,16 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetReplies",
|
||||||
|
data: {
|
||||||
replies: Vec<ReplyView>,
|
replies: Vec<ReplyView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /user/replies`
|
||||||
|
|
||||||
|
|
||||||
#### Get User Mentions
|
#### Get User Mentions
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -338,11 +444,42 @@ Only the first user will be able to be the admin.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetUserMentions",
|
||||||
|
data: {
|
||||||
mentions: Vec<UserMentionView>,
|
mentions: Vec<UserMentionView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /user/mentions`
|
||||||
|
|
||||||
|
#### Edit User Mention
|
||||||
|
##### Request
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "EditUserMention",
|
||||||
|
data: {
|
||||||
|
user_mention_id: i32,
|
||||||
|
read: Option<bool>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### Response
|
||||||
|
```rust
|
||||||
|
{
|
||||||
|
op: "EditUserMention",
|
||||||
|
data: {
|
||||||
|
mention: UserMentionView,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /user/mention`
|
||||||
|
|
||||||
#### Mark All As Read
|
#### Mark All As Read
|
||||||
|
|
||||||
Marks all user replies and mentions as read.
|
Marks all user replies and mentions as read.
|
||||||
|
@ -359,11 +496,17 @@ Marks all user replies and mentions as read.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "MarkAllAsRead",
|
||||||
|
data: {
|
||||||
replies: Vec<ReplyView>,
|
replies: Vec<ReplyView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /user/mark_all_as_read`
|
||||||
|
|
||||||
#### Delete Account
|
#### Delete Account
|
||||||
|
|
||||||
*Permananently deletes your posts and comments*
|
*Permananently deletes your posts and comments*
|
||||||
|
@ -381,11 +524,17 @@ Marks all user replies and mentions as read.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "DeleteAccount",
|
||||||
|
data: {
|
||||||
jwt: String,
|
jwt: String,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /user/delete_account`
|
||||||
|
|
||||||
#### Add admin
|
#### Add admin
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -401,10 +550,15 @@ Marks all user replies and mentions as read.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "AddAdmin",
|
||||||
|
data: {
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /admin/add`
|
||||||
|
|
||||||
#### Ban user
|
#### Ban user
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -423,11 +577,16 @@ Marks all user replies and mentions as read.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "BanUser",
|
||||||
|
data: {
|
||||||
user: UserView,
|
user: UserView,
|
||||||
banned: bool,
|
banned: bool,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /user/ban`
|
||||||
|
|
||||||
### Site
|
### Site
|
||||||
#### List Categories
|
#### List Categories
|
||||||
|
@ -440,13 +599,19 @@ Marks all user replies and mentions as read.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "ListCategories",
|
||||||
|
data: {
|
||||||
categories: Vec<Category>
|
categories: Vec<Category>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /categories`
|
||||||
|
|
||||||
#### Search
|
#### Search
|
||||||
Search types are `Both, Comments, Posts`.
|
|
||||||
|
Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -459,17 +624,26 @@ Search types are `Both, Comments, Posts`.
|
||||||
sort: String,
|
sort: String,
|
||||||
page: Option<i64>,
|
page: Option<i64>,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
|
auth?: Option<String>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "Search",
|
||||||
|
data: {
|
||||||
|
type_: String,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
|
communities: Vec<CommunityView>,
|
||||||
|
users: Vec<UserView>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /search`
|
||||||
|
|
||||||
#### Get Modlog
|
#### Get Modlog
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -487,7 +661,8 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetModlog",
|
||||||
|
data: {
|
||||||
removed_posts: Vec<ModRemovePostView>,
|
removed_posts: Vec<ModRemovePostView>,
|
||||||
locked_posts: Vec<ModLockPostView>,
|
locked_posts: Vec<ModLockPostView>,
|
||||||
removed_comments: Vec<ModRemoveCommentView>,
|
removed_comments: Vec<ModRemoveCommentView>,
|
||||||
|
@ -497,8 +672,13 @@ Search types are `Both, Comments, Posts`.
|
||||||
added_to_community: Vec<ModAddCommunityView>,
|
added_to_community: Vec<ModAddCommunityView>,
|
||||||
added: Vec<ModAddView>,
|
added: Vec<ModAddView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /modlog`
|
||||||
|
|
||||||
#### Create Site
|
#### Create Site
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -514,11 +694,17 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreateSite",
|
||||||
|
data: {
|
||||||
site: SiteView,
|
site: SiteView,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /site`
|
||||||
|
|
||||||
#### Edit Site
|
#### Edit Site
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -534,10 +720,15 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "EditSite",
|
||||||
|
data: {
|
||||||
site: SiteView,
|
site: SiteView,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /site`
|
||||||
|
|
||||||
#### Get Site
|
#### Get Site
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -549,12 +740,17 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetSite",
|
||||||
|
data: {
|
||||||
site: Option<SiteView>,
|
site: Option<SiteView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /site`
|
||||||
|
|
||||||
#### Transfer Site
|
#### Transfer Site
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -570,12 +766,17 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "TransferSite",
|
||||||
|
data: {
|
||||||
site: Option<SiteView>,
|
site: Option<SiteView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /site/transfer`
|
||||||
|
|
||||||
### Community
|
### Community
|
||||||
#### Get Community
|
#### Get Community
|
||||||
|
@ -593,12 +794,17 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetCommunity",
|
||||||
|
data: {
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /community`
|
||||||
|
|
||||||
#### Create Community
|
#### Create Community
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -617,10 +823,15 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreateCommunity",
|
||||||
|
data: {
|
||||||
community: CommunityView
|
community: CommunityView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community`
|
||||||
|
|
||||||
#### List Communities
|
#### List Communities
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -638,10 +849,15 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "ListCommunities",
|
||||||
|
data: {
|
||||||
communities: Vec<CommunityView>
|
communities: Vec<CommunityView>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /community/list`
|
||||||
|
|
||||||
#### Ban from Community
|
#### Ban from Community
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -661,11 +877,16 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "BanFromCommunity",
|
||||||
|
data: {
|
||||||
user: UserView,
|
user: UserView,
|
||||||
banned: bool,
|
banned: bool,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/ban_user`
|
||||||
|
|
||||||
#### Add Mod to Community
|
#### Add Mod to Community
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -683,10 +904,15 @@ Search types are `Both, Comments, Posts`.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "AddModToCommunity",
|
||||||
|
data: {
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/mod`
|
||||||
|
|
||||||
#### Edit Community
|
#### Edit Community
|
||||||
Mods and admins can remove and lock a community, creators can delete it.
|
Mods and admins can remove and lock a community, creators can delete it.
|
||||||
|
@ -712,10 +938,15 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "EditCommunity",
|
||||||
|
data: {
|
||||||
community: CommunityView
|
community: CommunityView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /community`
|
||||||
|
|
||||||
#### Follow Community
|
#### Follow Community
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -732,10 +963,15 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "FollowCommunity",
|
||||||
|
data: {
|
||||||
community: CommunityView
|
community: CommunityView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/follow`
|
||||||
|
|
||||||
#### Get Followed Communities
|
#### Get Followed Communities
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -750,10 +986,15 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetFollowedCommunities",
|
||||||
|
data: {
|
||||||
communities: Vec<CommunityFollowerView>
|
communities: Vec<CommunityFollowerView>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /user/followed_communities`
|
||||||
|
|
||||||
#### Transfer Community
|
#### Transfer Community
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -770,12 +1011,17 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "TransferCommunity",
|
||||||
|
data: {
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /community/transfer`
|
||||||
|
|
||||||
### Post
|
### Post
|
||||||
#### Create Post
|
#### Create Post
|
||||||
|
@ -795,10 +1041,15 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreatePost",
|
||||||
|
data: {
|
||||||
post: PostView
|
post: PostView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post`
|
||||||
|
|
||||||
#### Get Post
|
#### Get Post
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -814,16 +1065,22 @@ Mods and admins can remove and lock a community, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetPost",
|
||||||
|
data: {
|
||||||
post: PostView,
|
post: PostView,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /post`
|
||||||
|
|
||||||
#### Get Posts
|
#### Get Posts
|
||||||
|
|
||||||
Post listing types are `All, Subscribed, Community`
|
Post listing types are `All, Subscribed, Community`
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -843,12 +1100,18 @@ Post listing types are `All, Subscribed, Community`
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "GetPosts",
|
||||||
|
data: {
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`GET /post/list`
|
||||||
|
|
||||||
#### Create Post Like
|
#### Create Post Like
|
||||||
|
|
||||||
`score` can be 0, -1, or 1
|
`score` can be 0, -1, or 1
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -865,12 +1128,18 @@ Post listing types are `All, Subscribed, Community`
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreatePostLike",
|
||||||
|
data: {
|
||||||
post: PostView
|
post: PostView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/like`
|
||||||
|
|
||||||
#### Edit Post
|
#### Edit Post
|
||||||
|
|
||||||
Mods and admins can remove and lock a post, creators can delete it.
|
Mods and admins can remove and lock a post, creators can delete it.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -895,11 +1164,17 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "EditPost",
|
||||||
|
data: {
|
||||||
post: PostView
|
post: PostView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /post`
|
||||||
|
|
||||||
#### Save Post
|
#### Save Post
|
||||||
##### Request
|
##### Request
|
||||||
```rust
|
```rust
|
||||||
|
@ -915,10 +1190,15 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "SavePost",
|
||||||
|
data: {
|
||||||
post: PostView
|
post: PostView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /post/save`
|
||||||
|
|
||||||
### Comment
|
### Comment
|
||||||
#### Create Comment
|
#### Create Comment
|
||||||
|
@ -938,12 +1218,19 @@ Mods and admins can remove and lock a post, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreateComment",
|
||||||
|
data: {
|
||||||
comment: CommentView
|
comment: CommentView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment`
|
||||||
|
|
||||||
#### Edit Comment
|
#### Edit Comment
|
||||||
|
|
||||||
Mods and admins can remove a comment, creators can delete it.
|
Mods and admins can remove a comment, creators can delete it.
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -967,10 +1254,15 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "EditComment",
|
||||||
|
data: {
|
||||||
comment: CommentView
|
comment: CommentView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`PUT /comment`
|
||||||
|
|
||||||
#### Save Comment
|
#### Save Comment
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -987,12 +1279,18 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "SaveComment",
|
||||||
|
data: {
|
||||||
comment: CommentView
|
comment: CommentView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment/save`
|
||||||
|
|
||||||
#### Create Comment Like
|
#### Create Comment Like
|
||||||
|
|
||||||
`score` can be 0, -1, or 1
|
`score` can be 0, -1, or 1
|
||||||
|
|
||||||
##### Request
|
##### Request
|
||||||
|
@ -1010,10 +1308,15 @@ Mods and admins can remove a comment, creators can delete it.
|
||||||
##### Response
|
##### Response
|
||||||
```rust
|
```rust
|
||||||
{
|
{
|
||||||
op: String,
|
op: "CreateCommentLike",
|
||||||
|
data: {
|
||||||
comment: CommentView
|
comment: CommentView
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
##### HTTP
|
||||||
|
|
||||||
|
`POST /comment/like`
|
||||||
|
|
||||||
### RSS / Atom feeds
|
### RSS / Atom feeds
|
||||||
|
|
2
server/.gitignore
vendored
2
server/.gitignore
vendored
|
@ -2,3 +2,5 @@
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
env_setup.sh
|
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
|
tab_spaces = 2
|
||||||
|
edition="2018"
|
2359
server/Cargo.lock
generated
vendored
2359
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
28
server/Cargo.toml
vendored
28
server/Cargo.toml
vendored
|
@ -5,28 +5,32 @@ authors = ["Dessalines <happydooby@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "1.4.2", features = ["postgres","chrono"] }
|
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
dotenv = "0.14.1"
|
dotenv = "0.15.0"
|
||||||
bcrypt = "0.5.0"
|
bcrypt = "0.6.1"
|
||||||
activitypub = "0.1.5"
|
activitypub = "0.2.0"
|
||||||
chrono = { version = "0.4.7", features = ["serde"] }
|
chrono = { version = "0.4.7", features = ["serde"] }
|
||||||
failure = "0.1.5"
|
failure = "0.1.5"
|
||||||
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
||||||
serde = { version = "1.0.94", features = ["derive"] }
|
serde = { version = "1.0.94", features = ["derive"] }
|
||||||
actix = "0.8.3"
|
actix = "0.9.0"
|
||||||
actix-web = "1.0"
|
actix-web = "2.0.0"
|
||||||
actix-files = "0.1.3"
|
actix-files = "0.2.1"
|
||||||
actix-web-actors = "1.0"
|
actix-web-actors = "2.0.0"
|
||||||
env_logger = "0.6.2"
|
actix-rt = "1.0.0"
|
||||||
|
env_logger = "0.7.1"
|
||||||
rand = "0.7.0"
|
rand = "0.7.0"
|
||||||
strum = "0.15.0"
|
strum = "0.17.1"
|
||||||
strum_macros = "0.15.0"
|
strum_macros = "0.17.1"
|
||||||
jsonwebtoken = "6.0.1"
|
jsonwebtoken = "6.0.1"
|
||||||
regex = "1.1.9"
|
regex = "1.1.9"
|
||||||
lazy_static = "1.3.0"
|
lazy_static = "1.3.0"
|
||||||
lettre = "0.9.2"
|
lettre = "0.9.2"
|
||||||
lettre_email = "0.9.2"
|
lettre_email = "0.9.2"
|
||||||
rust-crypto = "^0.2"
|
sha2 = "0.8.0"
|
||||||
rss = "1.8.0"
|
rss = "1.8.0"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
|
config = "0.10.1"
|
||||||
|
hjson = "0.8.2"
|
||||||
|
url = "1.7.1"
|
||||||
|
|
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
|
58
server/config/defaults.hjson
vendored
Normal file
58
server/config/defaults.hjson
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
# 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"
|
||||||
|
# The url where pictshare is available (this should only be exposed to lemmy, not to the outside)
|
||||||
|
pictshare_url: "http://localhost:80"
|
||||||
|
# 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();
|
34
server/migrations/2020-01-21-001001_create_private_message/down.sql
vendored
Normal file
34
server/migrations/2020-01-21-001001_create_private_message/down.sql
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-- Drop the triggers
|
||||||
|
drop trigger refresh_private_message on private_message;
|
||||||
|
drop function refresh_private_message();
|
||||||
|
|
||||||
|
-- Drop the view and table
|
||||||
|
drop view private_message_view cascade;
|
||||||
|
drop table private_message;
|
||||||
|
|
||||||
|
-- Rebuild the old views
|
||||||
|
drop view user_view cascade;
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- Drop the columns
|
||||||
|
alter table user_ drop column matrix_user_id;
|
90
server/migrations/2020-01-21-001001_create_private_message/up.sql
vendored
Normal file
90
server/migrations/2020-01-21-001001_create_private_message/up.sql
vendored
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
-- Creating private message
|
||||||
|
create table private_message (
|
||||||
|
id serial primary key,
|
||||||
|
creator_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
recipient_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
content text not null,
|
||||||
|
deleted boolean default false not null,
|
||||||
|
read boolean default false not null,
|
||||||
|
published timestamp not null default now(),
|
||||||
|
updated timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create the view and materialized view which has the avatar and creator name
|
||||||
|
create view private_message_view as
|
||||||
|
select
|
||||||
|
pm.*,
|
||||||
|
u.name as creator_name,
|
||||||
|
u.avatar as creator_avatar,
|
||||||
|
u2.name as recipient_name,
|
||||||
|
u2.avatar as recipient_avatar
|
||||||
|
from private_message pm
|
||||||
|
inner join user_ u on u.id = pm.creator_id
|
||||||
|
inner join user_ u2 on u2.id = pm.recipient_id;
|
||||||
|
|
||||||
|
create materialized view private_message_mview as select * from private_message_view;
|
||||||
|
|
||||||
|
create unique index idx_private_message_mview_id on private_message_mview (id);
|
||||||
|
|
||||||
|
-- Create the triggers
|
||||||
|
create or replace function refresh_private_message()
|
||||||
|
returns trigger language plpgsql
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
refresh materialized view concurrently private_message_mview;
|
||||||
|
return null;
|
||||||
|
end $$;
|
||||||
|
|
||||||
|
create trigger refresh_private_message
|
||||||
|
after insert or update or delete or truncate
|
||||||
|
on private_message
|
||||||
|
for each statement
|
||||||
|
execute procedure refresh_private_message();
|
||||||
|
|
||||||
|
-- Update user to include matrix id
|
||||||
|
alter table user_ add column matrix_user_id text unique;
|
||||||
|
|
||||||
|
drop view user_view cascade;
|
||||||
|
create view user_view as
|
||||||
|
select
|
||||||
|
u.id,
|
||||||
|
u.name,
|
||||||
|
u.avatar,
|
||||||
|
u.email,
|
||||||
|
u.matrix_user_id,
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- This is what a group pm table would look like
|
||||||
|
-- Not going to do it now because of the complications
|
||||||
|
--
|
||||||
|
-- create table private_message (
|
||||||
|
-- id serial primary key,
|
||||||
|
-- creator_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
-- content text not null,
|
||||||
|
-- deleted boolean default false not null,
|
||||||
|
-- published timestamp not null default now(),
|
||||||
|
-- updated timestamp
|
||||||
|
-- );
|
||||||
|
--
|
||||||
|
-- create table private_message_recipient (
|
||||||
|
-- id serial primary key,
|
||||||
|
-- private_message_id int references private_message on update cascade on delete cascade not null,
|
||||||
|
-- recipient_id int references user_ on update cascade on delete cascade not null,
|
||||||
|
-- read boolean default false not null,
|
||||||
|
-- published timestamp not null default now(),
|
||||||
|
-- unique(private_message_id, recipient_id)
|
||||||
|
-- )
|
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,10 +1,13 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::send_email;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateComment {
|
pub struct CreateComment {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>,
|
parent_id: Option<i32>,
|
||||||
edit_id: Option<i32>,
|
edit_id: Option<i32>, // TODO this isn't used
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
@ -12,7 +15,7 @@ pub struct CreateComment {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EditComment {
|
pub struct EditComment {
|
||||||
content: String,
|
content: String,
|
||||||
parent_id: Option<i32>,
|
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
creator_id: i32,
|
creator_id: i32,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
|
@ -32,7 +35,6 @@ pub struct SaveComment {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CommentResponse {
|
pub struct CommentResponse {
|
||||||
op: String,
|
|
||||||
pub comment: CommentView,
|
pub comment: CommentView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,26 +47,27 @@ pub struct CreateCommentLike {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommentResponse> for Oper<CreateComment> {
|
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 data: &CreateComment = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
@ -82,24 +85,20 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||||
|
|
||||||
if mention_user.is_ok() {
|
|
||||||
let mention_user_id = mention_user?.id;
|
|
||||||
|
|
||||||
// You can't mention yourself
|
// You can't mention yourself
|
||||||
// At some point, make it so you can't tag the parent creator either
|
// At some point, make it so you can't tag the parent creator either
|
||||||
// This can cause two notifications, one for reply and the other for mention
|
// This can cause two notifications, one for reply and the other for mention
|
||||||
if mention_user_id != user_id {
|
if mention_user.id != user_id {
|
||||||
let user_mention_form = UserMentionForm {
|
let user_mention_form = UserMentionForm {
|
||||||
recipient_id: mention_user_id,
|
recipient_id: mention_user.id,
|
||||||
comment_id: inserted_comment.id,
|
comment_id: inserted_comment.id,
|
||||||
read: None,
|
read: None,
|
||||||
};
|
};
|
||||||
|
@ -109,10 +108,79 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
match UserMention::create(&conn, &user_mention_form) {
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
Ok(_mention) => (),
|
Ok(_mention) => (),
|
||||||
Err(_e) => eprintln!("{}", &_e),
|
Err(_e) => eprintln!("{}", &_e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send an email to those users that have notifications on
|
||||||
|
if mention_user.send_notifications_to_email {
|
||||||
|
if let Some(mention_email) = mention_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Mentioned by {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &mention_email, &mention_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifs to the parent commenter / poster
|
||||||
|
match data.parent_id {
|
||||||
|
Some(parent_id) => {
|
||||||
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
|
if parent_comment.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(comment_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Its a post
|
||||||
|
None => {
|
||||||
|
if post.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, post.creator_id)?;
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(post_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -124,26 +192,24 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommentResponse> for Oper<EditComment> {
|
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 data: &EditComment = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
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());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?;
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("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) {
|
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
|
||||||
|
|
||||||
if mention_user.is_ok() {
|
if mention_user.is_ok() {
|
||||||
let mention_user_id = mention_user?.id;
|
let mention_user_id = mention_user?.id;
|
||||||
|
@ -242,20 +308,18 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
|
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommentResponse> for Oper<SaveComment> {
|
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 data: &SaveComment = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -268,32 +332,30 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match CommentSaved::save(&conn, &comment_saved_form) {
|
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
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 data: &CreateCommentLike = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
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
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err("downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -328,11 +390,11 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
CommentLike::remove(&conn, &like_form)?;
|
CommentLike::remove(&conn, &like_form)?;
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err("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))?;
|
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
comment: liked_comment,
|
comment: liked_comment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diesel::PgConnection;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -10,7 +11,6 @@ pub struct GetCommunity {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetCommunityResponse {
|
pub struct GetCommunityResponse {
|
||||||
op: String,
|
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
|
@ -28,7 +28,6 @@ pub struct CreateCommunity {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CommunityResponse {
|
pub struct CommunityResponse {
|
||||||
op: String,
|
|
||||||
pub community: CommunityView,
|
pub community: CommunityView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +41,6 @@ pub struct ListCommunities {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ListCommunitiesResponse {
|
pub struct ListCommunitiesResponse {
|
||||||
op: String,
|
|
||||||
communities: Vec<CommunityView>,
|
communities: Vec<CommunityView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +56,6 @@ pub struct BanFromCommunity {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct BanFromCommunityResponse {
|
pub struct BanFromCommunityResponse {
|
||||||
op: String,
|
|
||||||
user: UserView,
|
user: UserView,
|
||||||
banned: bool,
|
banned: bool,
|
||||||
}
|
}
|
||||||
|
@ -73,7 +70,6 @@ pub struct AddModToCommunity {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AddModToCommunityResponse {
|
pub struct AddModToCommunityResponse {
|
||||||
op: String,
|
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +102,6 @@ pub struct GetFollowedCommunities {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetFollowedCommunitiesResponse {
|
pub struct GetFollowedCommunitiesResponse {
|
||||||
op: String,
|
|
||||||
communities: Vec<CommunityFollowerView>,
|
communities: Vec<CommunityFollowerView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,9 +113,8 @@ pub struct TransferCommunity {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
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 data: &GetCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let user_id: Option<i32> = match &data.auth {
|
let user_id: Option<i32> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
@ -136,21 +130,24 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
let community_id = match data.id {
|
let community_id = match data.id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
match Community::read_from_name(&conn, data.name.to_owned().unwrap_or("main".to_string())) {
|
match Community::read_from_name(
|
||||||
|
&conn,
|
||||||
|
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
|
||||||
|
) {
|
||||||
Ok(community) => community.id,
|
Ok(community) => community.id,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
|
@ -161,7 +158,6 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetCommunityResponse {
|
Ok(GetCommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
admins,
|
||||||
|
@ -170,27 +166,26 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
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 data: &CreateCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| has_slurs(&data.title)
|
|| has_slurs(&data.title)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When you create a community, make sure the user becomes a moderator and a follower
|
// When you create a community, make sure the user becomes a moderator and a follower
|
||||||
|
@ -208,7 +203,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
|
|
||||||
let inserted_community = match Community::create(&conn, &community_form) {
|
let inserted_community = match Community::create(&conn, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists"))?,
|
Err(_e) => return Err(APIError::err("community_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let community_moderator_form = CommunityModeratorForm {
|
||||||
|
@ -219,12 +214,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
let _inserted_community_moderator =
|
let _inserted_community_moderator =
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
@ -235,38 +225,35 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommunityResponse {
|
Ok(CommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
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;
|
let data: &EditCommunity = &self.data;
|
||||||
|
|
||||||
if has_slurs(&data.name) || has_slurs(&data.title) {
|
if has_slurs(&data.name) || has_slurs(&data.title) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify its a mod
|
// 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());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_community_edit_allowed"))?;
|
return Err(APIError::err("no_community_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
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) {
|
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -318,16 +305,14 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
|
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommunityResponse {
|
Ok(CommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
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 data: &ListCommunities = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
@ -351,28 +336,24 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||||
|
|
||||||
let communities = CommunityQueryBuilder::create(&conn)
|
let communities = CommunityQueryBuilder::create(&conn)
|
||||||
.sort(&sort)
|
.sort(&sort)
|
||||||
.from_user_id(user_id)
|
.for_user(user_id)
|
||||||
.show_nsfw(show_nsfw)
|
.show_nsfw(show_nsfw)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(ListCommunitiesResponse {
|
Ok(ListCommunitiesResponse { communities })
|
||||||
op: self.op.to_string(),
|
|
||||||
communities,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
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 data: &FollowCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -385,32 +366,30 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
if data.follow {
|
if data.follow {
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
|
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(CommunityResponse {
|
Ok(CommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
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 data: &GetFollowedCommunities = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -418,25 +397,21 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
let communities: Vec<CommunityFollowerView> =
|
let communities: Vec<CommunityFollowerView> =
|
||||||
match CommunityFollowerView::for_user(&conn, user_id) {
|
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||||
Ok(communities) => communities,
|
Ok(communities) => communities,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "system_err_login"))?,
|
Err(_e) => return Err(APIError::err("system_err_login").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetFollowedCommunitiesResponse {
|
Ok(GetFollowedCommunitiesResponse { communities })
|
||||||
op: self.op.to_string(),
|
|
||||||
communities,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
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 data: &BanFromCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -449,12 +424,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
if data.ban {
|
if data.ban {
|
||||||
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +452,6 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
let user_view = UserView::read(&conn, data.user_id)?;
|
let user_view = UserView::read(&conn, data.user_id)?;
|
||||||
|
|
||||||
Ok(BanFromCommunityResponse {
|
Ok(BanFromCommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
user: user_view,
|
user: user_view,
|
||||||
banned: data.ban,
|
banned: data.ban,
|
||||||
})
|
})
|
||||||
|
@ -485,13 +459,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
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 data: &AddModToCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -504,22 +477,12 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
if data.added {
|
if data.added {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,21 +497,17 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
|
|
||||||
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
||||||
|
|
||||||
Ok(AddModToCommunityResponse {
|
Ok(AddModToCommunityResponse { moderators })
|
||||||
op: self.op.to_string(),
|
|
||||||
moderators,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
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 data: &TransferCommunity = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -562,14 +521,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
// Make sure user is the creator, or an admin
|
// Make sure user is the creator, or an admin
|
||||||
if user_id != read_community.creator_id
|
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
|
||||||
&& !admins
|
return Err(APIError::err("not_an_admin").into());
|
||||||
.iter()
|
|
||||||
.map(|a| a.id)
|
|
||||||
.collect::<Vec<i32>>()
|
|
||||||
.contains(&user_id)
|
|
||||||
{
|
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
|
@ -586,7 +539,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// You also have to re-do the community_moderator table, reordering it.
|
// You also have to re-do the community_moderator table, reordering it.
|
||||||
|
@ -609,12 +562,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
let _inserted_community_moderator =
|
let _inserted_community_moderator =
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,17 +577,16 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetCommunityResponse {
|
Ok(GetCommunityResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
admins,
|
||||||
|
|
|
@ -8,6 +8,8 @@ use crate::db::moderator_views::*;
|
||||||
use crate::db::password_reset_request::*;
|
use crate::db::password_reset_request::*;
|
||||||
use crate::db::post::*;
|
use crate::db::post::*;
|
||||||
use crate::db::post_view::*;
|
use crate::db::post_view::*;
|
||||||
|
use crate::db::private_message::*;
|
||||||
|
use crate::db::private_message_view::*;
|
||||||
use crate::db::site::*;
|
use crate::db::site::*;
|
||||||
use crate::db::site_view::*;
|
use crate::db::site_view::*;
|
||||||
use crate::db::user::*;
|
use crate::db::user::*;
|
||||||
|
@ -15,7 +17,8 @@ use crate::db::user_mention::*;
|
||||||
use crate::db::user_mention_view::*;
|
use crate::db::user_mention_view::*;
|
||||||
use crate::db::user_view::*;
|
use crate::db::user_view::*;
|
||||||
use crate::db::*;
|
use crate::db::*;
|
||||||
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs};
|
||||||
|
use diesel::PgConnection;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -25,78 +28,32 @@ pub mod post;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
pub mod user;
|
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)]
|
#[derive(Fail, Debug)]
|
||||||
#[fail(display = "{{\"op\":\"{}\", \"error\":\"{}\"}}", op, message)]
|
#[fail(display = "{{\"error\":\"{}\"}}", message)]
|
||||||
pub struct APIError {
|
pub struct APIError {
|
||||||
pub op: String,
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl APIError {
|
impl APIError {
|
||||||
pub fn err(op: &UserOperation, msg: &str) -> Self {
|
pub fn err(msg: &str) -> Self {
|
||||||
APIError {
|
APIError {
|
||||||
op: op.to_string(),
|
|
||||||
message: msg.to_string(),
|
message: msg.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Oper<T> {
|
pub struct Oper<T> {
|
||||||
op: UserOperation,
|
|
||||||
data: T,
|
data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Oper<T> {
|
impl<T> Oper<T> {
|
||||||
pub fn new(op: UserOperation, data: T) -> Oper<T> {
|
pub fn new(data: T) -> Oper<T> {
|
||||||
Oper { op, data }
|
Oper { data }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Perform<T> {
|
pub trait Perform<T> {
|
||||||
fn perform(&self) -> Result<T, Error>
|
fn perform(&self, conn: &PgConnection) -> Result<T, Error>
|
||||||
where
|
where
|
||||||
T: Sized;
|
T: Sized;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diesel::PgConnection;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -13,7 +14,6 @@ pub struct CreatePost {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct PostResponse {
|
pub struct PostResponse {
|
||||||
op: String,
|
|
||||||
pub post: PostView,
|
pub post: PostView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ pub struct GetPost {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetPostResponse {
|
pub struct GetPostResponse {
|
||||||
op: String,
|
|
||||||
post: PostView,
|
post: PostView,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
community: CommunityView,
|
community: CommunityView,
|
||||||
|
@ -45,7 +44,6 @@ pub struct GetPosts {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetPostsResponse {
|
pub struct GetPostsResponse {
|
||||||
op: String,
|
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +56,6 @@ pub struct CreatePostLike {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreatePostLikeResponse {
|
pub struct CreatePostLikeResponse {
|
||||||
op: String,
|
|
||||||
post: PostView,
|
post: PostView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,29 +84,28 @@ pub struct SavePost {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<PostResponse> for Oper<CreatePost> {
|
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 data: &CreatePost = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
|
@ -128,7 +124,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let inserted_post = match Post::create(&conn, &post_form) {
|
let inserted_post = match Post::create(&conn, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// They like their own post by default
|
// 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
|
// Only add the like if the score isnt 0
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostResponse {
|
Ok(PostResponse { post: post_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
post: post_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetPostResponse> for Oper<GetPost> {
|
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 data: &GetPost = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let user_id: Option<i32> = match &data.auth {
|
let user_id: Option<i32> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&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) {
|
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comments = CommentQueryBuilder::create(&conn)
|
let comments = CommentQueryBuilder::create(&conn)
|
||||||
|
@ -196,7 +188,6 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetPostResponse {
|
Ok(GetPostResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
post: post_view,
|
post: post_view,
|
||||||
comments,
|
comments,
|
||||||
community,
|
community,
|
||||||
|
@ -207,9 +198,8 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
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 data: &GetPosts = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
@ -243,24 +233,20 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||||
.list()
|
.list()
|
||||||
{
|
{
|
||||||
Ok(posts) => posts,
|
Ok(posts) => posts,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts"))?,
|
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(GetPostsResponse {
|
Ok(GetPostsResponse { posts })
|
||||||
op: self.op.to_string(),
|
|
||||||
posts,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
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 data: &CreatePostLike = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
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
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err("downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = PostLikeForm {
|
let like_form = PostLikeForm {
|
||||||
|
@ -294,39 +280,34 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
PostLike::remove(&conn, &like_form)?;
|
PostLike::remove(&conn, &like_form)?;
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// just output the score
|
// just output the score
|
||||||
Ok(CreatePostLikeResponse {
|
Ok(CreatePostLikeResponse { post: post_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
post: post_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<PostResponse> for Oper<EditPost> {
|
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;
|
let data: &EditPost = &self.data;
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
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());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_post_edit_allowed"))?;
|
return Err(APIError::err("no_post_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
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) {
|
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -404,21 +385,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
|
|
||||||
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(PostResponse {
|
Ok(PostResponse { post: post_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
post: post_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<PostResponse> for Oper<SavePost> {
|
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 data: &SavePost = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -431,20 +408,17 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match PostSaved::save(&conn, &post_saved_form) {
|
match PostSaved::save(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match PostSaved::unsave(&conn, &post_saved_form) {
|
match PostSaved::unsave(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
|
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
|
||||||
|
|
||||||
Ok(PostResponse {
|
Ok(PostResponse { post: post_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
post: post_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use diesel::PgConnection;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -6,7 +7,6 @@ pub struct ListCategories;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ListCategoriesResponse {
|
pub struct ListCategoriesResponse {
|
||||||
op: String,
|
|
||||||
categories: Vec<Category>,
|
categories: Vec<Category>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ pub struct Search {
|
||||||
sort: String,
|
sort: String,
|
||||||
page: Option<i64>,
|
page: Option<i64>,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
|
auth: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SearchResponse {
|
pub struct SearchResponse {
|
||||||
op: String,
|
|
||||||
type_: String,
|
type_: String,
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
|
@ -40,7 +40,6 @@ pub struct GetModlog {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetModlogResponse {
|
pub struct GetModlogResponse {
|
||||||
op: String,
|
|
||||||
removed_posts: Vec<ModRemovePostView>,
|
removed_posts: Vec<ModRemovePostView>,
|
||||||
locked_posts: Vec<ModLockPostView>,
|
locked_posts: Vec<ModLockPostView>,
|
||||||
stickied_posts: Vec<ModStickyPostView>,
|
stickied_posts: Vec<ModStickyPostView>,
|
||||||
|
@ -77,13 +76,11 @@ pub struct GetSite;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SiteResponse {
|
pub struct SiteResponse {
|
||||||
op: String,
|
|
||||||
site: SiteView,
|
site: SiteView,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetSiteResponse {
|
pub struct GetSiteResponse {
|
||||||
op: String,
|
|
||||||
site: Option<SiteView>,
|
site: Option<SiteView>,
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
banned: Vec<UserView>,
|
banned: Vec<UserView>,
|
||||||
|
@ -97,24 +94,19 @@ pub struct TransferSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
|
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 _data: &ListCategories = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let categories: Vec<Category> = Category::list_all(&conn)?;
|
let categories: Vec<Category> = Category::list_all(&conn)?;
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(ListCategoriesResponse {
|
Ok(ListCategoriesResponse { categories })
|
||||||
op: self.op.to_string(),
|
|
||||||
categories,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
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 data: &GetModlog = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let removed_posts = ModRemovePostView::list(
|
let removed_posts = ModRemovePostView::list(
|
||||||
&conn,
|
&conn,
|
||||||
|
@ -160,20 +152,18 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// These arrays are only for the full modlog, when a community isn't given
|
// These arrays are only for the full modlog, when a community isn't given
|
||||||
let mut removed_communities = Vec::new();
|
let (removed_communities, banned, added) = if data.community_id.is_none() {
|
||||||
let mut banned = Vec::new();
|
(
|
||||||
let mut added = Vec::new();
|
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
|
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
if data.community_id.is_none() {
|
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
removed_communities =
|
)
|
||||||
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
} else {
|
||||||
banned = ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
added = ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetModlogResponse {
|
Ok(GetModlogResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
removed_posts,
|
removed_posts,
|
||||||
locked_posts,
|
locked_posts,
|
||||||
stickied_posts,
|
stickied_posts,
|
||||||
|
@ -188,26 +178,25 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<SiteResponse> for Oper<CreateSite> {
|
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 data: &CreateSite = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if !UserView::read(&conn, user_id)?.admin {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -222,39 +211,35 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
|
|
||||||
match Site::create(&conn, &site_form) {
|
match Site::create(&conn, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists"))?,
|
Err(_e) => return Err(APIError::err("site_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
Ok(SiteResponse {
|
Ok(SiteResponse { site: site_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
site: site_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<SiteResponse> for Oper<EditSite> {
|
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 data: &EditSite = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let found_site = Site::read(&conn, 1)?;
|
let found_site = Site::read(&conn, 1)?;
|
||||||
|
@ -271,22 +256,18 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
Ok(SiteResponse {
|
Ok(SiteResponse { site: site_view })
|
||||||
op: self.op.to_string(),
|
|
||||||
site: site_view,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetSiteResponse> for Oper<GetSite> {
|
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 _data: &GetSite = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
// It can return a null site in order to redirect
|
// It can return a null site in order to redirect
|
||||||
let site_view = match Site::read(&conn, 1) {
|
let site_view = match Site::read(&conn, 1) {
|
||||||
|
@ -305,7 +286,6 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
||||||
let banned = UserView::banned(&conn)?;
|
let banned = UserView::banned(&conn)?;
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
site: site_view,
|
site: site_view,
|
||||||
admins,
|
admins,
|
||||||
banned,
|
banned,
|
||||||
|
@ -315,9 +295,19 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<SearchResponse> for Oper<Search> {
|
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 data: &Search = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
let user_id: Option<i32> = match &data.auth {
|
||||||
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
Ok(claims) => {
|
||||||
|
let user_id = claims.claims.id;
|
||||||
|
Some(user_id)
|
||||||
|
}
|
||||||
|
Err(_e) => None,
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
let type_ = SearchType::from_str(&data.type_)?;
|
let type_ = SearchType::from_str(&data.type_)?;
|
||||||
|
@ -336,6 +326,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
.show_nsfw(true)
|
.show_nsfw(true)
|
||||||
.for_community_id(data.community_id)
|
.for_community_id(data.community_id)
|
||||||
.search_term(data.q.to_owned())
|
.search_term(data.q.to_owned())
|
||||||
|
.my_user_id(user_id)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
@ -344,6 +335,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
comments = CommentQueryBuilder::create(&conn)
|
comments = CommentQueryBuilder::create(&conn)
|
||||||
.sort(&sort)
|
.sort(&sort)
|
||||||
.search_term(data.q.to_owned())
|
.search_term(data.q.to_owned())
|
||||||
|
.my_user_id(user_id)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
@ -370,6 +362,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
.show_nsfw(true)
|
.show_nsfw(true)
|
||||||
.for_community_id(data.community_id)
|
.for_community_id(data.community_id)
|
||||||
.search_term(data.q.to_owned())
|
.search_term(data.q.to_owned())
|
||||||
|
.my_user_id(user_id)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
@ -377,6 +370,7 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
comments = CommentQueryBuilder::create(&conn)
|
comments = CommentQueryBuilder::create(&conn)
|
||||||
.sort(&sort)
|
.sort(&sort)
|
||||||
.search_term(data.q.to_owned())
|
.search_term(data.q.to_owned())
|
||||||
|
.my_user_id(user_id)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
@ -409,7 +403,6 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(SearchResponse {
|
Ok(SearchResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
type_: data.type_.to_owned(),
|
type_: data.type_.to_owned(),
|
||||||
comments,
|
comments,
|
||||||
posts,
|
posts,
|
||||||
|
@ -420,13 +413,12 @@ impl Perform<SearchResponse> for Oper<Search> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
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 data: &TransferSite = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -435,7 +427,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
// Make sure user is the creator
|
// Make sure user is the creator
|
||||||
if read_site.creator_id != user_id {
|
if read_site.creator_id != user_id {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -450,7 +442,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -475,7 +467,6 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
let banned = UserView::banned(&conn)?;
|
let banned = UserView::banned(&conn)?;
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
site: Some(site_view),
|
site: Some(site_view),
|
||||||
admins,
|
admins,
|
||||||
banned,
|
banned,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
use crate::{generate_random_string, send_email};
|
use crate::{generate_random_string, send_email};
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
|
use diesel::PgConnection;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -26,12 +28,19 @@ pub struct SaveUserSettings {
|
||||||
default_sort_type: i16,
|
default_sort_type: i16,
|
||||||
default_listing_type: i16,
|
default_listing_type: i16,
|
||||||
lang: String,
|
lang: String,
|
||||||
|
avatar: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
matrix_user_id: Option<String>,
|
||||||
|
new_password: Option<String>,
|
||||||
|
new_password_verify: Option<String>,
|
||||||
|
old_password: Option<String>,
|
||||||
|
show_avatars: bool,
|
||||||
|
send_notifications_to_email: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
op: String,
|
|
||||||
jwt: String,
|
jwt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +58,6 @@ pub struct GetUserDetails {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetUserDetailsResponse {
|
pub struct GetUserDetailsResponse {
|
||||||
op: String,
|
|
||||||
user: UserView,
|
user: UserView,
|
||||||
follows: Vec<CommunityFollowerView>,
|
follows: Vec<CommunityFollowerView>,
|
||||||
moderates: Vec<CommunityModeratorView>,
|
moderates: Vec<CommunityModeratorView>,
|
||||||
|
@ -60,13 +68,11 @@ pub struct GetUserDetailsResponse {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetRepliesResponse {
|
pub struct GetRepliesResponse {
|
||||||
op: String,
|
|
||||||
replies: Vec<ReplyView>,
|
replies: Vec<ReplyView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetUserMentionsResponse {
|
pub struct GetUserMentionsResponse {
|
||||||
op: String,
|
|
||||||
mentions: Vec<UserMentionView>,
|
mentions: Vec<UserMentionView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +90,6 @@ pub struct AddAdmin {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AddAdminResponse {
|
pub struct AddAdminResponse {
|
||||||
op: String,
|
|
||||||
admins: Vec<UserView>,
|
admins: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +104,6 @@ pub struct BanUser {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct BanUserResponse {
|
pub struct BanUserResponse {
|
||||||
op: String,
|
|
||||||
user: UserView,
|
user: UserView,
|
||||||
banned: bool,
|
banned: bool,
|
||||||
}
|
}
|
||||||
|
@ -131,7 +135,6 @@ pub struct EditUserMention {
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct UserMentionResponse {
|
pub struct UserMentionResponse {
|
||||||
op: String,
|
|
||||||
mention: UserMentionView,
|
mention: UserMentionView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,9 +150,7 @@ pub struct PasswordReset {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct PasswordResetResponse {
|
pub struct PasswordResetResponse {}
|
||||||
op: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct PasswordChange {
|
pub struct PasswordChange {
|
||||||
|
@ -158,67 +159,93 @@ pub struct PasswordChange {
|
||||||
password_verify: String,
|
password_verify: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CreatePrivateMessage {
|
||||||
|
content: String,
|
||||||
|
recipient_id: i32,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct EditPrivateMessage {
|
||||||
|
edit_id: i32,
|
||||||
|
content: Option<String>,
|
||||||
|
deleted: Option<bool>,
|
||||||
|
read: Option<bool>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GetPrivateMessages {
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PrivateMessagesResponse {
|
||||||
|
messages: Vec<PrivateMessageView>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PrivateMessageResponse {
|
||||||
|
message: PrivateMessageView,
|
||||||
|
}
|
||||||
|
|
||||||
impl Perform<LoginResponse> for Oper<Login> {
|
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 data: &Login = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
// Fetch that username / email
|
// Fetch that username / email
|
||||||
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err("password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse { jwt: user.jwt() })
|
||||||
op: self.op.to_string(),
|
|
||||||
jwt: user.jwt(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<LoginResponse> for Oper<Register> {
|
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 data: &Register = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
// Make sure site has open registration
|
// Make sure site has open registration
|
||||||
if let Ok(site) = SiteView::read(&conn) {
|
if let Ok(site) = SiteView::read(&conn) {
|
||||||
if !site.open_registration {
|
if !site.open_registration {
|
||||||
return Err(APIError::err(&self.op, "registration_closed"))?;
|
return Err(APIError::err("registration_closed").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err("passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_slurs(&data.username) {
|
if has_slurs(&data.username) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err("no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are no admins
|
// Make sure there are no admins
|
||||||
if data.admin && UserView::admins(&conn)?.len() > 0 {
|
if data.admin && !UserView::admins(&conn)?.is_empty() {
|
||||||
return Err(APIError::err(&self.op, "admin_already_created"))?;
|
return Err(APIError::err("admin_already_created").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the new user
|
// Register the new user
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: data.username.to_owned(),
|
name: data.username.to_owned(),
|
||||||
fedi_name: Settings::get().hostname.into(),
|
fedi_name: Settings::get().hostname.to_owned(),
|
||||||
email: data.email.to_owned(),
|
email: data.email.to_owned(),
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
password_encrypted: data.password.to_owned(),
|
password_encrypted: data.password.to_owned(),
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -229,12 +256,24 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the user
|
// Create the user
|
||||||
let inserted_user = match User_::register(&conn, &user_form) {
|
let inserted_user = match User_::register(&conn, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?,
|
Err(e) => {
|
||||||
|
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
|
// Create the main community if it doesn't exist
|
||||||
|
@ -265,7 +304,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If its an admin, add them as a mod and follower to main
|
// If its an admin, add them as a mod and follower to main
|
||||||
|
@ -278,42 +317,70 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
let _inserted_community_moderator =
|
let _inserted_community_moderator =
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
jwt: inserted_user.jwt(),
|
jwt: inserted_user.jwt(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
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 data: &SaveUserSettings = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
let read_user = User_::read(&conn, user_id)?;
|
||||||
|
|
||||||
|
let email = match &data.email {
|
||||||
|
Some(email) => Some(email.to_owned()),
|
||||||
|
None => read_user.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
let password_encrypted = match &data.new_password {
|
||||||
|
Some(new_password) => {
|
||||||
|
match &data.new_password_verify {
|
||||||
|
Some(new_password_verify) => {
|
||||||
|
// Make sure passwords match
|
||||||
|
if new_password != new_password_verify {
|
||||||
|
return Err(APIError::err("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 {
|
let user_form = UserForm {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email,
|
||||||
password_encrypted: read_user.password_encrypted,
|
matrix_user_id: data.matrix_user_id.to_owned(),
|
||||||
|
avatar: data.avatar.to_owned(),
|
||||||
|
password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
admin: read_user.admin,
|
admin: read_user.admin,
|
||||||
|
@ -323,25 +390,35 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
default_sort_type: data.default_sort_type,
|
default_sort_type: data.default_sort_type,
|
||||||
default_listing_type: data.default_listing_type,
|
default_listing_type: data.default_listing_type,
|
||||||
lang: data.lang.to_owned(),
|
lang: data.lang.to_owned(),
|
||||||
|
show_avatars: data.show_avatars,
|
||||||
|
send_notifications_to_email: data.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(e) => {
|
||||||
|
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
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
jwt: updated_user.jwt(),
|
jwt: updated_user.jwt(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
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 data: &GetUserDetails = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
|
@ -366,11 +443,16 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
let user_details_id = match data.user_id {
|
let user_details_id = match data.user_id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
User_::read_from_name(
|
match User_::read_from_name(
|
||||||
&conn,
|
&conn,
|
||||||
data.username.to_owned().unwrap_or("admin".to_string()),
|
data
|
||||||
)?
|
.username
|
||||||
.id
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| "admin".to_string()),
|
||||||
|
) {
|
||||||
|
Ok(user) => user.id,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -412,7 +494,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetUserDetailsResponse {
|
Ok(GetUserDetailsResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
user: user_view,
|
user: user_view,
|
||||||
follows,
|
follows,
|
||||||
moderates,
|
moderates,
|
||||||
|
@ -424,28 +505,30 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
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 data: &AddAdmin = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
|
||||||
|
// TODO make addadmin easier
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email: read_user.email,
|
||||||
|
matrix_user_id: read_user.matrix_user_id,
|
||||||
|
avatar: read_user.avatar,
|
||||||
password_encrypted: read_user.password_encrypted,
|
password_encrypted: read_user.password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
|
@ -456,11 +539,13 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
default_sort_type: read_user.default_sort_type,
|
default_sort_type: read_user.default_sort_type,
|
||||||
default_listing_type: read_user.default_listing_type,
|
default_listing_type: read_user.default_listing_type,
|
||||||
lang: read_user.lang,
|
lang: read_user.lang,
|
||||||
|
show_avatars: read_user.show_avatars,
|
||||||
|
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -478,36 +563,35 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
let creator_user = admins.remove(creator_index);
|
let creator_user = admins.remove(creator_index);
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
Ok(AddAdminResponse {
|
Ok(AddAdminResponse { admins })
|
||||||
op: self.op.to_string(),
|
|
||||||
admins,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<BanUserResponse> for Oper<BanUser> {
|
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 data: &BanUser = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
|
||||||
|
// TODO make bans and addadmins easier
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email: read_user.email,
|
||||||
|
matrix_user_id: read_user.matrix_user_id,
|
||||||
|
avatar: read_user.avatar,
|
||||||
password_encrypted: read_user.password_encrypted,
|
password_encrypted: read_user.password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username: read_user.preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
|
@ -518,11 +602,13 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
default_sort_type: read_user.default_sort_type,
|
default_sort_type: read_user.default_sort_type,
|
||||||
default_listing_type: read_user.default_listing_type,
|
default_listing_type: read_user.default_listing_type,
|
||||||
lang: read_user.lang,
|
lang: read_user.lang,
|
||||||
|
show_avatars: read_user.show_avatars,
|
||||||
|
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||||
};
|
};
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -544,7 +630,6 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
let user_view = UserView::read(&conn, data.user_id)?;
|
let user_view = UserView::read(&conn, data.user_id)?;
|
||||||
|
|
||||||
Ok(BanUserResponse {
|
Ok(BanUserResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
user: user_view,
|
user: user_view,
|
||||||
banned: data.ban,
|
banned: data.ban,
|
||||||
})
|
})
|
||||||
|
@ -552,13 +637,12 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
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 data: &GetReplies = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -572,21 +656,17 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
|
||||||
Ok(GetRepliesResponse {
|
Ok(GetRepliesResponse { replies })
|
||||||
op: self.op.to_string(),
|
|
||||||
replies,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
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 data: &GetUserMentions = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -600,21 +680,17 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
|
||||||
Ok(GetUserMentionsResponse {
|
Ok(GetUserMentionsResponse { mentions })
|
||||||
op: self.op.to_string(),
|
|
||||||
mentions,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
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 data: &EditUserMention = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -630,26 +706,24 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
let _updated_user_mention =
|
let _updated_user_mention =
|
||||||
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||||
|
|
||||||
Ok(UserMentionResponse {
|
Ok(UserMentionResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
mention: user_mention_view,
|
mention: user_mention_view,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
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 data: &MarkAllAsRead = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -674,7 +748,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,25 +769,45 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
let _updated_mention =
|
let _updated_mention =
|
||||||
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||||
Ok(mention) => mention,
|
Ok(mention) => mention,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GetRepliesResponse {
|
// messages
|
||||||
op: self.op.to_string(),
|
let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
|
||||||
replies: vec![],
|
.page(1)
|
||||||
})
|
.limit(999)
|
||||||
|
.unread_only(true)
|
||||||
|
.list()?;
|
||||||
|
|
||||||
|
for message in &messages {
|
||||||
|
let private_message_form = PrivateMessageForm {
|
||||||
|
content: None,
|
||||||
|
creator_id: message.to_owned().creator_id,
|
||||||
|
recipient_id: message.to_owned().recipient_id,
|
||||||
|
deleted: None,
|
||||||
|
read: Some(true),
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
|
||||||
|
{
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(GetRepliesResponse { replies: vec![] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
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 data: &DeleteAccount = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -723,7 +817,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err("password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
|
@ -746,7 +840,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,31 +868,24 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
jwt: data.auth.to_owned(),
|
jwt: data.auth.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
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 data: &PasswordReset = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
// Fetch that email
|
// Fetch that email
|
||||||
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a random token
|
// Generate a random token
|
||||||
|
@ -815,57 +902,172 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||||
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
||||||
match send_email(subject, user_email, &user.name, html) {
|
match send_email(subject, user_email, &user.name, html) {
|
||||||
Ok(_o) => _o,
|
Ok(_o) => _o,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?,
|
Err(_e) => return Err(APIError::err(&_e).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PasswordResetResponse {
|
Ok(PasswordResetResponse {})
|
||||||
op: self.op.to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Perform<LoginResponse> for Oper<PasswordChange> {
|
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 data: &PasswordChange = &self.data;
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
// Fetch the user_id from the token
|
// Fetch the user_id from the token
|
||||||
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err("passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the user
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
|
||||||
|
|
||||||
// Update the user with the new password
|
// Update the user with the new password
|
||||||
let user_form = UserForm {
|
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
|
||||||
name: read_user.name,
|
|
||||||
fedi_name: read_user.fedi_name,
|
|
||||||
email: read_user.email,
|
|
||||||
password_encrypted: data.password.to_owned(),
|
|
||||||
preferred_username: read_user.preferred_username,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
admin: read_user.admin,
|
|
||||||
banned: read_user.banned,
|
|
||||||
show_nsfw: read_user.show_nsfw,
|
|
||||||
theme: read_user.theme,
|
|
||||||
default_sort_type: read_user.default_sort_type,
|
|
||||||
default_listing_type: read_user.default_listing_type,
|
|
||||||
lang: read_user.lang,
|
|
||||||
};
|
|
||||||
|
|
||||||
let updated_user = match User_::update_password(&conn, user_id, &user_form) {
|
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
Ok(LoginResponse {
|
||||||
op: self.op.to_string(),
|
|
||||||
jwt: updated_user.jwt(),
|
jwt: updated_user.jwt(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
||||||
|
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||||
|
let data: &CreatePrivateMessage = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
|
||||||
|
let private_message_form = PrivateMessageForm {
|
||||||
|
content: Some(content_slurs_removed.to_owned()),
|
||||||
|
creator_id: user_id,
|
||||||
|
recipient_id: data.recipient_id,
|
||||||
|
deleted: None,
|
||||||
|
read: None,
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
|
||||||
|
Ok(private_message) => private_message,
|
||||||
|
Err(_e) => {
|
||||||
|
return Err(APIError::err("couldnt_create_private_message").into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send notifications to the recipient
|
||||||
|
let recipient_user = User_::read(&conn, data.recipient_id)?;
|
||||||
|
if recipient_user.send_notifications_to_email {
|
||||||
|
if let Some(email) = recipient_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Private Message from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, &content_slurs_removed, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &email, &recipient_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => eprintln!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
|
||||||
|
|
||||||
|
Ok(PrivateMessageResponse { message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
|
||||||
|
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||||
|
let data: &EditPrivateMessage = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to make sure they are the creator (or the recipient marking as read
|
||||||
|
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|
||||||
|
|| orig_private_message.creator_id.eq(&user_id))
|
||||||
|
{
|
||||||
|
return Err(APIError::err("no_private_message_edit_allowed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_slurs_removed = match &data.content {
|
||||||
|
Some(content) => Some(remove_slurs(content)),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let private_message_form = PrivateMessageForm {
|
||||||
|
content: content_slurs_removed,
|
||||||
|
creator_id: orig_private_message.creator_id,
|
||||||
|
recipient_id: orig_private_message.recipient_id,
|
||||||
|
deleted: data.deleted.to_owned(),
|
||||||
|
read: data.read.to_owned(),
|
||||||
|
updated: if data.read.is_some() {
|
||||||
|
orig_private_message.updated
|
||||||
|
} else {
|
||||||
|
Some(naive_now())
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let _updated_private_message =
|
||||||
|
match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
|
||||||
|
Ok(private_message) => private_message,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = PrivateMessageView::read(&conn, data.edit_id)?;
|
||||||
|
|
||||||
|
Ok(PrivateMessageResponse { message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
|
||||||
|
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
|
||||||
|
let data: &GetPrivateMessages = &self.data;
|
||||||
|
|
||||||
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
Ok(claims) => claims.claims,
|
||||||
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
|
||||||
|
.page(data.page)
|
||||||
|
.limit(data.limit)
|
||||||
|
.unread_only(data.unread_only)
|
||||||
|
.list()?;
|
||||||
|
|
||||||
|
Ok(PrivateMessagesResponse { messages })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
103
server/src/apub/mod.rs
Normal file
103
server/src/apub/mod.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
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,
|
||||||
|
matrix_user_id: 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::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let categories = Category::list_all(&conn).unwrap();
|
let categories = Category::list_all(&conn).unwrap();
|
||||||
let expected_first_category = Category {
|
let expected_first_category = Category {
|
||||||
|
|
|
@ -166,7 +166,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "terry".into(),
|
name: "terry".into(),
|
||||||
|
@ -174,6 +174,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -182,6 +184,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -18,6 +18,33 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
|
score -> BigInt,
|
||||||
|
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,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -46,6 +73,7 @@ pub struct CommentView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -56,7 +84,7 @@ pub struct CommentView {
|
||||||
|
|
||||||
pub struct CommentQueryBuilder<'a> {
|
pub struct CommentQueryBuilder<'a> {
|
||||||
conn: &'a PgConnection,
|
conn: &'a PgConnection,
|
||||||
query: super::comment_view::comment_view::BoxedQuery<'a, Pg>,
|
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
||||||
sort: &'a SortType,
|
sort: &'a SortType,
|
||||||
for_post_id: Option<i32>,
|
for_post_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
|
@ -69,9 +97,9 @@ pub struct CommentQueryBuilder<'a> {
|
||||||
|
|
||||||
impl<'a> CommentQueryBuilder<'a> {
|
impl<'a> CommentQueryBuilder<'a> {
|
||||||
pub fn create(conn: &'a PgConnection) -> Self {
|
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 {
|
CommentQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
|
@ -128,7 +156,7 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(self) -> Result<Vec<CommentView>, Error> {
|
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;
|
let mut query = self.query;
|
||||||
|
|
||||||
|
@ -226,6 +254,7 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -255,6 +284,7 @@ pub struct ReplyView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -360,7 +390,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "timmy".into(),
|
name: "timmy".into(),
|
||||||
|
@ -368,6 +398,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -376,6 +408,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -447,6 +481,7 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
@ -470,6 +505,7 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
|
|
|
@ -68,6 +68,10 @@ impl Community {
|
||||||
.filter(name.eq(community_name))
|
.filter(name.eq(community_name))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_url(&self) -> String {
|
||||||
|
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||||
|
@ -208,7 +212,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "bobbee".into(),
|
name: "bobbee".into(),
|
||||||
|
@ -216,6 +220,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -224,6 +230,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::community_view::community_view::BoxedQuery;
|
use super::community_view::community_mview::BoxedQuery;
|
||||||
use super::*;
|
use super::*;
|
||||||
use diesel::pg::Pg;
|
use diesel::pg::Pg;
|
||||||
|
|
||||||
|
@ -16,6 +16,32 @@ table! {
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
nsfw -> Bool,
|
nsfw -> Bool,
|
||||||
creator_name -> Varchar,
|
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,
|
category_name -> Varchar,
|
||||||
number_of_subscribers -> BigInt,
|
number_of_subscribers -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
|
@ -33,6 +59,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +71,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +83,7 @@ table! {
|
||||||
user_id -> Int4,
|
user_id -> Int4,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +105,7 @@ pub struct CommunityView {
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub category_name: String,
|
pub category_name: String,
|
||||||
pub number_of_subscribers: i64,
|
pub number_of_subscribers: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
|
@ -98,9 +128,9 @@ pub struct CommunityQueryBuilder<'a> {
|
||||||
|
|
||||||
impl<'a> CommunityQueryBuilder<'a> {
|
impl<'a> CommunityQueryBuilder<'a> {
|
||||||
pub fn create(conn: &'a PgConnection) -> Self {
|
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 {
|
CommunityQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
|
@ -119,7 +149,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_user_id<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
pub fn for_user<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
||||||
self.from_user_id = from_user_id.get_optional();
|
self.from_user_id = from_user_id.get_optional();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -145,7 +175,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(self) -> Result<Vec<CommunityView>, Error> {
|
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;
|
let mut query = self.query;
|
||||||
|
|
||||||
|
@ -224,6 +254,7 @@ pub struct CommunityModeratorView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +284,7 @@ pub struct CommunityFollowerView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +314,7 @@ pub struct CommunityUserBanView {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::Settings;
|
extern crate lazy_static;
|
||||||
|
use crate::settings::Settings;
|
||||||
use diesel::dsl::*;
|
use diesel::dsl::*;
|
||||||
use diesel::result::Error;
|
use diesel::result::Error;
|
||||||
use diesel::*;
|
use diesel::*;
|
||||||
|
@ -14,6 +15,8 @@ pub mod moderator_views;
|
||||||
pub mod password_reset_request;
|
pub mod password_reset_request;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod post_view;
|
pub mod post_view;
|
||||||
|
pub mod private_message;
|
||||||
|
pub mod private_message_view;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
pub mod site_view;
|
pub mod site_view;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
@ -99,19 +102,19 @@ pub trait MaybeOptional<T> {
|
||||||
|
|
||||||
impl<T> MaybeOptional<T> for T {
|
impl<T> MaybeOptional<T> for T {
|
||||||
fn get_optional(self) -> Option<T> {
|
fn get_optional(self) -> Option<T> {
|
||||||
return Some(self);
|
Some(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MaybeOptional<T> for Option<T> {
|
impl<T> MaybeOptional<T> for Option<T> {
|
||||||
fn get_optional(self) -> Option<T> {
|
fn get_optional(self) -> Option<T> {
|
||||||
return self;
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn establish_connection() -> PgConnection {
|
pub fn establish_unpooled_connection() -> PgConnection {
|
||||||
let db_url = Settings::get().db_url;
|
let db_url = Settings::get().get_database_url();
|
||||||
PgConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url))
|
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
|
|
|
@ -434,7 +434,7 @@ mod tests {
|
||||||
// use Crud;
|
// use Crud;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_mod = UserForm {
|
let new_mod = UserForm {
|
||||||
name: "the mod".into(),
|
name: "the mod".into(),
|
||||||
|
@ -442,6 +442,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -450,6 +452,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
||||||
|
@ -460,6 +464,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -468,6 +474,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::schema::password_reset_request;
|
use crate::schema::password_reset_request;
|
||||||
use crate::schema::password_reset_request::dsl::*;
|
use crate::schema::password_reset_request::dsl::*;
|
||||||
use crypto::digest::Digest;
|
use sha2::{Digest, Sha256};
|
||||||
use crypto::sha2::Sha256;
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
||||||
#[table_name = "password_reset_request"]
|
#[table_name = "password_reset_request"]
|
||||||
|
@ -49,8 +48,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
|
||||||
impl PasswordResetRequest {
|
impl PasswordResetRequest {
|
||||||
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.input_str(token);
|
hasher.input(token);
|
||||||
let token_hash = hasher.result_str();
|
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||||
|
|
||||||
let form = PasswordResetRequestForm {
|
let form = PasswordResetRequestForm {
|
||||||
user_id: from_user_id,
|
user_id: from_user_id,
|
||||||
|
@ -61,13 +60,21 @@ impl PasswordResetRequest {
|
||||||
}
|
}
|
||||||
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.input_str(token);
|
hasher.input(token);
|
||||||
let token_hash = hasher.result_str();
|
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||||
password_reset_request
|
password_reset_request
|
||||||
.filter(token_encrypted.eq(token_hash))
|
.filter(token_encrypted.eq(token_hash))
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bytes_to_hex(bytes: Vec<u8>) -> String {
|
||||||
|
let mut str = String::new();
|
||||||
|
for byte in bytes {
|
||||||
|
str = format!("{}{:02x}", str, byte);
|
||||||
|
}
|
||||||
|
str
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -77,7 +84,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "thommy prw".into(),
|
name: "thommy prw".into(),
|
||||||
|
@ -85,6 +92,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -93,27 +102,26 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
||||||
let new_password_reset_request = PasswordResetRequestForm {
|
let token = "nope";
|
||||||
user_id: inserted_user.id,
|
let token_encrypted_ = "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce";
|
||||||
token_encrypted: "no".into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inserted_password_reset_request =
|
let inserted_password_reset_request =
|
||||||
PasswordResetRequest::create(&conn, &new_password_reset_request).unwrap();
|
PasswordResetRequest::create_token(&conn, inserted_user.id, token).unwrap();
|
||||||
|
|
||||||
let expected_password_reset_request = PasswordResetRequest {
|
let expected_password_reset_request = PasswordResetRequest {
|
||||||
id: inserted_password_reset_request.id,
|
id: inserted_password_reset_request.id,
|
||||||
user_id: inserted_user.id,
|
user_id: inserted_user.id,
|
||||||
token_encrypted: "no".into(),
|
token_encrypted: token_encrypted_.to_string(),
|
||||||
published: inserted_password_reset_request.published,
|
published: inserted_password_reset_request.published,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_password_reset_request =
|
let read_password_reset_request = PasswordResetRequest::read_from_token(&conn, token).unwrap();
|
||||||
PasswordResetRequest::read(&conn, inserted_password_reset_request.id).unwrap();
|
|
||||||
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
||||||
|
|
||||||
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
||||||
|
|
|
@ -179,7 +179,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "jim".into(),
|
name: "jim".into(),
|
||||||
|
@ -187,6 +187,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -195,6 +197,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::post_view::post_view::BoxedQuery;
|
use super::post_view::post_mview::BoxedQuery;
|
||||||
use super::*;
|
use super::*;
|
||||||
use diesel::pg::Pg;
|
use diesel::pg::Pg;
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ table! {
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
stickied -> Bool,
|
stickied -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
community_removed -> Bool,
|
community_removed -> Bool,
|
||||||
community_deleted -> Bool,
|
community_deleted -> Bool,
|
||||||
|
@ -59,6 +60,7 @@ pub struct PostView {
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub stickied: bool,
|
pub stickied: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
pub community_removed: bool,
|
pub community_removed: bool,
|
||||||
pub community_deleted: bool,
|
pub community_deleted: bool,
|
||||||
|
@ -75,6 +77,43 @@ pub struct PostView {
|
||||||
pub saved: Option<bool>,
|
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> {
|
pub struct PostQueryBuilder<'a> {
|
||||||
conn: &'a PgConnection,
|
conn: &'a PgConnection,
|
||||||
query: BoxedQuery<'a, Pg>,
|
query: BoxedQuery<'a, Pg>,
|
||||||
|
@ -91,9 +130,9 @@ pub struct PostQueryBuilder<'a> {
|
||||||
|
|
||||||
impl<'a> PostQueryBuilder<'a> {
|
impl<'a> PostQueryBuilder<'a> {
|
||||||
pub fn create(conn: &'a PgConnection) -> Self {
|
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 {
|
PostQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
|
@ -121,7 +160,7 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
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() {
|
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.filter(community_id.eq(for_community_id));
|
||||||
self.query = self.query.then_order_by(stickied.desc());
|
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 {
|
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() {
|
if let Some(search_term) = search_term.get_optional() {
|
||||||
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
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 {
|
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() {
|
if let Some(url_search) = url_search.get_optional() {
|
||||||
self.query = self.query.filter(url.eq(url_search));
|
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> {
|
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;
|
let mut query = self.query;
|
||||||
|
|
||||||
match self.listing_type {
|
if let ListingType::Subscribed = self.listing_type {
|
||||||
ListingType::Subscribed => {
|
|
||||||
query = query.filter(subscribed.eq(true));
|
query = query.filter(subscribed.eq(true));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
query = match self.sort {
|
query = match self.sort {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
|
@ -291,7 +327,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let user_name = "tegan".to_string();
|
let user_name = "tegan".to_string();
|
||||||
let community_name = "test_community_3".to_string();
|
let community_name = "test_community_3".to_string();
|
||||||
|
@ -303,6 +339,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
|
@ -311,6 +349,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -377,6 +417,7 @@ mod tests {
|
||||||
body: None,
|
body: None,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name.to_owned(),
|
||||||
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
@ -405,7 +446,7 @@ mod tests {
|
||||||
user_id: Some(inserted_user.id),
|
user_id: Some(inserted_user.id),
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
id: inserted_post.id,
|
id: inserted_post.id,
|
||||||
name: post_name.to_owned(),
|
name: post_name,
|
||||||
url: None,
|
url: None,
|
||||||
body: None,
|
body: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
@ -413,11 +454,12 @@ mod tests {
|
||||||
locked: false,
|
locked: false,
|
||||||
stickied: false,
|
stickied: false,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name,
|
||||||
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name: community_name.to_owned(),
|
community_name,
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
community_deleted: false,
|
community_deleted: false,
|
||||||
community_nsfw: false,
|
community_nsfw: false,
|
||||||
|
|
144
server/src/db/private_message.rs
Normal file
144
server/src/db/private_message.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::schema::private_message;
|
||||||
|
|
||||||
|
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
#[table_name = "private_message"]
|
||||||
|
pub struct PrivateMessage {
|
||||||
|
pub id: i32,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub content: String,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
#[table_name = "private_message"]
|
||||||
|
pub struct PrivateMessageForm {
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub content: Option<String>,
|
||||||
|
pub deleted: Option<bool>,
|
||||||
|
pub read: Option<bool>,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Crud<PrivateMessageForm> for PrivateMessage {
|
||||||
|
fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
private_message.find(private_message_id).first::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::delete(private_message.find(private_message_id)).execute(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
insert_into(private_message)
|
||||||
|
.values(private_message_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
conn: &PgConnection,
|
||||||
|
private_message_id: i32,
|
||||||
|
private_message_form: &PrivateMessageForm,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
use crate::schema::private_message::dsl::*;
|
||||||
|
diesel::update(private_message.find(private_message_id))
|
||||||
|
.set(private_message_form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::super::user::*;
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_crud() {
|
||||||
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
|
let creator_form = UserForm {
|
||||||
|
name: "creator_pm".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
|
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 inserted_creator = User_::create(&conn, &creator_form).unwrap();
|
||||||
|
|
||||||
|
let recipient_form = UserForm {
|
||||||
|
name: "recipient_pm".into(),
|
||||||
|
fedi_name: "rrf".into(),
|
||||||
|
preferred_username: None,
|
||||||
|
password_encrypted: "nope".into(),
|
||||||
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
|
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 inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||||
|
|
||||||
|
let private_message_form = PrivateMessageForm {
|
||||||
|
content: Some("A test private message".into()),
|
||||||
|
creator_id: inserted_creator.id,
|
||||||
|
recipient_id: inserted_recipient.id,
|
||||||
|
deleted: None,
|
||||||
|
read: None,
|
||||||
|
updated: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
|
||||||
|
|
||||||
|
let expected_private_message = PrivateMessage {
|
||||||
|
id: inserted_private_message.id,
|
||||||
|
content: "A test private message".into(),
|
||||||
|
creator_id: inserted_creator.id,
|
||||||
|
recipient_id: inserted_recipient.id,
|
||||||
|
deleted: false,
|
||||||
|
read: false,
|
||||||
|
updated: None,
|
||||||
|
published: inserted_private_message.published,
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
||||||
|
let updated_private_message =
|
||||||
|
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
||||||
|
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_creator.id).unwrap();
|
||||||
|
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(expected_private_message, read_private_message);
|
||||||
|
assert_eq!(expected_private_message, updated_private_message);
|
||||||
|
assert_eq!(expected_private_message, inserted_private_message);
|
||||||
|
assert_eq!(1, num_deleted);
|
||||||
|
}
|
||||||
|
}
|
140
server/src/db/private_message_view.rs
Normal file
140
server/src/db/private_message_view.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use super::*;
|
||||||
|
use diesel::pg::Pg;
|
||||||
|
|
||||||
|
// The faked schema since diesel doesn't do views
|
||||||
|
table! {
|
||||||
|
private_message_view (id) {
|
||||||
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
content -> Text,
|
||||||
|
deleted -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
|
recipient_name -> Varchar,
|
||||||
|
recipient_avatar -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
private_message_mview (id) {
|
||||||
|
id -> Int4,
|
||||||
|
creator_id -> Int4,
|
||||||
|
recipient_id -> Int4,
|
||||||
|
content -> Text,
|
||||||
|
deleted -> Bool,
|
||||||
|
read -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
|
recipient_name -> Varchar,
|
||||||
|
recipient_avatar -> Nullable<Text>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
||||||
|
)]
|
||||||
|
#[table_name = "private_message_view"]
|
||||||
|
pub struct PrivateMessageView {
|
||||||
|
pub id: i32,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub recipient_id: i32,
|
||||||
|
pub content: String,
|
||||||
|
pub deleted: bool,
|
||||||
|
pub read: bool,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
|
pub recipient_name: String,
|
||||||
|
pub recipient_avatar: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PrivateMessageQueryBuilder<'a> {
|
||||||
|
conn: &'a PgConnection,
|
||||||
|
query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>,
|
||||||
|
for_recipient_id: i32,
|
||||||
|
unread_only: bool,
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PrivateMessageQueryBuilder<'a> {
|
||||||
|
pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
|
||||||
|
use super::private_message_view::private_message_mview::dsl::*;
|
||||||
|
|
||||||
|
let query = private_message_mview.into_boxed();
|
||||||
|
|
||||||
|
PrivateMessageQueryBuilder {
|
||||||
|
conn,
|
||||||
|
query,
|
||||||
|
for_recipient_id,
|
||||||
|
unread_only: false,
|
||||||
|
page: None,
|
||||||
|
limit: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unread_only(mut self, unread_only: bool) -> Self {
|
||||||
|
self.unread_only = unread_only;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
|
||||||
|
self.page = page.get_optional();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
|
||||||
|
self.limit = limit.get_optional();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
|
||||||
|
use super::private_message_view::private_message_mview::dsl::*;
|
||||||
|
|
||||||
|
let mut query = self.query;
|
||||||
|
|
||||||
|
// If its unread, I only want the ones to me
|
||||||
|
if self.unread_only {
|
||||||
|
query = query
|
||||||
|
.filter(read.eq(false))
|
||||||
|
.filter(recipient_id.eq(self.for_recipient_id));
|
||||||
|
}
|
||||||
|
// Otherwise, I want the ALL view to show both sent and received
|
||||||
|
else {
|
||||||
|
query = query.filter(
|
||||||
|
recipient_id
|
||||||
|
.eq(self.for_recipient_id)
|
||||||
|
.or(creator_id.eq(self.for_recipient_id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
|
||||||
|
query
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.order_by(published.desc())
|
||||||
|
.load::<PrivateMessageView>(self.conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrivateMessageView {
|
||||||
|
pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
|
||||||
|
use super::private_message_view::private_message_view::dsl::*;
|
||||||
|
|
||||||
|
let mut query = private_message_view.into_boxed();
|
||||||
|
|
||||||
|
query = query
|
||||||
|
.filter(id.eq(from_private_message_id))
|
||||||
|
.order_by(published.desc());
|
||||||
|
|
||||||
|
query.first::<Self>(conn)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ table! {
|
||||||
open_registration -> Bool,
|
open_registration -> Bool,
|
||||||
enable_nsfw -> Bool,
|
enable_nsfw -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
number_of_users -> BigInt,
|
number_of_users -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
number_of_comments -> BigInt,
|
number_of_comments -> BigInt,
|
||||||
|
@ -34,6 +35,7 @@ pub struct SiteView {
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: bool,
|
pub enable_nsfw: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub number_of_users: i64,
|
pub number_of_users: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub number_of_comments: i64,
|
pub number_of_comments: i64,
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub struct User_ {
|
||||||
pub preferred_username: Option<String>,
|
pub preferred_username: Option<String>,
|
||||||
pub password_encrypted: String,
|
pub password_encrypted: String,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub icon: Option<Vec<u8>>,
|
pub avatar: Option<String>,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
|
@ -24,6 +24,9 @@ pub struct User_ {
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
|
pub matrix_user_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
|
@ -36,12 +39,16 @@ pub struct UserForm {
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
|
pub matrix_user_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<UserForm> for User_ {
|
impl Crud<UserForm> for User_ {
|
||||||
|
@ -74,14 +81,13 @@ impl User_ {
|
||||||
pub fn update_password(
|
pub fn update_password(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
form: &UserForm,
|
new_password: &str,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let mut edited_user = form.clone();
|
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
|
||||||
let password_hash =
|
|
||||||
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
|
|
||||||
edited_user.password_encrypted = password_hash;
|
|
||||||
|
|
||||||
Self::update(&conn, user_id, &edited_user)
|
diesel::update(user_.find(user_id))
|
||||||
|
.set(password_encrypted.eq(password_hash))
|
||||||
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
|
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
|
||||||
|
@ -99,6 +105,8 @@ pub struct Claims {
|
||||||
pub default_sort_type: i16,
|
pub default_sort_type: i16,
|
||||||
pub default_listing_type: i16,
|
pub default_listing_type: i16,
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub show_avatars: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -123,6 +131,8 @@ impl User_ {
|
||||||
default_sort_type: self.default_sort_type,
|
default_sort_type: self.default_sort_type,
|
||||||
default_listing_type: self.default_listing_type,
|
default_listing_type: self.default_listing_type,
|
||||||
lang: self.lang.to_owned(),
|
lang: self.lang.to_owned(),
|
||||||
|
avatar: self.avatar.to_owned(),
|
||||||
|
show_avatars: self.show_avatars.to_owned(),
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
@ -132,23 +142,27 @@ impl User_ {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
|
||||||
|
user_.filter(name.eq(username)).first::<User_>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
||||||
|
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_by_email_or_username(
|
pub fn find_by_email_or_username(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
username_or_email: &str,
|
username_or_email: &str,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
if is_email_regex(username_or_email) {
|
if is_email_regex(username_or_email) {
|
||||||
user_
|
User_::find_by_email(conn, username_or_email)
|
||||||
.filter(email.eq(username_or_email))
|
|
||||||
.first::<User_>(conn)
|
|
||||||
} else {
|
} else {
|
||||||
user_
|
User_::find_by_username(conn, username_or_email)
|
||||||
.filter(name.eq(username_or_email))
|
|
||||||
.first::<User_>(conn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
pub fn get_profile_url(&self) -> String {
|
||||||
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
format!("https://{}/u/{}", Settings::get().hostname, self.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
||||||
|
@ -164,7 +178,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "thommy".into(),
|
name: "thommy".into(),
|
||||||
|
@ -172,6 +186,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -180,6 +196,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -191,7 +209,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
icon: None,
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
published: inserted_user.published,
|
published: inserted_user.published,
|
||||||
|
@ -201,6 +220,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||||
|
|
|
@ -60,7 +60,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
||||||
let new_user = UserForm {
|
let new_user = UserForm {
|
||||||
name: "terrylake".into(),
|
name: "terrylake".into(),
|
||||||
|
@ -68,6 +68,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -76,6 +78,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||||
|
@ -86,6 +90,8 @@ mod tests {
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
password_encrypted: "nope".into(),
|
password_encrypted: "nope".into(),
|
||||||
email: None,
|
email: None,
|
||||||
|
matrix_user_id: None,
|
||||||
|
avatar: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -94,6 +100,8 @@ mod tests {
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Hot as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
|
show_avatars: true,
|
||||||
|
send_notifications_to_email: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||||
|
|
|
@ -20,6 +20,7 @@ table! {
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
@ -50,6 +51,7 @@ pub struct UserMentionView {
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
@ -78,7 +80,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
UserMentionQueryBuilder {
|
UserMentionQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
for_user_id: for_user_id,
|
for_user_id,
|
||||||
sort: &SortType::New,
|
sort: &SortType::New,
|
||||||
unread_only: false,
|
unread_only: false,
|
||||||
page: None,
|
page: None,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::user_view::user_view::BoxedQuery;
|
use super::user_view::user_mview::BoxedQuery;
|
||||||
use super::*;
|
use super::*;
|
||||||
use diesel::pg::Pg;
|
use diesel::pg::Pg;
|
||||||
|
|
||||||
|
@ -6,9 +6,34 @@ table! {
|
||||||
user_view (id) {
|
user_view (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
|
avatar -> Nullable<Text>,
|
||||||
|
email -> Nullable<Text>,
|
||||||
|
matrix_user_id -> Nullable<Text>,
|
||||||
fedi_name -> Varchar,
|
fedi_name -> Varchar,
|
||||||
admin -> Bool,
|
admin -> Bool,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
|
show_avatars -> Bool,
|
||||||
|
send_notifications_to_email -> Bool,
|
||||||
|
published -> Timestamp,
|
||||||
|
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>,
|
||||||
|
matrix_user_id -> Nullable<Text>,
|
||||||
|
fedi_name -> Varchar,
|
||||||
|
admin -> Bool,
|
||||||
|
banned -> Bool,
|
||||||
|
show_avatars -> Bool,
|
||||||
|
send_notifications_to_email -> Bool,
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
post_score -> BigInt,
|
post_score -> BigInt,
|
||||||
|
@ -24,9 +49,14 @@ table! {
|
||||||
pub struct UserView {
|
pub struct UserView {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub matrix_user_id: Option<String>,
|
||||||
pub fedi_name: String,
|
pub fedi_name: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
|
pub show_avatars: bool,
|
||||||
|
pub send_notifications_to_email: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
pub post_score: i64,
|
pub post_score: i64,
|
||||||
|
@ -44,9 +74,9 @@ pub struct UserQueryBuilder<'a> {
|
||||||
|
|
||||||
impl<'a> UserQueryBuilder<'a> {
|
impl<'a> UserQueryBuilder<'a> {
|
||||||
pub fn create(conn: &'a PgConnection) -> Self {
|
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 {
|
UserQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
|
@ -63,7 +93,7 @@ impl<'a> UserQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
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() {
|
if let Some(search_term) = search_term.get_optional() {
|
||||||
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
|
||||||
}
|
}
|
||||||
|
@ -81,7 +111,7 @@ impl<'a> UserQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(self) -> Result<Vec<UserView>, Error> {
|
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;
|
let mut query = self.query;
|
||||||
|
|
||||||
|
@ -120,12 +150,12 @@ impl UserView {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||||
use super::user_view::user_view::dsl::*;
|
use super::user_view::user_mview::dsl::*;
|
||||||
user_view.filter(admin.eq(true)).load::<Self>(conn)
|
user_mview.filter(admin.eq(true)).load::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||||
use super::user_view::user_view::dsl::*;
|
use super::user_view::user_mview::dsl::*;
|
||||||
user_view.filter(banned.eq(true)).load::<Self>(conn)
|
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 actix_web;
|
||||||
pub extern crate bcrypt;
|
pub extern crate bcrypt;
|
||||||
pub extern crate chrono;
|
pub extern crate chrono;
|
||||||
pub extern crate crypto;
|
|
||||||
pub extern crate dotenv;
|
pub extern crate dotenv;
|
||||||
pub extern crate jsonwebtoken;
|
pub extern crate jsonwebtoken;
|
||||||
pub extern crate lettre;
|
pub extern crate lettre;
|
||||||
|
@ -20,19 +19,20 @@ pub extern crate rand;
|
||||||
pub extern crate regex;
|
pub extern crate regex;
|
||||||
pub extern crate serde;
|
pub extern crate serde;
|
||||||
pub extern crate serde_json;
|
pub extern crate serde_json;
|
||||||
|
pub extern crate sha2;
|
||||||
pub extern crate strum;
|
pub extern crate strum;
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod apub;
|
pub mod apub;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod feeds;
|
pub mod routes;
|
||||||
pub mod nodeinfo;
|
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod settings;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use dotenv::dotenv;
|
|
||||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||||
use lettre::smtp::extension::ClientId;
|
use lettre::smtp::extension::ClientId;
|
||||||
use lettre::smtp::ConnectionReuseParameters;
|
use lettre::smtp::ConnectionReuseParameters;
|
||||||
|
@ -40,91 +40,7 @@ use lettre::{SmtpClient, Transport};
|
||||||
use lettre_email::Email;
|
use lettre_email::Email;
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use regex::Regex;
|
use regex::{Regex, RegexBuilder};
|
||||||
use std::env;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
pub struct Settings {
|
|
||||||
pub db_url: String,
|
|
||||||
pub hostname: String,
|
|
||||||
pub bind: IpAddr,
|
|
||||||
pub port: u16,
|
|
||||||
pub jwt_secret: String,
|
|
||||||
pub rate_limit_message: i32,
|
|
||||||
pub rate_limit_message_per_second: i32,
|
|
||||||
pub rate_limit_post: i32,
|
|
||||||
pub rate_limit_post_per_second: i32,
|
|
||||||
pub rate_limit_register: i32,
|
|
||||||
pub rate_limit_register_per_second: i32,
|
|
||||||
pub email_config: Option<EmailConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EmailConfig {
|
|
||||||
smtp_server: String,
|
|
||||||
smtp_login: String,
|
|
||||||
smtp_password: String,
|
|
||||||
smtp_from_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Settings {
|
|
||||||
pub fn get() -> Self {
|
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
let email_config =
|
|
||||||
if env::var("SMTP_SERVER").is_ok() && !env::var("SMTP_SERVER").unwrap().eq("") {
|
|
||||||
Some(EmailConfig {
|
|
||||||
smtp_server: env::var("SMTP_SERVER").expect("SMTP_SERVER must be set"),
|
|
||||||
smtp_login: env::var("SMTP_LOGIN").expect("SMTP_LOGIN must be set"),
|
|
||||||
smtp_password: env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD must be set"),
|
|
||||||
smtp_from_address: env::var("SMTP_FROM_ADDRESS").expect("SMTP_FROM_ADDRESS must be set"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Settings {
|
|
||||||
db_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
|
|
||||||
hostname: env::var("HOSTNAME").unwrap_or("rrr".to_string()),
|
|
||||||
bind: env::var("BIND")
|
|
||||||
.unwrap_or("0.0.0.0".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
port: env::var("PORT")
|
|
||||||
.unwrap_or("8536".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
jwt_secret: env::var("JWT_SECRET").unwrap_or("changeme".to_string()),
|
|
||||||
rate_limit_message: env::var("RATE_LIMIT_MESSAGE")
|
|
||||||
.unwrap_or("30".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_message_per_second: env::var("RATE_LIMIT_MESSAGE_PER_SECOND")
|
|
||||||
.unwrap_or("60".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_post: env::var("RATE_LIMIT_POST")
|
|
||||||
.unwrap_or("3".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_post_per_second: env::var("RATE_LIMIT_POST_PER_SECOND")
|
|
||||||
.unwrap_or("600".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_register: env::var("RATE_LIMIT_REGISTER")
|
|
||||||
.unwrap_or("1".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
rate_limit_register_per_second: env::var("RATE_LIMIT_REGISTER_PER_SECOND")
|
|
||||||
.unwrap_or("3600".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
email_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn api_endpoint(&self) -> String {
|
|
||||||
format!("{}/api/v1", self.hostname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||||
|
@ -174,13 +90,13 @@ pub fn send_email(
|
||||||
to_username: &str,
|
to_username: &str,
|
||||||
html: &str,
|
html: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let email_config = Settings::get().email_config.ok_or("no_email_setup")?;
|
let email_config = Settings::get().email.as_ref().ok_or("no_email_setup")?;
|
||||||
|
|
||||||
let email = Email::builder()
|
let email = Email::builder()
|
||||||
.to((to_email, to_username))
|
.to((to_email, to_username))
|
||||||
.from((
|
.from((
|
||||||
email_config.smtp_login.to_owned(),
|
email_config.smtp_login.to_owned(),
|
||||||
email_config.smtp_from_address,
|
email_config.smtp_from_address.to_owned(),
|
||||||
))
|
))
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.html(html)
|
.html(html)
|
||||||
|
@ -189,7 +105,7 @@ pub fn send_email(
|
||||||
|
|
||||||
let mut mailer = SmtpClient::new_simple(&email_config.smtp_server)
|
let mut mailer = SmtpClient::new_simple(&email_config.smtp_server)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.hello_name(ClientId::Domain("localhost".to_string()))
|
.hello_name(ClientId::Domain(Settings::get().hostname.to_owned()))
|
||||||
.credentials(Credentials::new(
|
.credentials(Credentials::new(
|
||||||
email_config.smtp_login.to_owned(),
|
email_config.smtp_login.to_owned(),
|
||||||
email_config.smtp_password.to_owned(),
|
email_config.smtp_password.to_owned(),
|
||||||
|
@ -201,6 +117,8 @@ pub fn send_email(
|
||||||
|
|
||||||
let result = mailer.send(email.into());
|
let result = mailer.send(email.into());
|
||||||
|
|
||||||
|
mailer.close();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(_) => Err("no_email_setup".to_string()),
|
Err(_) => Err("no_email_setup".to_string()),
|
||||||
|
@ -209,11 +127,7 @@ pub fn send_email(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs};
|
||||||
#[test]
|
|
||||||
fn test_api() {
|
|
||||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_email() {
|
fn test_email() {
|
||||||
|
@ -224,11 +138,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_slur_filter() {
|
fn test_slur_filter() {
|
||||||
let test =
|
let test =
|
||||||
"coons test dindu ladyboy tranny retardeds. This is a bunch of other safe text.".to_string();
|
"coons test dindu ladyboy tranny retardeds. Capitalized Niggerz. This is a bunch of other safe text.".to_string();
|
||||||
let slur_free = "No slurs here";
|
let slur_free = "No slurs here";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
remove_slurs(&test),
|
remove_slurs(&test),
|
||||||
"*removed* test *removed* *removed* *removed* *removed*. This is a bunch of other safe text."
|
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
|
||||||
.to_string()
|
.to_string()
|
||||||
);
|
);
|
||||||
assert!(has_slurs(&test));
|
assert!(has_slurs(&test));
|
||||||
|
@ -251,6 +165,6 @@ mod tests {
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
|
||||||
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,259 +3,69 @@ extern crate lemmy_server;
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_files::NamedFile;
|
use actix_web::middleware::Logger;
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use actix_web_actors::ws;
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
use lemmy_server::db::establish_connection;
|
use diesel::PgConnection;
|
||||||
use lemmy_server::feeds;
|
use env_logger;
|
||||||
use lemmy_server::nodeinfo;
|
use lemmy_server::routes::{api, federation, feeds, images, index, nodeinfo, webfinger, websocket};
|
||||||
|
use lemmy_server::settings::Settings;
|
||||||
use lemmy_server::websocket::server::*;
|
use lemmy_server::websocket::server::*;
|
||||||
use lemmy_server::Settings;
|
use std::io;
|
||||||
use std::env;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
embed_migrations!();
|
embed_migrations!();
|
||||||
|
|
||||||
/// How often heartbeat pings are sent
|
#[actix_rt::main]
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
async fn main() -> io::Result<()> {
|
||||||
/// How long before lack of client response causes a timeout
|
env_logger::init();
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
let settings = Settings::get();
|
||||||
|
|
||||||
/// Entry point for our route
|
// Set up the r2d2 connection pool
|
||||||
fn chat_route(
|
let manager = ConnectionManager::<PgConnection>::new(&settings.get_database_url());
|
||||||
req: HttpRequest,
|
let pool = Pool::builder()
|
||||||
stream: web::Payload,
|
.max_size(settings.database.pool_size)
|
||||||
chat_server: web::Data<Addr<ChatServer>>,
|
.build(manager)
|
||||||
) -> Result<HttpResponse, Error> {
|
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url()));
|
||||||
ws::start(
|
|
||||||
WSSession {
|
|
||||||
cs_addr: chat_server.get_ref().to_owned(),
|
|
||||||
id: 0,
|
|
||||||
hb: Instant::now(),
|
|
||||||
ip: req
|
|
||||||
.connection_info()
|
|
||||||
.remote()
|
|
||||||
.unwrap_or("127.0.0.1:12345")
|
|
||||||
.split(":")
|
|
||||||
.next()
|
|
||||||
.unwrap_or("127.0.0.1")
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
&req,
|
|
||||||
stream,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WSSession {
|
|
||||||
cs_addr: Addr<ChatServer>,
|
|
||||||
/// unique session id
|
|
||||||
id: usize,
|
|
||||||
ip: String,
|
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
|
||||||
/// otherwise we drop connection.
|
|
||||||
hb: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for WSSession {
|
|
||||||
type Context = ws::WebsocketContext<Self>;
|
|
||||||
|
|
||||||
/// Method is called on actor start.
|
|
||||||
/// We register ws session with ChatServer
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
// we'll start heartbeat process on session start.
|
|
||||||
self.hb(ctx);
|
|
||||||
|
|
||||||
// register self in chat server. `AsyncContext::wait` register
|
|
||||||
// future within context, but context waits until this future resolves
|
|
||||||
// before processing any other events.
|
|
||||||
// across all routes within application
|
|
||||||
let addr = ctx.address();
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(Connect {
|
|
||||||
addr: addr.recipient(),
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, act, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => act.id = res,
|
|
||||||
// something is wrong with chat server
|
|
||||||
_ => ctx.stop(),
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
|
||||||
// notify chat server
|
|
||||||
self.cs_addr.do_send(Disconnect {
|
|
||||||
id: self.id,
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
});
|
|
||||||
Running::Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle messages from chat server, we simply send it to peer websocket
|
|
||||||
/// These are room messages, IE sent to others in the room
|
|
||||||
impl Handler<WSMessage> for WSSession {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
|
||||||
// println!("id: {} msg: {}", self.id, msg.0);
|
|
||||||
ctx.text(msg.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WebSocket message handler
|
|
||||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
|
||||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
|
||||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
|
||||||
match msg {
|
|
||||||
ws::Message::Ping(msg) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
ctx.pong(&msg);
|
|
||||||
}
|
|
||||||
ws::Message::Pong(_) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
}
|
|
||||||
ws::Message::Text(text) => {
|
|
||||||
let m = text.trim().to_owned();
|
|
||||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
|
||||||
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(StandardMessage {
|
|
||||||
id: self.id,
|
|
||||||
msg: m,
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, _, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => ctx.text(res),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", &e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
|
||||||
ws::Message::Close(_) => {
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WSSession {
|
|
||||||
/// helper method that sends ping to client every second.
|
|
||||||
///
|
|
||||||
/// also this method checks heartbeats from client
|
|
||||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
|
||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
|
||||||
// check client heartbeats
|
|
||||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
|
||||||
// heartbeat timed out
|
|
||||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
|
||||||
|
|
||||||
// notify chat server
|
|
||||||
act.cs_addr.do_send(Disconnect {
|
|
||||||
id: act.id,
|
|
||||||
ip: act.ip.to_owned(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// stop actor
|
|
||||||
ctx.stop();
|
|
||||||
|
|
||||||
// don't try to send a ping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ping("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let _ = env_logger::init();
|
|
||||||
let sys = actix::System::new("lemmy");
|
|
||||||
|
|
||||||
// Run the migrations from code
|
// Run the migrations from code
|
||||||
let conn = establish_connection();
|
let conn = pool.get().unwrap();
|
||||||
embedded_migrations::run(&conn).unwrap();
|
embedded_migrations::run(&conn).unwrap();
|
||||||
|
|
||||||
// Start chat server actor in separate thread
|
// Set up websocket server
|
||||||
let server = ChatServer::default().start();
|
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
|
// Create Http server with websocket support
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
|
.data(pool.clone())
|
||||||
.data(server.clone())
|
.data(server.clone())
|
||||||
// Front end routes
|
// The routes
|
||||||
.service(actix_files::Files::new("/static", front_end_dir()))
|
.configure(api::config)
|
||||||
.route("/", web::get().to(index))
|
.wrap(Logger::default())
|
||||||
.route(
|
.configure(federation::config)
|
||||||
"/home/type/{type}/sort/{sort}/page/{page}",
|
.configure(feeds::config)
|
||||||
web::get().to(index),
|
.configure(images::config)
|
||||||
)
|
.configure(index::config)
|
||||||
.route("/login", web::get().to(index))
|
.configure(nodeinfo::config)
|
||||||
.route("/create_post", web::get().to(index))
|
.configure(webfinger::config)
|
||||||
.route("/create_community", web::get().to(index))
|
.configure(websocket::config)
|
||||||
.route("/communities/page/{page}", web::get().to(index))
|
// static files
|
||||||
.route("/communities", web::get().to(index))
|
.service(actix_files::Files::new(
|
||||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
"/static",
|
||||||
.route("/post/{id}", web::get().to(index))
|
settings.front_end_dir.to_owned(),
|
||||||
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
))
|
||||||
.route("/c/{name}", web::get().to(index))
|
.service(actix_files::Files::new(
|
||||||
.route("/community/{id}", web::get().to(index))
|
"/docs",
|
||||||
.route(
|
settings.front_end_dir.to_owned() + "/documentation",
|
||||||
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
))
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/u/{username}", web::get().to(index))
|
|
||||||
.route("/user/{id}", web::get().to(index))
|
|
||||||
.route("/inbox", web::get().to(index))
|
|
||||||
.route("/modlog/community/{community_id}", web::get().to(index))
|
|
||||||
.route("/modlog", web::get().to(index))
|
|
||||||
.route("/setup", web::get().to(index))
|
|
||||||
.route(
|
|
||||||
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/search", web::get().to(index))
|
|
||||||
.route("/sponsors", web::get().to(index))
|
|
||||||
.route("/password_change/{token}", web::get().to(index))
|
|
||||||
// Websocket
|
|
||||||
.service(web::resource("/api/v1/ws").to(chat_route))
|
|
||||||
// NodeInfo
|
|
||||||
.route("/nodeinfo/2.0.json", web::get().to(nodeinfo::node_info))
|
|
||||||
.route(
|
|
||||||
"/.well-known/nodeinfo",
|
|
||||||
web::get().to(nodeinfo::node_info_well_known),
|
|
||||||
)
|
|
||||||
// RSS
|
|
||||||
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
|
||||||
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
|
||||||
})
|
})
|
||||||
.bind((settings.bind, settings.port))
|
.bind((settings.bind, settings.port))?
|
||||||
.unwrap()
|
.run()
|
||||||
.start();
|
.await
|
||||||
|
|
||||||
println!("Started http server at {}:{}", settings.bind, settings.port);
|
|
||||||
let _ = sys.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
|
||||||
Ok(NamedFile::open(front_end_dir() + "/index.html")?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn front_end_dir() -> String {
|
|
||||||
env::var("LEMMY_FRONT_END_DIR").unwrap_or("../ui/dist".to_string())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::community::Community;
|
||||||
use crate::db::post_view::{PostQueryBuilder, PostView};
|
use crate::db::post_view::{PostQueryBuilder, PostView};
|
||||||
use crate::db::site_view::SiteView;
|
use crate::db::site_view::SiteView;
|
||||||
use crate::db::user::User_;
|
use crate::db::user::{Claims, User_};
|
||||||
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
||||||
use crate::db::{establish_connection, ListingType, SortType};
|
use crate::db::{ListingType, SortType};
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
|
||||||
use actix_web::{web, HttpResponse, Result};
|
use actix_web::{web, HttpResponse, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
|
use diesel::PgConnection;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -29,61 +31,79 @@ enum RequestType {
|
||||||
Inbox,
|
Inbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
let sort_type = match get_sort_type(info) {
|
cfg
|
||||||
Ok(sort_type) => sort_type,
|
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
||||||
};
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
|
||||||
|
}
|
||||||
|
|
||||||
let feed_result = get_feed_all_data(&sort_type);
|
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()?;
|
||||||
|
|
||||||
match feed_result {
|
let sort_type = get_sort_type(info)?;
|
||||||
Ok(rss) => HttpResponse::Ok()
|
get_feed_all_data(&conn, &sort_type)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|rss| {
|
||||||
|
HttpResponse::Ok()
|
||||||
.content_type("application/rss+xml")
|
.content_type("application/rss+xml")
|
||||||
.body(rss),
|
.body(rss)
|
||||||
Err(_) => HttpResponse::NotFound().finish(),
|
})
|
||||||
}
|
.map_err(|_| HttpResponse::InternalServerError())?;
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
|
async fn get_feed(
|
||||||
let sort_type = match get_sort_type(info) {
|
path: web::Path<(String, String)>,
|
||||||
Ok(sort_type) => sort_type,
|
info: web::Query<Params>,
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
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() {
|
let request_type = match path.0.as_ref() {
|
||||||
"u" => RequestType::User,
|
"u" => RequestType::User,
|
||||||
"c" => RequestType::Community,
|
"c" => RequestType::Community,
|
||||||
"front" => RequestType::Front,
|
"front" => RequestType::Front,
|
||||||
"inbox" => RequestType::Inbox,
|
"inbox" => RequestType::Inbox,
|
||||||
_ => return HttpResponse::NotFound().finish(),
|
_ => return Err(format_err!("wrong_type")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let param = path.1.to_owned();
|
let param = path.1.to_owned();
|
||||||
|
|
||||||
let feed_result = match request_type {
|
match request_type {
|
||||||
RequestType::User => get_feed_user(&sort_type, param),
|
RequestType::User => get_feed_user(&conn, &sort_type, param),
|
||||||
RequestType::Community => get_feed_community(&sort_type, param),
|
RequestType::Community => get_feed_community(&conn, &sort_type, param),
|
||||||
RequestType::Front => get_feed_front(&sort_type, param),
|
RequestType::Front => get_feed_front(&conn, &sort_type, param),
|
||||||
RequestType::Inbox => get_feed_inbox(param),
|
RequestType::Inbox => get_feed_inbox(&conn, param),
|
||||||
};
|
|
||||||
|
|
||||||
match feed_result {
|
|
||||||
Ok(rss) => HttpResponse::Ok()
|
|
||||||
.content_type("application/rss+xml")
|
|
||||||
.body(rss),
|
|
||||||
Err(_) => HttpResponse::NotFound().finish(),
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.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> {
|
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
|
||||||
let sort_query = info.sort.to_owned().unwrap_or(SortType::Hot.to_string());
|
let sort_query = info
|
||||||
|
.sort
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| SortType::Hot.to_string());
|
||||||
SortType::from_str(&sort_query)
|
SortType::from_str(&sort_query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feed_all_data(sort_type: &SortType) -> Result<String, Error> {
|
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> {
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&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())
|
Ok(channel_builder.build().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Error> {
|
fn get_feed_user(
|
||||||
let conn = establish_connection();
|
conn: &PgConnection,
|
||||||
|
sort_type: &SortType,
|
||||||
|
user_name: String,
|
||||||
|
) -> Result<String, Error> {
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user = User_::find_by_email_or_username(&conn, &user_name)?;
|
let user = User_::find_by_username(&conn, &user_name)?;
|
||||||
let user_url = format!("https://{}/u/{}", Settings::get().hostname, user.name);
|
let user_url = user.get_profile_url();
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::All)
|
.listing_type(ListingType::All)
|
||||||
|
@ -130,12 +152,14 @@ fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Erro
|
||||||
Ok(channel_builder.build().unwrap().to_string())
|
Ok(channel_builder.build().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<String, Error> {
|
fn get_feed_community(
|
||||||
let conn = establish_connection();
|
conn: &PgConnection,
|
||||||
|
sort_type: &SortType,
|
||||||
|
community_name: String,
|
||||||
|
) -> Result<String, Error> {
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let community = Community::read_from_name(&conn, community_name)?;
|
let community = Community::read_from_name(&conn, community_name)?;
|
||||||
let community_url = format!("https://{}/c/{}", Settings::get().hostname, community.name);
|
let community_url = community.get_url();
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::All)
|
.listing_type(ListingType::All)
|
||||||
|
@ -158,11 +182,9 @@ fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<St
|
||||||
Ok(channel_builder.build().unwrap().to_string())
|
Ok(channel_builder.build().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
fn get_feed_front(conn: &PgConnection, sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::Subscribed)
|
.listing_type(ListingType::Subscribed)
|
||||||
|
@ -185,11 +207,9 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||||
Ok(channel_builder.build().unwrap().to_string())
|
Ok(channel_builder.build().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_feed_inbox(jwt: String) -> Result<String, Error> {
|
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let sort = SortType::New;
|
let sort = SortType::New;
|
||||||
|
|
||||||
|
@ -331,7 +351,7 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
|
||||||
"/c/{} <a href=\"{}\">(link)</a>",
|
"/c/{} <a href=\"{}\">(link)</a>",
|
||||||
p.community_name, community_url
|
p.community_name, community_url
|
||||||
))
|
))
|
||||||
.domain(Settings::get().hostname)
|
.domain(Settings::get().hostname.to_owned())
|
||||||
.build();
|
.build();
|
||||||
i.categories(vec![category.unwrap()]);
|
i.categories(vec![category.unwrap()]);
|
||||||
|
|
76
server/src/routes/images.rs
Normal file
76
server/src/routes/images.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use actix_web::{web, Error, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
|
use crate::routes::get_jwt_token_from_cookies;
|
||||||
|
use actix_web::client::Client;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.data(Client::new())
|
||||||
|
.route("/pictshare/api/upload.php", web::post().to(upload))
|
||||||
|
.route("/pictshare/{image}", web::get().to(get_image))
|
||||||
|
.route(
|
||||||
|
"/pictshare/{resolution}/{image}",
|
||||||
|
web::get().to(get_thumbnail),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For uploads, check the jwt token to make sure the user is logged in.
|
||||||
|
async fn upload(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Bytes,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
match get_jwt_token_from_cookies(req.headers()) {
|
||||||
|
Ok(_) => forward(req, body, client, "/api/upload.php").await,
|
||||||
|
Err(_) => Ok(HttpResponse::Forbidden().finish()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_image(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Bytes,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
forward(req, body, client, &path.into_inner()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_thumbnail(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Bytes,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
path: web::Path<(String, String)>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
forward(req, body, client, &format!("/{}/{}", path.0, path.1)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Based on https://github.com/actix/examples/blob/master/http-proxy/src/main.rs
|
||||||
|
async fn forward(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Bytes,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let mut new_url = match Url::parse(&Settings::get().pictshare_url) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(_) => return Ok(HttpResponse::InternalServerError().finish()),
|
||||||
|
};
|
||||||
|
new_url.set_path(path);
|
||||||
|
|
||||||
|
let forwarded_req = client
|
||||||
|
.request_from(new_url.as_str(), req.head())
|
||||||
|
.no_decompress();
|
||||||
|
|
||||||
|
let mut res = forwarded_req.send_body(body).await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let mut client_resp = HttpResponse::build(res.status());
|
||||||
|
// Remove `Connection` as per
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives
|
||||||
|
for (header_name, header_value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
||||||
|
client_resp.header(header_name.clone(), header_value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(client_resp.body(res.body().await?))
|
||||||
|
}
|
46
server/src/routes/index.rs
Normal file
46
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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("/create_private_message", 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",
|
||||||
|
)?)
|
||||||
|
}
|
21
server/src/routes/mod.rs
Normal file
21
server/src/routes/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
pub mod api;
|
||||||
|
use crate::db::user::Claims;
|
||||||
|
use actix_web::http::HeaderMap;
|
||||||
|
use jsonwebtoken::TokenData;
|
||||||
|
|
||||||
|
pub mod federation;
|
||||||
|
pub mod feeds;
|
||||||
|
pub mod images;
|
||||||
|
pub mod index;
|
||||||
|
pub mod nodeinfo;
|
||||||
|
pub mod webfinger;
|
||||||
|
pub mod websocket;
|
||||||
|
|
||||||
|
fn get_jwt_token_from_cookies(headers: &HeaderMap) -> Result<TokenData<Claims>, failure::Error> {
|
||||||
|
// TODO: this parsing will break if we add any new cookies
|
||||||
|
let jwt = headers.get("Cookie").unwrap().to_str()?;
|
||||||
|
match Claims::decode(&jwt.replace("jwt=", "")) {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => Err(format_err!("{}", e)),
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue