Merge branch 'master' into federation
This commit is contained in:
commit
e09a035373
57 changed files with 1245 additions and 929 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
|
||||||
|
|
3
.travis.yml
vendored
3
.travis.yml
vendored
|
@ -13,9 +13,12 @@ before_cache:
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
- psql -c 'create database lemmy with owner lemmy;' -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:
|
||||||
|
# Default checks, but fail if anything is detected
|
||||||
|
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
||||||
- cargo build
|
- cargo build
|
||||||
- diesel migration run
|
- diesel migration run
|
||||||
- cargo test
|
- cargo test
|
||||||
|
|
149
README.md
vendored
149
README.md
vendored
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
||||||
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
||||||
![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)
|
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
|
||||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||||
|
@ -36,31 +36,17 @@ Front Page|Post
|
||||||
---|---
|
---|---
|
||||||
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
|
||||||
|
|
||||||
## 📝 Table of Contents
|
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||||
|
|
||||||
<!-- toc -->
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
- [Features](#features)
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
- [About](#about)
|
|
||||||
* [Why's it called Lemmy?](#whys-it-called-lemmy)
|
|
||||||
- [Install](#install)
|
|
||||||
* [Docker](#docker)
|
|
||||||
+ [Updating](#updating)
|
|
||||||
* [Ansible](#ansible)
|
|
||||||
* [Kubernetes](#kubernetes)
|
|
||||||
- [Develop](#develop)
|
|
||||||
* [Docker Development](#docker-development)
|
|
||||||
* [Local Development](#local-development)
|
|
||||||
+ [Requirements](#requirements)
|
|
||||||
+ [Set up Postgres DB](#set-up-postgres-db)
|
|
||||||
+ [Running](#running)
|
|
||||||
- [Configuration](#configuration)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [Support](#support)
|
|
||||||
- [Translations](#translations)
|
|
||||||
- [Credits](#credits)
|
|
||||||
|
|
||||||
<!-- tocstop -->
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
||||||
|
|
||||||
|
[Documentation](https://dev.lemmy.ml/docs/index.html)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -91,25 +77,13 @@ Front Page|Post
|
||||||
- Front end is `~80kB` gzipped.
|
- Front end is `~80kB` gzipped.
|
||||||
- Supports arm64 / Raspberry Pi.
|
- Supports arm64 / Raspberry Pi.
|
||||||
|
|
||||||
## About
|
## Why's it called Lemmy?
|
||||||
|
|
||||||
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
|
||||||
|
|
||||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
|
||||||
|
|
||||||
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
|
||||||
|
|
||||||
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
|
||||||
|
|
||||||
### Why's it called Lemmy?
|
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
@ -121,7 +95,7 @@ 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/lemmy.hjson
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -157,88 +131,6 @@ nano inventory # enter your server, domain, contact email
|
||||||
ansible-playbook lemmy.yml --become
|
ansible-playbook lemmy.yml --become
|
||||||
```
|
```
|
||||||
|
|
||||||
### Kubernetes
|
|
||||||
|
|
||||||
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
|
|
||||||
Setting this up will vary depending on your provider.
|
|
||||||
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
|
||||||
|
|
||||||
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
|
|
||||||
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
|
|
||||||
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
|
|
||||||
|
|
||||||
**Important** Running a database in Kubernetes will work, but is generally not recommended.
|
|
||||||
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
|
|
||||||
|
|
||||||
Now you can deploy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
|
|
||||||
# otherwise your resources will be created in the `default` namespace.
|
|
||||||
kubectl apply -f docker/k8s/db.yml
|
|
||||||
kubectl apply -f docker/k8s/pictshare.yml
|
|
||||||
kubectl apply -f docker/k8s/lemmy.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
### Docker Development
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dessalines/lemmy
|
|
||||||
cd lemmy/docker/dev
|
|
||||||
./docker_update.sh # This builds and runs it, updating for your changes
|
|
||||||
```
|
|
||||||
|
|
||||||
and go to http://localhost:8536.
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
#### Requirements
|
|
||||||
|
|
||||||
- [Rust](https://www.rust-lang.org/)
|
|
||||||
- [Yarn](https://yarnpkg.com/en/)
|
|
||||||
- [Postgres](https://www.postgresql.org/)
|
|
||||||
|
|
||||||
#### Set up Postgres DB
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
|
||||||
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
|
||||||
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dessalines/lemmy
|
|
||||||
cd lemmy
|
|
||||||
./install.sh
|
|
||||||
# For live coding, where both the front and back end, automagically reload on any save, do:
|
|
||||||
# cd ui && yarn start
|
|
||||||
# cd server && cargo watch -x run
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [Websocket API for App developers](docs/api.md)
|
|
||||||
- [ActivityPub API.md](docs/apub_api_outline.md)
|
|
||||||
- [Goals](docs/goals.md)
|
|
||||||
- [Ranking Algorithm](docs/ranking.md)
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
@ -257,16 +149,15 @@ If you'd like to add translations, take a look a look at the [English translatio
|
||||||
|
|
||||||
lang | done | missing
|
lang | done | missing
|
||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
de | 97% | avatar,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
de | 96% | avatar,docs,old_password,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
eo | 84% | number_of_communities,preview,upload_image,avatar,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,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
|
eo | 83% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
|
||||||
es | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
es | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
fr | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
fr | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
it | 93% | avatar,archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
it | 92% | avatar,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
nl | 86% | preview,upload_image,avatar,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,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
|
nl | 85% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
|
||||||
ru | 80% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,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,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
ru | 79% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
sv | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
sv | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||||
zh | 78% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,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,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
zh | 77% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||||
|
|
||||||
|
|
||||||
If you'd like to update this report, run:
|
If you'd like to update this report, run:
|
||||||
|
|
||||||
|
|
9
docker/dev/Dockerfile
vendored
9
docker/dev/Dockerfile
vendored
|
@ -32,6 +32,14 @@ RUN cargo build --frozen --release
|
||||||
# Get diesel-cli on there just in case
|
# Get diesel-cli on there just in case
|
||||||
# RUN cargo install diesel_cli --no-default-features --features postgres
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
|
||||||
|
FROM ekidd/rust-musl-builder:1.38.0-openssl11 as docs
|
||||||
|
WORKDIR /app
|
||||||
|
COPY docs ./docs
|
||||||
|
RUN sudo chown -R rust:rust .
|
||||||
|
RUN mdbook build docs/
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.10
|
FROM alpine:3.10
|
||||||
|
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
|
@ -40,6 +48,7 @@ RUN apk add libpq
|
||||||
# Copy resources
|
# Copy resources
|
||||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
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
|
||||||
|
|
20
docker/dev/deploy.sh
vendored
20
docker/dev/deploy.sh
vendored
|
@ -5,12 +5,14 @@ git checkout master
|
||||||
new_tag="$1"
|
new_tag="$1"
|
||||||
git tag $new_tag
|
git tag $new_tag
|
||||||
|
|
||||||
|
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||||
|
|
||||||
# Setting the version on the front end
|
# Setting the version on the front end
|
||||||
cd ../../
|
cd ../../
|
||||||
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||||
git add "ui/src/version.ts"
|
git add "ui/src/version.ts"
|
||||||
# Setting the version on the backend
|
# Setting the version on the backend
|
||||||
echo "pub const VERSION: &'static str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||||
git add "server/src/version.rs"
|
git add "server/src/version.rs"
|
||||||
|
|
||||||
cd docker/dev
|
cd docker/dev
|
||||||
|
@ -38,14 +40,22 @@ docker push dessalines/lemmy:x64-$new_tag
|
||||||
# docker push dessalines/lemmy:armv7hf-$new_tag
|
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||||
|
|
||||||
# aarch64
|
# aarch64
|
||||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
# Only do this on major releases (IE the third semver is 0)
|
||||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
if [ $third_semver -eq 0 ]; then
|
||||||
docker push dessalines/lemmy:arm64-$new_tag
|
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||||
|
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||||
|
docker push dessalines/lemmy:arm64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
# Creating the manifest for the multi-arch build
|
# Creating the manifest for the multi-arch build
|
||||||
docker manifest create dessalines/lemmy:$new_tag \
|
if [ $third_semver -eq 0 ]; then
|
||||||
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
dessalines/lemmy:x64-$new_tag \
|
dessalines/lemmy: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
|
||||||
|
|
||||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -11,7 +11,7 @@ services:
|
||||||
- lemmy_db:/var/lib/postgresql/data
|
- lemmy_db:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.5.9
|
image: dessalines/lemmy:v0.5.14
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
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 API](contributing_websocket_api.md)
|
||||||
|
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
20
docs/src/about.md
vendored
Normal file
20
docs/src/about.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Lemmy - A link aggregator / reddit clone for the fediverse.
|
||||||
|
|
||||||
|
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
|
||||||
|
|
||||||
|
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||||
|
|
||||||
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
|
|
||||||
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
### Why's it called Lemmy?
|
||||||
|
|
||||||
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
|
||||||
|
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
27
docs/src/about_features.md
vendored
Normal file
27
docs/src/about_features.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Features
|
||||||
|
- Open source, [AGPL License](/LICENSE).
|
||||||
|
- Self hostable, easy to deploy.
|
||||||
|
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||||
|
- Clean, mobile-friendly interface.
|
||||||
|
- Live-updating Comment threads.
|
||||||
|
- Full vote scores `(+/-)` like old reddit.
|
||||||
|
- Themes, including light, dark, and solarized.
|
||||||
|
- Emojis with autocomplete support. Start typing `:`
|
||||||
|
- User tagging using `@`, Community tagging using `#`.
|
||||||
|
- Notifications, on comment replies and when you're tagged.
|
||||||
|
- i18n / internationalization support.
|
||||||
|
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||||
|
- Cross-posting support.
|
||||||
|
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||||
|
- Moderation abilities.
|
||||||
|
- Public Moderation Logs.
|
||||||
|
- Both site admins, and community moderators, who can appoint other moderators.
|
||||||
|
- Can lock, remove, and restore posts and comments.
|
||||||
|
- Can ban and unban users from communities and the site.
|
||||||
|
- Can transfer site and communities to others.
|
||||||
|
- Can fully erase your data, replacing all posts and comments.
|
||||||
|
- NSFW post / community support.
|
||||||
|
- High performance.
|
||||||
|
- Server is written in rust.
|
||||||
|
- Front end is `~80kB` gzipped.
|
||||||
|
- Supports arm64 / Raspberry Pi.
|
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
1
docs/src/administration.md
vendored
Normal file
1
docs/src/administration.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Information for Lemmy instance admins, and those who want to start an instance.
|
6
docs/src/administration_configuration.md
vendored
Normal file
6
docs/src/administration_configuration.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file.
|
||||||
|
|
||||||
|
Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with
|
||||||
|
`LEMMY__DATABASE__POOL_SIZE=10`.
|
||||||
|
|
||||||
|
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once.
|
11
docs/src/administration_install_ansible.md
vendored
Normal file
11
docs/src/administration_install_ansible.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
|
||||||
|
|
||||||
|
Then run the following commands on your local computer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy.git
|
||||||
|
cd lemmy/ansible/
|
||||||
|
cp inventory.example inventory
|
||||||
|
nano inventory # enter your server, domain, contact email
|
||||||
|
ansible-playbook lemmy.yml --become
|
||||||
|
```
|
28
docs/src/administration_install_docker.md
vendored
Normal file
28
docs/src/administration_install_docker.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir lemmy/
|
||||||
|
cd lemmy/
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
|
# Edit lemmy.hjson to do more configuration
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
|
[A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
||||||
|
# Replace the {{ vars }}
|
||||||
|
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||||
|
```
|
||||||
|
#### Updating
|
||||||
|
|
||||||
|
To update to the newest version, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
|
||||||
|
Setting this up will vary depending on your provider.
|
||||||
|
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
||||||
|
|
||||||
|
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
|
||||||
|
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
|
||||||
|
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
|
||||||
|
|
||||||
|
**Important** Running a database in Kubernetes will work, but is generally not recommended.
|
||||||
|
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
|
||||||
|
|
||||||
|
Now you can deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
|
||||||
|
# otherwise your resources will be created in the `default` namespace.
|
||||||
|
kubectl apply -f docker/k8s/db.yml
|
||||||
|
kubectl apply -f docker/k8s/pictshare.yml
|
||||||
|
kubectl apply -f docker/k8s/lemmy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
|
1
docs/src/contributing.md
vendored
Normal file
1
docs/src/contributing.md
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.
|
11
docs/src/contributing_docker_development.md
vendored
Normal file
11
docs/src/contributing_docker_development.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy
|
||||||
|
cd lemmy/docker/dev
|
||||||
|
./docker_update.sh # This builds and runs it, updating for your changes
|
||||||
|
```
|
||||||
|
|
||||||
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
|
Note that compile times are relatively long with Docker, because builds can't be properly cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
|
24
docs/src/contributing_local_development.md
vendored
Normal file
24
docs/src/contributing_local_development.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
- [Rust](https://www.rust-lang.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/en/)
|
||||||
|
- [Postgres](https://www.postgresql.org/)
|
||||||
|
|
||||||
|
#### Set up Postgres DB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
|
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
|
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dessalines/lemmy
|
||||||
|
cd lemmy
|
||||||
|
./install.sh
|
||||||
|
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||||
|
# cd ui && yarn start
|
||||||
|
# cd server && cargo watch -x run
|
||||||
|
```
|
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;
|
|
@ -51,7 +51,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -59,12 +59,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
@ -82,14 +82,14 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
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;
|
||||||
|
@ -124,7 +124,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
|
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||||
|
@ -143,7 +143,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -163,17 +163,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_comment_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,14 +196,14 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let extracted_usernames = extract_usernames(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
|
|
||||||
for username_mention in &extracted_usernames {
|
for username_mention in &extracted_usernames {
|
||||||
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
|
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
|
||||||
|
|
||||||
if mention_user.is_ok() {
|
if mention_user.is_ok() {
|
||||||
let mention_user_id = mention_user?.id;
|
let mention_user_id = mention_user?.id;
|
||||||
|
@ -255,7 +255,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -268,12 +268,12 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match CommentSaved::save(&conn, &comment_saved_form) {
|
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -301,20 +301,20 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err(&self.op, "downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -332,7 +332,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,21 +136,24 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
let community_id = match data.id {
|
let community_id = match data.id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => {
|
None => {
|
||||||
match Community::read_from_name(&conn, data.name.to_owned().unwrap_or("main".to_string())) {
|
match Community::read_from_name(
|
||||||
|
&conn,
|
||||||
|
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
|
||||||
|
) {
|
||||||
Ok(community) => community.id,
|
Ok(community) => community.id,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
|
@ -176,21 +179,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| has_slurs(&data.title)
|
|| has_slurs(&data.title)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When you create a community, make sure the user becomes a moderator and a follower
|
// When you create a community, make sure the user becomes a moderator and a follower
|
||||||
|
@ -208,7 +211,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
|
|
||||||
let inserted_community = match Community::create(&conn, &community_form) {
|
let inserted_community = match Community::create(&conn, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let community_moderator_form = CommunityModeratorForm {
|
||||||
|
@ -220,10 +223,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
||||||
|
@ -252,21 +252,21 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
let data: &EditCommunity = &self.data;
|
let data: &EditCommunity = &self.data;
|
||||||
|
|
||||||
if has_slurs(&data.name) || has_slurs(&data.title) {
|
if has_slurs(&data.name) || has_slurs(&data.title) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify its a mod
|
// Verify its a mod
|
||||||
|
@ -279,7 +279,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
);
|
);
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_community_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_community_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
|
@ -296,7 +296,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
|
|
||||||
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -351,7 +351,7 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||||
|
|
||||||
let communities = CommunityQueryBuilder::create(&conn)
|
let communities = CommunityQueryBuilder::create(&conn)
|
||||||
.sort(&sort)
|
.sort(&sort)
|
||||||
.from_user_id(user_id)
|
.for_user(user_id)
|
||||||
.show_nsfw(show_nsfw)
|
.show_nsfw(show_nsfw)
|
||||||
.page(data.page)
|
.page(data.page)
|
||||||
.limit(data.limit)
|
.limit(data.limit)
|
||||||
|
@ -372,7 +372,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -385,12 +385,12 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
if data.follow {
|
if data.follow {
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +410,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -418,7 +418,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
let communities: Vec<CommunityFollowerView> =
|
let communities: Vec<CommunityFollowerView> =
|
||||||
match CommunityFollowerView::for_user(&conn, user_id) {
|
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||||
Ok(communities) => communities,
|
Ok(communities) => communities,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "system_err_login"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -436,7 +436,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -449,12 +449,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
if data.ban {
|
if data.ban {
|
||||||
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,7 +491,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -505,20 +505,14 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -548,7 +542,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -562,14 +556,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
// Make sure user is the creator, or an admin
|
// Make sure user is the creator, or an admin
|
||||||
if user_id != read_community.creator_id
|
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
|
||||||
&& !admins
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
.iter()
|
|
||||||
.map(|a| a.id)
|
|
||||||
.collect::<Vec<i32>>()
|
|
||||||
.contains(&user_id)
|
|
||||||
{
|
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
|
@ -586,7 +574,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// You also have to re-do the community_moderator table, reordering it.
|
// You also have to re-do the community_moderator table, reordering it.
|
||||||
|
@ -610,10 +598,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -629,12 +614,12 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
|
|
@ -93,23 +93,23 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
|
@ -128,7 +128,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
|
|
||||||
let inserted_post = match Post::create(&conn, &post_form) {
|
let inserted_post = match Post::create(&conn, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// They like their own post by default
|
// They like their own post by default
|
||||||
|
@ -141,13 +141,13 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostResponse {
|
Ok(PostResponse {
|
||||||
|
@ -175,7 +175,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||||
|
|
||||||
let post_view = match PostView::read(&conn, data.id, user_id) {
|
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let comments = CommentQueryBuilder::create(&conn)
|
let comments = CommentQueryBuilder::create(&conn)
|
||||||
|
@ -243,7 +243,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||||
.list()
|
.list()
|
||||||
{
|
{
|
||||||
Ok(posts) => posts,
|
Ok(posts) => posts,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(GetPostsResponse {
|
Ok(GetPostsResponse {
|
||||||
|
@ -260,7 +260,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -268,20 +268,20 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = SiteView::read(&conn)?;
|
let site = SiteView::read(&conn)?;
|
||||||
if site.enable_downvotes == false {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
|
return Err(APIError::err(&self.op, "downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post = Post::read(&conn, data.post_id)?;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let like_form = PostLikeForm {
|
let like_form = PostLikeForm {
|
||||||
|
@ -294,17 +294,17 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
|
||||||
PostLike::remove(&conn, &like_form)?;
|
PostLike::remove(&conn, &like_form)?;
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
Ok(like) => like,
|
Ok(like) => like,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// just output the score
|
// just output the score
|
||||||
|
@ -319,14 +319,14 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
fn perform(&self) -> Result<PostResponse, Error> {
|
fn perform(&self) -> Result<PostResponse, Error> {
|
||||||
let data: &EditPost = &self.data;
|
let data: &EditPost = &self.data;
|
||||||
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -341,17 +341,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
);
|
);
|
||||||
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err(&self.op, "no_post_edit_allowed"))?;
|
return Err(APIError::err(&self.op, "no_post_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
return Err(APIError::err(&self.op, "community_ban"))?;
|
return Err(APIError::err(&self.op, "community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if UserView::read(&conn, user_id)?.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err(&self.op, "site_ban"))?;
|
return Err(APIError::err(&self.op, "site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
|
@ -370,7 +370,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -418,7 +418,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -431,12 +431,12 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
if data.save {
|
if data.save {
|
||||||
match PostSaved::save(&conn, &post_saved_form) {
|
match PostSaved::save(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
match PostSaved::unsave(&conn, &post_saved_form) {
|
match PostSaved::unsave(&conn, &post_saved_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,16 +160,15 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// These arrays are only for the full modlog, when a community isn't given
|
// These arrays are only for the full modlog, when a community isn't given
|
||||||
let mut removed_communities = Vec::new();
|
let (removed_communities, banned, added) = if data.community_id.is_none() {
|
||||||
let mut banned = Vec::new();
|
(
|
||||||
let mut added = Vec::new();
|
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
|
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
if data.community_id.is_none() {
|
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
removed_communities =
|
)
|
||||||
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
} else {
|
||||||
banned = ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
added = ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetModlogResponse {
|
Ok(GetModlogResponse {
|
||||||
|
@ -194,20 +193,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if !UserView::read(&conn, user_id)?.admin {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -222,7 +221,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
|
|
||||||
match Site::create(&conn, &site_form) {
|
match Site::create(&conn, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
@ -241,20 +240,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_slurs(&data.name)
|
if has_slurs(&data.name)
|
||||||
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
|
||||||
{
|
{
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let found_site = Site::read(&conn, 1)?;
|
let found_site = Site::read(&conn, 1)?;
|
||||||
|
@ -271,7 +270,7 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
@ -426,7 +425,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -435,7 +434,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
// Make sure user is the creator
|
// Make sure user is the creator
|
||||||
if read_site.creator_id != user_id {
|
if read_site.creator_id != user_id {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
|
@ -450,7 +449,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
|
|
||||||
match Site::update(&conn, 1, &site_form) {
|
match Site::update(&conn, 1, &site_form) {
|
||||||
Ok(site) => site,
|
Ok(site) => site,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
|
|
@ -28,6 +28,10 @@ pub struct SaveUserSettings {
|
||||||
default_listing_type: i16,
|
default_listing_type: i16,
|
||||||
lang: String,
|
lang: String,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
new_password: Option<String>,
|
||||||
|
new_password_verify: Option<String>,
|
||||||
|
old_password: Option<String>,
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,18 +172,13 @@ impl Perform<LoginResponse> for Oper<Login> {
|
||||||
// Fetch that username / email
|
// Fetch that username / email
|
||||||
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -198,22 +197,22 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
// Make sure site has open registration
|
// Make sure site has open registration
|
||||||
if let Ok(site) = SiteView::read(&conn) {
|
if let Ok(site) = SiteView::read(&conn) {
|
||||||
if !site.open_registration {
|
if !site.open_registration {
|
||||||
return Err(APIError::err(&self.op, "registration_closed"))?;
|
return Err(APIError::err(&self.op, "registration_closed").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_slurs(&data.username) {
|
if has_slurs(&data.username) {
|
||||||
return Err(APIError::err(&self.op, "no_slurs"))?;
|
return Err(APIError::err(&self.op, "no_slurs").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are no admins
|
// Make sure there are no admins
|
||||||
if data.admin && UserView::admins(&conn)?.len() > 0 {
|
if data.admin && !UserView::admins(&conn)?.is_empty() {
|
||||||
return Err(APIError::err(&self.op, "admin_already_created"))?;
|
return Err(APIError::err(&self.op, "admin_already_created").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the new user
|
// Register the new user
|
||||||
|
@ -237,7 +236,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
// Create the user
|
// Create the user
|
||||||
let inserted_user = match User_::register(&conn, &user_form) {
|
let inserted_user = match User_::register(&conn, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the main community if it doesn't exist
|
// Create the main community if it doesn't exist
|
||||||
|
@ -268,7 +267,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
let _inserted_community_follower =
|
let _inserted_community_follower =
|
||||||
match CommunityFollower::follow(&conn, &community_follower_form) {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If its an admin, add them as a mod and follower to main
|
// If its an admin, add them as a mod and follower to main
|
||||||
|
@ -282,10 +281,7 @@ impl Perform<LoginResponse> for Oper<Register> {
|
||||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
|
||||||
&self.op,
|
|
||||||
"community_moderator_already_exists",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -305,19 +301,52 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
let read_user = User_::read(&conn, user_id)?;
|
||||||
|
|
||||||
|
let email = match &data.email {
|
||||||
|
Some(email) => Some(email.to_owned()),
|
||||||
|
None => read_user.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
let password_encrypted = match &data.new_password {
|
||||||
|
Some(new_password) => {
|
||||||
|
match &data.new_password_verify {
|
||||||
|
Some(new_password_verify) => {
|
||||||
|
// Make sure passwords match
|
||||||
|
if new_password != new_password_verify {
|
||||||
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the old password
|
||||||
|
match &data.old_password {
|
||||||
|
Some(old_password) => {
|
||||||
|
let valid: bool =
|
||||||
|
verify(old_password, &read_user.password_encrypted).unwrap_or(false);
|
||||||
|
if !valid {
|
||||||
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
|
}
|
||||||
|
User_::update_password(&conn, user_id, &new_password)?.password_encrypted
|
||||||
|
}
|
||||||
|
None => return Err(APIError::err(&self.op, "password_incorrect").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(APIError::err(&self.op, "passwords_dont_match").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => read_user.password_encrypted,
|
||||||
|
};
|
||||||
|
|
||||||
let user_form = UserForm {
|
let user_form = UserForm {
|
||||||
name: read_user.name,
|
name: read_user.name,
|
||||||
fedi_name: read_user.fedi_name,
|
fedi_name: read_user.fedi_name,
|
||||||
email: read_user.email,
|
email,
|
||||||
avatar: data.avatar.to_owned(),
|
avatar: data.avatar.to_owned(),
|
||||||
password_encrypted: read_user.password_encrypted,
|
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,
|
||||||
|
@ -331,7 +360,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||||
|
|
||||||
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
@ -372,14 +401,14 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||||
None => {
|
None => {
|
||||||
match User_::read_from_name(
|
match User_::read_from_name(
|
||||||
&conn,
|
&conn,
|
||||||
data.username.to_owned().unwrap_or("admin".to_string()),
|
data
|
||||||
|
.username
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| "admin".to_string()),
|
||||||
) {
|
) {
|
||||||
Ok(user) => user.id,
|
Ok(user) => user.id,
|
||||||
Err(_e) => {
|
Err(_e) => {
|
||||||
return Err(APIError::err(
|
return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into())
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,14 +470,14 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
@ -472,7 +501,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -504,14 +533,14 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
if UserView::read(&conn, user_id)?.admin == false {
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
return Err(APIError::err(&self.op, "not_an_admin"))?;
|
return Err(APIError::err(&self.op, "not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_user = User_::read(&conn, data.user_id)?;
|
let read_user = User_::read(&conn, data.user_id)?;
|
||||||
|
@ -535,7 +564,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||||
|
|
||||||
match User_::update(&conn, data.user_id, &user_form) {
|
match User_::update(&conn, data.user_id, &user_form) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -571,7 +600,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -599,7 +628,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -627,7 +656,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -643,7 +672,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||||
let _updated_user_mention =
|
let _updated_user_mention =
|
||||||
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
|
||||||
|
@ -662,7 +691,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -687,7 +716,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -708,7 +737,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||||
let _updated_mention =
|
let _updated_mention =
|
||||||
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
|
||||||
Ok(mention) => mention,
|
Ok(mention) => mention,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -726,7 +755,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
@ -736,7 +765,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
// Verify the password
|
// Verify the password
|
||||||
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(APIError::err(&self.op, "password_incorrect"))?;
|
return Err(APIError::err(&self.op, "password_incorrect").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
|
@ -759,7 +788,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -787,7 +816,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||||
|
|
||||||
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
let _updated_post = match Post::update(&conn, post.id, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -806,12 +835,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||||
// Fetch that email
|
// Fetch that email
|
||||||
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => {
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
|
||||||
return Err(APIError::err(
|
|
||||||
&self.op,
|
|
||||||
"couldnt_find_that_username_or_email",
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate a random token
|
// Generate a random token
|
||||||
|
@ -828,7 +852,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||||
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
|
||||||
match send_email(subject, user_email, &user.name, html) {
|
match send_email(subject, user_email, &user.name, html) {
|
||||||
Ok(_o) => _o,
|
Ok(_o) => _o,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?,
|
Err(_e) => return Err(APIError::err(&self.op, &_e).into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PasswordResetResponse {
|
Ok(PasswordResetResponse {
|
||||||
|
@ -846,34 +870,14 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
|
||||||
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
||||||
|
|
||||||
// Make sure passwords match
|
// Make sure passwords match
|
||||||
if &data.password != &data.password_verify {
|
if data.password != data.password_verify {
|
||||||
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
|
return Err(APIError::err(&self.op, "passwords_dont_match").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the user
|
|
||||||
let read_user = User_::read(&conn, user_id)?;
|
|
||||||
|
|
||||||
// Update the user with the new password
|
// Update the user with the new password
|
||||||
let user_form = UserForm {
|
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
|
||||||
name: read_user.name,
|
|
||||||
fedi_name: read_user.fedi_name,
|
|
||||||
email: read_user.email,
|
|
||||||
avatar: read_user.avatar,
|
|
||||||
password_encrypted: data.password.to_owned(),
|
|
||||||
preferred_username: read_user.preferred_username,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
admin: read_user.admin,
|
|
||||||
banned: read_user.banned,
|
|
||||||
show_nsfw: read_user.show_nsfw,
|
|
||||||
theme: read_user.theme,
|
|
||||||
default_sort_type: read_user.default_sort_type,
|
|
||||||
default_listing_type: read_user.default_listing_type,
|
|
||||||
lang: read_user.lang,
|
|
||||||
};
|
|
||||||
|
|
||||||
let updated_user = match User_::update_password(&conn, user_id, &user_form) {
|
|
||||||
Ok(user) => user,
|
Ok(user) => user,
|
||||||
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
|
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
|
|
|
@ -60,10 +60,7 @@ impl Community {
|
||||||
|
|
||||||
let mut collection = UnorderedCollection::default();
|
let mut collection = UnorderedCollection::default();
|
||||||
collection.object_props.set_context_object(context()).ok();
|
collection.object_props.set_context_object(context()).ok();
|
||||||
collection
|
collection.object_props.set_id_string(base_url).ok();
|
||||||
.object_props
|
|
||||||
.set_id_string(base_url.to_string())
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let connection = establish_connection();
|
let connection = establish_connection();
|
||||||
//As we are an object, we validated that the community id was valid
|
//As we are an object, we validated that the community id was valid
|
||||||
|
|
|
@ -9,7 +9,7 @@ impl Post {
|
||||||
let mut page = Page::default();
|
let mut page = Page::default();
|
||||||
|
|
||||||
page.object_props.set_context_object(context()).ok();
|
page.object_props.set_context_object(context()).ok();
|
||||||
page.object_props.set_id_string(base_url.to_string()).ok();
|
page.object_props.set_id_string(base_url).ok();
|
||||||
page.object_props.set_name_string(self.name.to_owned()).ok();
|
page.object_props.set_name_string(self.name.to_owned()).ok();
|
||||||
|
|
||||||
if let Some(body) = &self.body {
|
if let Some(body) = &self.body {
|
||||||
|
|
|
@ -60,6 +60,7 @@ pub fn get_remote_community(identifier: String) -> Result<GetCommunityResponse,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
creator_name: "".to_string(),
|
creator_name: "".to_string(),
|
||||||
|
creator_avatar: None,
|
||||||
category_name: "".to_string(),
|
category_name: "".to_string(),
|
||||||
number_of_subscribers: -1,
|
number_of_subscribers: -1,
|
||||||
number_of_posts: -1,
|
number_of_posts: -1,
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl<'a> CommunityQueryBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_user_id<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
pub fn for_user<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
|
||||||
self.from_user_id = from_user_id.get_optional();
|
self.from_user_id = from_user_id.get_optional();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,13 +101,13 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,12 +118,12 @@ lazy_static! {
|
||||||
Pool::builder()
|
Pool::builder()
|
||||||
.max_size(Settings::get().database.pool_size)
|
.max_size(Settings::get().database.pool_size)
|
||||||
.build(manager)
|
.build(manager)
|
||||||
.expect(&format!("Error connecting to {}", db_url))
|
.unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn establish_connection() -> PooledConnection<ConnectionManager<PgConnection>> {
|
pub fn establish_connection() -> PooledConnection<ConnectionManager<PgConnection>> {
|
||||||
return PG_POOL.get().unwrap();
|
PG_POOL.get().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
|
|
|
@ -189,12 +189,9 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
|
|
||||||
let mut query = self.query;
|
let mut query = self.query;
|
||||||
|
|
||||||
match self.listing_type {
|
if let ListingType::Subscribed = self.listing_type {
|
||||||
ListingType::Subscribed => {
|
|
||||||
query = query.filter(subscribed.eq(true));
|
query = query.filter(subscribed.eq(true));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
query = match self.sort {
|
query = match self.sort {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
|
|
|
@ -75,14 +75,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> {
|
||||||
|
|
|
@ -7,6 +7,7 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
|
email -> Nullable<Text>,
|
||||||
fedi_name -> Varchar,
|
fedi_name -> Varchar,
|
||||||
admin -> Bool,
|
admin -> Bool,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
|
@ -26,6 +27,7 @@ pub struct UserView {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
pub fedi_name: String,
|
pub fedi_name: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
|
|
|
@ -25,12 +25,10 @@ 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 settings;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod webfinger;
|
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
|
|
@ -2,294 +2,40 @@ extern crate lemmy_server;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel_migrations;
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_files::NamedFile;
|
|
||||||
use actix_web::web::Query;
|
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use actix_web_actors::ws;
|
|
||||||
use lemmy_server::api::community::ListCommunities;
|
|
||||||
use lemmy_server::api::Oper;
|
|
||||||
use lemmy_server::api::Perform;
|
|
||||||
use lemmy_server::api::UserOperation;
|
|
||||||
use lemmy_server::apub;
|
|
||||||
use lemmy_server::db::establish_connection;
|
use lemmy_server::db::establish_connection;
|
||||||
use lemmy_server::feeds;
|
use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
|
||||||
use lemmy_server::nodeinfo;
|
|
||||||
use lemmy_server::settings::Settings;
|
use lemmy_server::settings::Settings;
|
||||||
use lemmy_server::webfinger;
|
|
||||||
use lemmy_server::websocket::server::*;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
embed_migrations!();
|
embed_migrations!();
|
||||||
|
|
||||||
/// How often heartbeat pings are sent
|
|
||||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
|
||||||
/// How long before lack of client response causes a timeout
|
|
||||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
/// Entry point for our route
|
|
||||||
fn chat_route(
|
|
||||||
req: HttpRequest,
|
|
||||||
stream: web::Payload,
|
|
||||||
chat_server: web::Data<Addr<ChatServer>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
ws::start(
|
|
||||||
WSSession {
|
|
||||||
cs_addr: chat_server.get_ref().to_owned(),
|
|
||||||
id: 0,
|
|
||||||
hb: Instant::now(),
|
|
||||||
ip: req
|
|
||||||
.connection_info()
|
|
||||||
.remote()
|
|
||||||
.unwrap_or("127.0.0.1:12345")
|
|
||||||
.split(":")
|
|
||||||
.next()
|
|
||||||
.unwrap_or("127.0.0.1")
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
&req,
|
|
||||||
stream,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WSSession {
|
|
||||||
cs_addr: Addr<ChatServer>,
|
|
||||||
/// unique session id
|
|
||||||
id: usize,
|
|
||||||
ip: String,
|
|
||||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
|
||||||
/// otherwise we drop connection.
|
|
||||||
hb: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for WSSession {
|
|
||||||
type Context = ws::WebsocketContext<Self>;
|
|
||||||
|
|
||||||
/// Method is called on actor start.
|
|
||||||
/// We register ws session with ChatServer
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
// we'll start heartbeat process on session start.
|
|
||||||
self.hb(ctx);
|
|
||||||
|
|
||||||
// register self in chat server. `AsyncContext::wait` register
|
|
||||||
// future within context, but context waits until this future resolves
|
|
||||||
// before processing any other events.
|
|
||||||
// across all routes within application
|
|
||||||
let addr = ctx.address();
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(Connect {
|
|
||||||
addr: addr.recipient(),
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, act, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => act.id = res,
|
|
||||||
// something is wrong with chat server
|
|
||||||
_ => ctx.stop(),
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
|
||||||
// notify chat server
|
|
||||||
self.cs_addr.do_send(Disconnect {
|
|
||||||
id: self.id,
|
|
||||||
ip: self.ip.to_owned(),
|
|
||||||
});
|
|
||||||
Running::Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle messages from chat server, we simply send it to peer websocket
|
|
||||||
/// These are room messages, IE sent to others in the room
|
|
||||||
impl Handler<WSMessage> for WSSession {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
|
||||||
// println!("id: {} msg: {}", self.id, msg.0);
|
|
||||||
ctx.text(msg.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WebSocket message handler
|
|
||||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
|
||||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
|
||||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
|
||||||
match msg {
|
|
||||||
ws::Message::Ping(msg) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
ctx.pong(&msg);
|
|
||||||
}
|
|
||||||
ws::Message::Pong(_) => {
|
|
||||||
self.hb = Instant::now();
|
|
||||||
}
|
|
||||||
ws::Message::Text(text) => {
|
|
||||||
let m = text.trim().to_owned();
|
|
||||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
|
||||||
|
|
||||||
self
|
|
||||||
.cs_addr
|
|
||||||
.send(StandardMessage {
|
|
||||||
id: self.id,
|
|
||||||
msg: m,
|
|
||||||
})
|
|
||||||
.into_actor(self)
|
|
||||||
.then(|res, _, ctx| {
|
|
||||||
match res {
|
|
||||||
Ok(res) => ctx.text(res),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", &e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fut::ok(())
|
|
||||||
})
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
|
||||||
ws::Message::Close(_) => {
|
|
||||||
ctx.stop();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WSSession {
|
|
||||||
/// helper method that sends ping to client every second.
|
|
||||||
///
|
|
||||||
/// also this method checks heartbeats from client
|
|
||||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
|
||||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
|
||||||
// check client heartbeats
|
|
||||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
|
||||||
// heartbeat timed out
|
|
||||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
|
||||||
|
|
||||||
// notify chat server
|
|
||||||
act.cs_addr.do_send(Disconnect {
|
|
||||||
id: act.id,
|
|
||||||
ip: act.ip.to_owned(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// stop actor
|
|
||||||
ctx.stop();
|
|
||||||
|
|
||||||
// don't try to send a ping
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ping("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let _ = env_logger::init();
|
env_logger::init();
|
||||||
let sys = actix::System::new("lemmy");
|
let sys = actix::System::new("lemmy");
|
||||||
|
|
||||||
// Run the migrations from code
|
// Run the migrations from code
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
embedded_migrations::run(&conn).unwrap();
|
embedded_migrations::run(&conn).unwrap();
|
||||||
|
|
||||||
// Start chat server actor in separate thread
|
|
||||||
let server = ChatServer::default().start();
|
|
||||||
|
|
||||||
let settings = Settings::get();
|
let settings = Settings::get();
|
||||||
|
|
||||||
// Create Http server with websocket support
|
// Create Http server with websocket support
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let app = App::new()
|
App::new()
|
||||||
.data(server.clone())
|
.configure(federation::config)
|
||||||
// Front end routes
|
.configure(feeds::config)
|
||||||
|
.configure(index::config)
|
||||||
|
.configure(nodeinfo::config)
|
||||||
|
.configure(webfinger::config)
|
||||||
|
.configure(websocket::config)
|
||||||
.service(actix_files::Files::new(
|
.service(actix_files::Files::new(
|
||||||
"/static",
|
"/static",
|
||||||
settings.front_end_dir.to_owned(),
|
settings.front_end_dir.to_owned(),
|
||||||
))
|
))
|
||||||
.route("/", web::get().to(index))
|
.service(actix_files::Files::new(
|
||||||
.route(
|
"/docs",
|
||||||
"/home/type/{type}/sort/{sort}/page/{page}",
|
settings.front_end_dir.to_owned() + "/documentation",
|
||||||
web::get().to(index),
|
))
|
||||||
)
|
|
||||||
.route("/login", web::get().to(index))
|
|
||||||
.route("/create_post", web::get().to(index))
|
|
||||||
.route("/create_community", web::get().to(index))
|
|
||||||
.route("/communities/page/{page}", web::get().to(index))
|
|
||||||
.route("/communities", web::get().to(index))
|
|
||||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
|
||||||
.route("/post/{id}", web::get().to(index))
|
|
||||||
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
|
||||||
.route("/c/{name}", web::get().to(index))
|
|
||||||
.route("/community/{id}", web::get().to(index))
|
|
||||||
.route(
|
|
||||||
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/u/{username}", web::get().to(index))
|
|
||||||
.route("/user/{id}", web::get().to(index))
|
|
||||||
.route("/inbox", web::get().to(index))
|
|
||||||
.route("/modlog/community/{community_id}", web::get().to(index))
|
|
||||||
.route("/modlog", web::get().to(index))
|
|
||||||
.route("/setup", web::get().to(index))
|
|
||||||
.route(
|
|
||||||
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
|
||||||
web::get().to(index),
|
|
||||||
)
|
|
||||||
.route("/search", web::get().to(index))
|
|
||||||
.route("/sponsors", web::get().to(index))
|
|
||||||
.route("/password_change/{token}", web::get().to(index))
|
|
||||||
// Websocket
|
|
||||||
.service(web::resource("/api/v1/ws").to(chat_route))
|
|
||||||
// NodeInfo
|
|
||||||
.route("/nodeinfo/2.0.json", web::get().to(nodeinfo::node_info))
|
|
||||||
.route(
|
|
||||||
"/.well-known/nodeinfo",
|
|
||||||
web::get().to(nodeinfo::node_info_well_known),
|
|
||||||
)
|
|
||||||
// RSS
|
|
||||||
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
|
||||||
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
|
||||||
// Federation
|
|
||||||
.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),
|
|
||||||
)
|
|
||||||
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
|
|
||||||
|
|
||||||
// Federation
|
|
||||||
if Settings::get().federation_enabled {
|
|
||||||
println!("federation enabled, host is {}", Settings::get().hostname);
|
|
||||||
app
|
|
||||||
.route(
|
|
||||||
".well-known/webfinger",
|
|
||||||
web::get().to(webfinger::get_webfinger_response),
|
|
||||||
)
|
|
||||||
// TODO: this is a very quick and dirty implementation for http api calls
|
|
||||||
.route(
|
|
||||||
"/api/v1/communities/list",
|
|
||||||
web::get().to(|query: Query<ListCommunities>| {
|
|
||||||
let res = Oper::new(UserOperation::ListCommunities, query.into_inner())
|
|
||||||
.perform()
|
|
||||||
.unwrap();
|
|
||||||
HttpResponse::Ok()
|
|
||||||
.content_type("application/json")
|
|
||||||
.body(serde_json::to_string(&res).unwrap())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
app
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.bind((settings.bind, settings.port))
|
.bind((settings.bind, settings.port))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -299,9 +45,3 @@ fn main() {
|
||||||
|
|
||||||
let _ = sys.run();
|
let _ = sys.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
|
||||||
Ok(NamedFile::open(
|
|
||||||
Settings::get().front_end_dir.to_owned() + "/index.html",
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
38
server/src/routes/federation.rs
Normal file
38
server/src/routes/federation.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use crate::api::community::ListCommunities;
|
||||||
|
use crate::api::Perform;
|
||||||
|
use crate::api::{Oper, UserOperation};
|
||||||
|
use crate::apub;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use actix_web::web::Query;
|
||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
if Settings::get().federation_enabled {
|
||||||
|
println!("federation enabled, host is {}", Settings::get().hostname);
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
// TODO: this is a very quick and dirty implementation for http api calls
|
||||||
|
.route(
|
||||||
|
"/api/v1/communities/list",
|
||||||
|
web::get().to(|query: Query<ListCommunities>| {
|
||||||
|
let res = Oper::new(UserOperation::ListCommunities, query.into_inner())
|
||||||
|
.perform()
|
||||||
|
.unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(serde_json::to_string(&res).unwrap())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,12 +5,13 @@ use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
|
||||||
use crate::db::community::Community;
|
use crate::db::community::Community;
|
||||||
use crate::db::post_view::{PostQueryBuilder, PostView};
|
use crate::db::post_view::{PostQueryBuilder, PostView};
|
||||||
use crate::db::site_view::SiteView;
|
use crate::db::site_view::SiteView;
|
||||||
use crate::db::user::User_;
|
use crate::db::user::{Claims, User_};
|
||||||
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
||||||
use crate::db::{establish_connection, ListingType, SortType};
|
use crate::db::{establish_connection, ListingType, SortType};
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
use actix_web::{web, HttpResponse, Result};
|
use actix_web::{web, HttpResponse, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -29,7 +30,14 @@ enum RequestType {
|
||||||
Inbox,
|
Inbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
|
||||||
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
|
||||||
|
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
||||||
let sort_type = match get_sort_type(info) {
|
let sort_type = match get_sort_type(info) {
|
||||||
Ok(sort_type) => sort_type,
|
Ok(sort_type) => sort_type,
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
@ -45,7 +53,7 @@ pub fn get_all_feed(info: web::Query<Params>) -> HttpResponse<Body> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
|
fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) -> HttpResponse<Body> {
|
||||||
let sort_type = match get_sort_type(info) {
|
let sort_type = match get_sort_type(info) {
|
||||||
Ok(sort_type) => sort_type,
|
Ok(sort_type) => sort_type,
|
||||||
Err(_) => return HttpResponse::BadRequest().finish(),
|
Err(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
@ -77,7 +85,10 @@ pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
|
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
|
||||||
let sort_query = info.sort.to_owned().unwrap_or(SortType::Hot.to_string());
|
let sort_query = info
|
||||||
|
.sort
|
||||||
|
.to_owned()
|
||||||
|
.unwrap_or_else(|| SortType::Hot.to_string());
|
||||||
SortType::from_str(&sort_query)
|
SortType::from_str(&sort_query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +173,7 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::Subscribed)
|
.listing_type(ListingType::Subscribed)
|
||||||
|
@ -189,7 +200,7 @@ fn get_feed_inbox(jwt: String) -> Result<String, Error> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
|
|
||||||
let site_view = SiteView::read(&conn)?;
|
let site_view = SiteView::read(&conn)?;
|
||||||
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
|
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||||
|
|
||||||
let sort = SortType::New;
|
let sort = SortType::New;
|
||||||
|
|
45
server/src/routes/index.rs
Normal file
45
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/home/type/{type}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/login", web::get().to(index))
|
||||||
|
.route("/create_post", web::get().to(index))
|
||||||
|
.route("/create_community", web::get().to(index))
|
||||||
|
.route("/communities/page/{page}", web::get().to(index))
|
||||||
|
.route("/communities", web::get().to(index))
|
||||||
|
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||||
|
.route("/post/{id}", web::get().to(index))
|
||||||
|
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
||||||
|
.route("/c/{name}", web::get().to(index))
|
||||||
|
.route("/community/{id}", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/u/{username}", web::get().to(index))
|
||||||
|
.route("/user/{id}", web::get().to(index))
|
||||||
|
.route("/inbox", web::get().to(index))
|
||||||
|
.route("/modlog/community/{community_id}", web::get().to(index))
|
||||||
|
.route("/modlog", web::get().to(index))
|
||||||
|
.route("/setup", web::get().to(index))
|
||||||
|
.route(
|
||||||
|
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
||||||
|
web::get().to(index),
|
||||||
|
)
|
||||||
|
.route("/search", web::get().to(index))
|
||||||
|
.route("/sponsors", web::get().to(index))
|
||||||
|
.route("/password_change/{token}", web::get().to(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
||||||
|
Ok(NamedFile::open(
|
||||||
|
Settings::get().front_end_dir.to_owned() + "/index.html",
|
||||||
|
)?)
|
||||||
|
}
|
6
server/src/routes/mod.rs
Normal file
6
server/src/routes/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod federation;
|
||||||
|
pub mod feeds;
|
||||||
|
pub mod index;
|
||||||
|
pub mod nodeinfo;
|
||||||
|
pub mod webfinger;
|
||||||
|
pub mod websocket;
|
|
@ -3,9 +3,16 @@ use crate::db::site_view::SiteView;
|
||||||
use crate::version;
|
use crate::version;
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
|
use actix_web::web;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg
|
||||||
|
.route("/nodeinfo/2.0.json", web::get().to(node_info))
|
||||||
|
.route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn node_info_well_known() -> HttpResponse<Body> {
|
pub fn node_info_well_known() -> HttpResponse<Body> {
|
||||||
let json = json!({
|
let json = json!({
|
||||||
"links": {
|
"links": {
|
||||||
|
@ -14,12 +21,12 @@ pub fn node_info_well_known() -> HttpResponse<Body> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.body(json.to_string());
|
.body(json.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn node_info() -> HttpResponse<Body> {
|
fn node_info() -> HttpResponse<Body> {
|
||||||
let conn = establish_connection();
|
let conn = establish_connection();
|
||||||
let site_view = match SiteView::read(&conn) {
|
let site_view = match SiteView::read(&conn) {
|
||||||
Ok(site_view) => site_view,
|
Ok(site_view) => site_view,
|
||||||
|
@ -43,10 +50,10 @@ pub fn node_info() -> HttpResponse<Body> {
|
||||||
},
|
},
|
||||||
"localPosts": site_view.number_of_posts,
|
"localPosts": site_view.number_of_posts,
|
||||||
"localComments": site_view.number_of_comments,
|
"localComments": site_view.number_of_comments,
|
||||||
"openRegistrations": true,
|
"openRegistrations": site_view.open_registration,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("application/json")
|
.content_type("application/json")
|
||||||
.body(json.to_string());
|
.body(json.to_string())
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ use crate::db::community::Community;
|
||||||
use crate::db::establish_connection;
|
use crate::db::establish_connection;
|
||||||
use crate::Settings;
|
use crate::Settings;
|
||||||
use actix_web::body::Body;
|
use actix_web::body::Body;
|
||||||
|
use actix_web::web;
|
||||||
use actix_web::web::Query;
|
use actix_web::web::Query;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -13,6 +14,15 @@ pub struct Params {
|
||||||
resource: String,
|
resource: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
if Settings::get().federation_enabled {
|
||||||
|
cfg.route(
|
||||||
|
".well-known/webfinger",
|
||||||
|
web::get().to(get_webfinger_response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
|
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
|
||||||
"^group:([a-z0-9_]{{3, 20}})@{}$",
|
"^group:([a-z0-9_]{{3, 20}})@{}$",
|
||||||
|
@ -27,7 +37,7 @@ lazy_static! {
|
||||||
///
|
///
|
||||||
/// You can also view the webfinger response that Mastodon sends:
|
/// You can also view the webfinger response that Mastodon sends:
|
||||||
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
|
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
|
||||||
pub fn get_webfinger_response(info: Query<Params>) -> HttpResponse<Body> {
|
fn get_webfinger_response(info: Query<Params>) -> HttpResponse<Body> {
|
||||||
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
|
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
|
||||||
.captures(&info.resource)
|
.captures(&info.resource)
|
||||||
.map(|c| c.get(1));
|
.map(|c| c.get(1));
|
179
server/src/routes/websocket.rs
Normal file
179
server/src/routes/websocket.rs
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
use crate::websocket::server::*;
|
||||||
|
use actix::prelude::*;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::*;
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
// Start chat server actor in separate thread
|
||||||
|
let server = ChatServer::default().start();
|
||||||
|
cfg
|
||||||
|
.data(server)
|
||||||
|
.service(web::resource("/api/v1/ws").to(chat_route));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How often heartbeat pings are sent
|
||||||
|
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
/// How long before lack of client response causes a timeout
|
||||||
|
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// Entry point for our route
|
||||||
|
fn chat_route(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: web::Payload,
|
||||||
|
chat_server: web::Data<Addr<ChatServer>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
ws::start(
|
||||||
|
WSSession {
|
||||||
|
cs_addr: chat_server.get_ref().to_owned(),
|
||||||
|
id: 0,
|
||||||
|
hb: Instant::now(),
|
||||||
|
ip: req
|
||||||
|
.connection_info()
|
||||||
|
.remote()
|
||||||
|
.unwrap_or("127.0.0.1:12345")
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("127.0.0.1")
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
&req,
|
||||||
|
stream,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WSSession {
|
||||||
|
cs_addr: Addr<ChatServer>,
|
||||||
|
/// unique session id
|
||||||
|
id: usize,
|
||||||
|
ip: String,
|
||||||
|
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||||
|
/// otherwise we drop connection.
|
||||||
|
hb: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for WSSession {
|
||||||
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
|
||||||
|
/// Method is called on actor start.
|
||||||
|
/// We register ws session with ChatServer
|
||||||
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
// we'll start heartbeat process on session start.
|
||||||
|
self.hb(ctx);
|
||||||
|
|
||||||
|
// register self in chat server. `AsyncContext::wait` register
|
||||||
|
// future within context, but context waits until this future resolves
|
||||||
|
// before processing any other events.
|
||||||
|
// across all routes within application
|
||||||
|
let addr = ctx.address();
|
||||||
|
self
|
||||||
|
.cs_addr
|
||||||
|
.send(Connect {
|
||||||
|
addr: addr.recipient(),
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
|
})
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, act, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(res) => act.id = res,
|
||||||
|
// something is wrong with chat server
|
||||||
|
_ => ctx.stop(),
|
||||||
|
}
|
||||||
|
fut::ok(())
|
||||||
|
})
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
||||||
|
// notify chat server
|
||||||
|
self.cs_addr.do_send(Disconnect {
|
||||||
|
id: self.id,
|
||||||
|
ip: self.ip.to_owned(),
|
||||||
|
});
|
||||||
|
Running::Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle messages from chat server, we simply send it to peer websocket
|
||||||
|
/// These are room messages, IE sent to others in the room
|
||||||
|
impl Handler<WSMessage> for WSSession {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
||||||
|
// println!("id: {} msg: {}", self.id, msg.0);
|
||||||
|
ctx.text(msg.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket message handler
|
||||||
|
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
||||||
|
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||||
|
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||||
|
match msg {
|
||||||
|
ws::Message::Ping(msg) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
ctx.pong(&msg);
|
||||||
|
}
|
||||||
|
ws::Message::Pong(_) => {
|
||||||
|
self.hb = Instant::now();
|
||||||
|
}
|
||||||
|
ws::Message::Text(text) => {
|
||||||
|
let m = text.trim().to_owned();
|
||||||
|
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
||||||
|
|
||||||
|
self
|
||||||
|
.cs_addr
|
||||||
|
.send(StandardMessage {
|
||||||
|
id: self.id,
|
||||||
|
msg: m,
|
||||||
|
})
|
||||||
|
.into_actor(self)
|
||||||
|
.then(|res, _, ctx| {
|
||||||
|
match res {
|
||||||
|
Ok(res) => ctx.text(res),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fut::ok(())
|
||||||
|
})
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
||||||
|
ws::Message::Close(_) => {
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WSSession {
|
||||||
|
/// helper method that sends ping to client every second.
|
||||||
|
///
|
||||||
|
/// also this method checks heartbeats from client
|
||||||
|
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||||
|
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||||
|
// check client heartbeats
|
||||||
|
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||||
|
// heartbeat timed out
|
||||||
|
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||||
|
|
||||||
|
// notify chat server
|
||||||
|
act.cs_addr.do_send(Disconnect {
|
||||||
|
id: act.id,
|
||||||
|
ip: act.ip.to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// stop actor
|
||||||
|
ctx.stop();
|
||||||
|
|
||||||
|
// don't try to send a ping
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ping("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,10 +51,10 @@ pub struct Database {
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SETTINGS: Settings = {
|
static ref SETTINGS: Settings = {
|
||||||
return match Settings::init() {
|
match Settings::init() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => panic!("{}", e),
|
Err(e) => panic!("{}", e),
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ impl Settings {
|
||||||
// https://github.com/mehcode/config-rs/issues/73
|
// https://github.com/mehcode/config-rs/issues/73
|
||||||
s.merge(Environment::with_prefix("LEMMY").separator("__"))?;
|
s.merge(Environment::with_prefix("LEMMY").separator("__"))?;
|
||||||
|
|
||||||
return s.try_into();
|
s.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the config as a struct.
|
/// Returns the config as a struct.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
pub const VERSION: &'static str = "v0.5.9";
|
pub const VERSION: &str = "v0.5.14";
|
||||||
|
|
|
@ -92,7 +92,7 @@ impl Default for ChatServer {
|
||||||
ChatServer {
|
ChatServer {
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
rate_limits: HashMap::new(),
|
rate_limits: HashMap::new(),
|
||||||
rooms: rooms,
|
rooms,
|
||||||
rng: rand::thread_rng(),
|
rng: rand::thread_rng(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,8 +100,8 @@ impl Default for ChatServer {
|
||||||
|
|
||||||
impl ChatServer {
|
impl ChatServer {
|
||||||
/// Send message to all users in the room
|
/// Send message to all users in the room
|
||||||
fn send_room_message(&self, room: &i32, message: &str, skip_id: usize) {
|
fn send_room_message(&self, room: i32, message: &str, skip_id: usize) {
|
||||||
if let Some(sessions) = self.rooms.get(room) {
|
if let Some(sessions) = self.rooms.get(&room) {
|
||||||
for id in sessions {
|
for id in sessions {
|
||||||
if *id != skip_id {
|
if *id != skip_id {
|
||||||
if let Some(info) = self.sessions.get(id) {
|
if let Some(info) = self.sessions.get(id) {
|
||||||
|
@ -114,7 +114,7 @@ impl ChatServer {
|
||||||
|
|
||||||
fn join_room(&mut self, room_id: i32, id: usize) {
|
fn join_room(&mut self, room_id: i32, id: usize) {
|
||||||
// remove session from all rooms
|
// remove session from all rooms
|
||||||
for (_n, sessions) in &mut self.rooms {
|
for sessions in self.rooms.values_mut() {
|
||||||
sessions.remove(&id);
|
sessions.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,12 +123,12 @@ impl ChatServer {
|
||||||
self.rooms.insert(room_id, HashSet::new());
|
self.rooms.insert(room_id, HashSet::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
&self.rooms.get_mut(&room_id).unwrap().insert(id);
|
self.rooms.get_mut(&room_id).unwrap().insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_community_message(
|
fn send_community_message(
|
||||||
&self,
|
&self,
|
||||||
community_id: &i32,
|
community_id: i32,
|
||||||
message: &str,
|
message: &str,
|
||||||
skip_id: usize,
|
skip_id: usize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
@ -139,12 +139,12 @@ impl ChatServer {
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
.listing_type(ListingType::Community)
|
.listing_type(ListingType::Community)
|
||||||
.sort(&SortType::New)
|
.sort(&SortType::New)
|
||||||
.for_community_id(*community_id)
|
.for_community_id(community_id)
|
||||||
.limit(9999)
|
.limit(9999)
|
||||||
.list()?;
|
.list()?;
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
self.send_room_message(&post.id, message, skip_id);
|
self.send_room_message(post.id, message, skip_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -174,6 +174,7 @@ impl ChatServer {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> {
|
fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> {
|
||||||
if let Some(info) = self.sessions.get(&id) {
|
if let Some(info) = self.sessions.get(&id) {
|
||||||
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
|
||||||
|
@ -195,10 +196,13 @@ impl ChatServer {
|
||||||
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
||||||
&info.ip, time_passed, rate_limit.allowance
|
&info.ip, time_passed, rate_limit.allowance
|
||||||
);
|
);
|
||||||
Err(APIError {
|
Err(
|
||||||
|
APIError {
|
||||||
op: "Rate Limit".to_string(),
|
op: "Rate Limit".to_string(),
|
||||||
message: format!("Too many requests. {} per {} seconds", rate, per),
|
message: format!("Too many requests. {} per {} seconds", rate, per),
|
||||||
})?
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
rate_limit.allowance -= 1.0;
|
rate_limit.allowance -= 1.0;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -265,7 +269,7 @@ impl Handler<Disconnect> for ChatServer {
|
||||||
// remove address
|
// remove address
|
||||||
if self.sessions.remove(&msg.id).is_some() {
|
if self.sessions.remove(&msg.id).is_some() {
|
||||||
// remove session from all rooms
|
// remove session from all rooms
|
||||||
for (_id, sessions) in &mut self.rooms {
|
for sessions in self.rooms.values_mut() {
|
||||||
if sessions.remove(&msg.id) {
|
if sessions.remove(&msg.id) {
|
||||||
// rooms.push(*id);
|
// rooms.push(*id);
|
||||||
}
|
}
|
||||||
|
@ -293,7 +297,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let data = &json["data"].to_string();
|
let data = &json["data"].to_string();
|
||||||
let op = &json["op"].as_str().ok_or(APIError {
|
let op = &json["op"].as_str().ok_or(APIError {
|
||||||
op: "Unknown op type".to_string(),
|
op: "Unknown op type".to_string(),
|
||||||
message: format!("Unknown op type"),
|
message: "Unknown op type".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let user_operation: UserOperation = UserOperation::from_str(&op)?;
|
let user_operation: UserOperation = UserOperation::from_str(&op)?;
|
||||||
|
@ -396,7 +400,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
community_sent.community.user_id = None;
|
community_sent.community.user_id = None;
|
||||||
community_sent.community.subscribed = None;
|
community_sent.community.subscribed = None;
|
||||||
let community_sent_str = serde_json::to_string(&community_sent)?;
|
let community_sent_str = serde_json::to_string(&community_sent)?;
|
||||||
chat.send_community_message(&community_sent.community.id, &community_sent_str, msg.id)?;
|
chat.send_community_message(community_sent.community.id, &community_sent_str, msg.id)?;
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::FollowCommunity => {
|
UserOperation::FollowCommunity => {
|
||||||
|
@ -414,7 +418,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let community_id = ban_from_community.community_id;
|
let community_id = ban_from_community.community_id;
|
||||||
let res = Oper::new(user_operation, ban_from_community).perform()?;
|
let res = Oper::new(user_operation, ban_from_community).perform()?;
|
||||||
let res_str = serde_json::to_string(&res)?;
|
let res_str = serde_json::to_string(&res)?;
|
||||||
chat.send_community_message(&community_id, &res_str, msg.id)?;
|
chat.send_community_message(community_id, &res_str, msg.id)?;
|
||||||
Ok(res_str)
|
Ok(res_str)
|
||||||
}
|
}
|
||||||
UserOperation::AddModToCommunity => {
|
UserOperation::AddModToCommunity => {
|
||||||
|
@ -422,7 +426,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let community_id = mod_add_to_community.community_id;
|
let community_id = mod_add_to_community.community_id;
|
||||||
let res = Oper::new(user_operation, mod_add_to_community).perform()?;
|
let res = Oper::new(user_operation, mod_add_to_community).perform()?;
|
||||||
let res_str = serde_json::to_string(&res)?;
|
let res_str = serde_json::to_string(&res)?;
|
||||||
chat.send_community_message(&community_id, &res_str, msg.id)?;
|
chat.send_community_message(community_id, &res_str, msg.id)?;
|
||||||
Ok(res_str)
|
Ok(res_str)
|
||||||
}
|
}
|
||||||
UserOperation::ListCategories => {
|
UserOperation::ListCategories => {
|
||||||
|
@ -459,7 +463,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let mut post_sent = res.clone();
|
let mut post_sent = res.clone();
|
||||||
post_sent.post.my_vote = None;
|
post_sent.post.my_vote = None;
|
||||||
let post_sent_str = serde_json::to_string(&post_sent)?;
|
let post_sent_str = serde_json::to_string(&post_sent)?;
|
||||||
chat.send_room_message(&post_sent.post.id, &post_sent_str, msg.id);
|
chat.send_room_message(post_sent.post.id, &post_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::SavePost => {
|
UserOperation::SavePost => {
|
||||||
|
@ -476,7 +480,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::EditComment => {
|
UserOperation::EditComment => {
|
||||||
|
@ -487,7 +491,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::SaveComment => {
|
UserOperation::SaveComment => {
|
||||||
|
@ -504,7 +508,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
comment_sent.comment.my_vote = None;
|
comment_sent.comment.my_vote = None;
|
||||||
comment_sent.comment.user_id = None;
|
comment_sent.comment.user_id = None;
|
||||||
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
let comment_sent_str = serde_json::to_string(&comment_sent)?;
|
||||||
chat.send_room_message(&post_id, &comment_sent_str, msg.id);
|
chat.send_room_message(post_id, &comment_sent_str, msg.id);
|
||||||
Ok(serde_json::to_string(&res)?)
|
Ok(serde_json::to_string(&res)?)
|
||||||
}
|
}
|
||||||
UserOperation::GetModlog => {
|
UserOperation::GetModlog => {
|
||||||
|
|
151
ui/src/components/comment-node.tsx
vendored
151
ui/src/components/comment-node.tsx
vendored
|
@ -42,6 +42,8 @@ interface CommentNodeState {
|
||||||
banType: BanType;
|
banType: BanType;
|
||||||
showConfirmTransferSite: boolean;
|
showConfirmTransferSite: boolean;
|
||||||
showConfirmTransferCommunity: boolean;
|
showConfirmTransferCommunity: boolean;
|
||||||
|
showConfirmAppointAsMod: boolean;
|
||||||
|
showConfirmAppointAsAdmin: boolean;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
viewSource: boolean;
|
viewSource: boolean;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
viewSource: false,
|
viewSource: false,
|
||||||
showConfirmTransferSite: false,
|
showConfirmTransferSite: false,
|
||||||
showConfirmTransferCommunity: false,
|
showConfirmTransferCommunity: false,
|
||||||
|
showConfirmAppointAsMod: false,
|
||||||
|
showConfirmAppointAsAdmin: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -206,6 +210,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||||
|
{this.props.markable && (
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleMarkRead)}
|
||||||
|
>
|
||||||
|
{node.comment.read
|
||||||
|
? i18n.t('mark_as_unread')
|
||||||
|
: i18n.t('mark_as_read')}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{UserService.Instance.user && !this.props.viewOnly && (
|
{UserService.Instance.user && !this.props.viewOnly && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
@ -246,13 +262,35 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span
|
||||||
|
className="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleViewSource)}
|
||||||
|
>
|
||||||
|
<T i18nKey="view_source">#</T>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
||||||
|
>
|
||||||
|
<T i18nKey="link">#</T>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
{/* Admins and mods can remove comments */}
|
{/* Admins and mods can remove comments */}
|
||||||
{(this.canMod || this.canAdmin) && (
|
{(this.canMod || this.canAdmin) && (
|
||||||
|
<>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!node.comment.removed ? (
|
{!node.comment.removed ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleModRemoveShow
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<T i18nKey="remove">#</T>
|
<T i18nKey="remove">#</T>
|
||||||
</span>
|
</span>
|
||||||
|
@ -268,6 +306,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{/* Mods can ban from community, and appoint as mods to community */}
|
{/* Mods can ban from community, and appoint as mods to community */}
|
||||||
{this.canMod && (
|
{this.canMod && (
|
||||||
|
@ -299,17 +338,43 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
{!node.comment.banned_from_community && (
|
{!node.comment.banned_from_community && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
{!this.state.showConfirmAppointAsMod ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
this.handleAddModToCommunity
|
this.handleShowConfirmAppointAsMod
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{this.isMod
|
{this.isMod
|
||||||
? i18n.t('remove_as_mod')
|
? i18n.t('remove_as_mod')
|
||||||
: i18n.t('appoint_as_mod')}
|
: i18n.t('appoint_as_mod')}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleAddModToCommunity
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelConfirmAppointAsMod
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -381,14 +446,40 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
{!node.comment.banned && (
|
{!node.comment.banned && (
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
|
{!this.state.showConfirmAppointAsAdmin ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleAddAdmin)}
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleShowConfirmAppointAsAdmin
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{this.isAdmin
|
{this.isAdmin
|
||||||
? i18n.t('remove_as_admin')
|
? i18n.t('remove_as_admin')
|
||||||
: i18n.t('appoint_as_admin')}
|
: i18n.t('appoint_as_admin')}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span class="d-inline-block mr-1">
|
||||||
|
<T i18nKey="are_you_sure">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block mr-1"
|
||||||
|
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||||
|
>
|
||||||
|
<T i18nKey="yes">#</T>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="pointer d-inline-block"
|
||||||
|
onClick={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleCancelConfirmAppointAsAdmin
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<T i18nKey="no">#</T>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -432,34 +523,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<li className="list-inline-item">
|
|
||||||
<span
|
|
||||||
className="pointer"
|
|
||||||
onClick={linkEvent(this, this.handleViewSource)}
|
|
||||||
>
|
|
||||||
<T i18nKey="view_source">#</T>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<Link
|
|
||||||
className="text-muted"
|
|
||||||
to={`/post/${node.comment.post_id}/comment/${node.comment.id}`}
|
|
||||||
>
|
|
||||||
<T i18nKey="link">#</T>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{this.props.markable && (
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<span
|
|
||||||
class="pointer"
|
|
||||||
onClick={linkEvent(this, this.handleMarkRead)}
|
|
||||||
>
|
|
||||||
{node.comment.read
|
|
||||||
? i18n.t('mark_as_unread')
|
|
||||||
: i18n.t('mark_as_read')}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -725,13 +788,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModBanFromCommunityShow(i: CommentNode) {
|
handleModBanFromCommunityShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = !i.state.showBanDialog;
|
||||||
i.state.banType = BanType.Community;
|
i.state.banType = BanType.Community;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModBanShow(i: CommentNode) {
|
handleModBanShow(i: CommentNode) {
|
||||||
i.state.showBanDialog = true;
|
i.state.showBanDialog = !i.state.showBanDialog;
|
||||||
i.state.banType = BanType.Site;
|
i.state.banType = BanType.Site;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
@ -784,6 +847,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleShowConfirmAppointAsMod(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsMod = true;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelConfirmAppointAsMod(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsMod = false;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
handleAddModToCommunity(i: CommentNode) {
|
handleAddModToCommunity(i: CommentNode) {
|
||||||
let form: AddModToCommunityForm = {
|
let form: AddModToCommunityForm = {
|
||||||
user_id: i.props.node.comment.creator_id,
|
user_id: i.props.node.comment.creator_id,
|
||||||
|
@ -791,6 +864,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
added: !i.isMod,
|
added: !i.isMod,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.addModToCommunity(form);
|
WebSocketService.Instance.addModToCommunity(form);
|
||||||
|
i.state.showConfirmAppointAsMod = false;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowConfirmAppointAsAdmin(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsAdmin = true;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelConfirmAppointAsAdmin(i: CommentNode) {
|
||||||
|
i.state.showConfirmAppointAsAdmin = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -800,6 +884,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
added: !i.isAdmin,
|
added: !i.isAdmin,
|
||||||
};
|
};
|
||||||
WebSocketService.Instance.addAdmin(form);
|
WebSocketService.Instance.addAdmin(form);
|
||||||
|
i.state.showConfirmAppointAsAdmin = false;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
ui/src/components/footer.tsx
vendored
4
ui/src/components/footer.tsx
vendored
|
@ -23,8 +23,8 @@ export class Footer extends Component<any, any> {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>
|
<a class="nav-link" href={'/docs/index.html'}>
|
||||||
<T i18nKey="api">#</T>
|
<T i18nKey="docs">#</T>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|
38
ui/src/components/post-form.tsx
vendored
38
ui/src/components/post-form.tsx
vendored
|
@ -74,6 +74,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
|
||||||
|
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
|
|
||||||
|
@ -350,9 +352,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
|
|
||||||
handlePostUrlChange(i: PostForm, event: any) {
|
handlePostUrlChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.url = event.target.value;
|
i.state.postForm.url = event.target.value;
|
||||||
if (validURL(i.state.postForm.url)) {
|
i.setState(i.state);
|
||||||
|
i.fetchPageTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPageTitle() {
|
||||||
|
if (validURL(this.state.postForm.url)) {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: i.state.postForm.url,
|
q: this.state.postForm.url,
|
||||||
type_: SearchType[SearchType.Url],
|
type_: SearchType[SearchType.Url],
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -362,36 +369,39 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
|
|
||||||
// Fetch the page title
|
// Fetch the page title
|
||||||
getPageTitle(i.state.postForm.url).then(d => {
|
getPageTitle(this.state.postForm.url).then(d => {
|
||||||
i.state.suggestedTitle = d;
|
this.state.suggestedTitle = d;
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
i.state.suggestedTitle = undefined;
|
this.state.suggestedTitle = undefined;
|
||||||
i.state.crossPosts = [];
|
this.state.crossPosts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
i.setState(i.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostNameChange(i: PostForm, event: any) {
|
handlePostNameChange(i: PostForm, event: any) {
|
||||||
i.state.postForm.name = event.target.value;
|
i.state.postForm.name = event.target.value;
|
||||||
|
i.setState(i.state);
|
||||||
|
i.fetchSimilarPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchSimilarPosts() {
|
||||||
let form: SearchForm = {
|
let form: SearchForm = {
|
||||||
q: i.state.postForm.name,
|
q: this.state.postForm.name,
|
||||||
type_: SearchType[SearchType.Posts],
|
type_: SearchType[SearchType.Posts],
|
||||||
sort: SortType[SortType.TopAll],
|
sort: SortType[SortType.TopAll],
|
||||||
community_id: i.state.postForm.community_id,
|
community_id: this.state.postForm.community_id,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (i.state.postForm.name !== '') {
|
if (this.state.postForm.name !== '') {
|
||||||
WebSocketService.Instance.search(form);
|
WebSocketService.Instance.search(form);
|
||||||
} else {
|
} else {
|
||||||
i.state.suggestedPosts = [];
|
this.state.suggestedPosts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
i.setState(i.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePostBodyChange(i: PostForm, event: any) {
|
handlePostBodyChange(i: PostForm, event: any) {
|
||||||
|
|
133
ui/src/components/user.tsx
vendored
133
ui/src/components/user.tsx
vendored
|
@ -99,7 +99,6 @@ export class User extends Component<any, UserState> {
|
||||||
default_sort_type: null,
|
default_sort_type: null,
|
||||||
default_listing_type: null,
|
default_listing_type: null,
|
||||||
lang: null,
|
lang: null,
|
||||||
avatar: null,
|
|
||||||
auth: null,
|
auth: null,
|
||||||
},
|
},
|
||||||
userSettingsLoading: null,
|
userSettingsLoading: null,
|
||||||
|
@ -437,7 +436,6 @@ export class User extends Component<any, UserState> {
|
||||||
</h5>
|
</h5>
|
||||||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<label>
|
<label>
|
||||||
<T i18nKey="avatar">#</T>
|
<T i18nKey="avatar">#</T>
|
||||||
</label>
|
</label>
|
||||||
|
@ -468,18 +466,13 @@ export class User extends Component<any, UserState> {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<label>
|
<label>
|
||||||
<T i18nKey="language">#</T>
|
<T i18nKey="language">#</T>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={this.state.userSettingsForm.lang}
|
value={this.state.userSettingsForm.lang}
|
||||||
onChange={linkEvent(
|
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
|
||||||
this,
|
|
||||||
this.handleUserSettingsLangChange
|
|
||||||
)}
|
|
||||||
class="ml-2 custom-select custom-select-sm w-auto"
|
class="ml-2 custom-select custom-select-sm w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>
|
<option disabled>
|
||||||
|
@ -494,18 +487,13 @@ export class User extends Component<any, UserState> {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<label>
|
<label>
|
||||||
<T i18nKey="theme">#</T>
|
<T i18nKey="theme">#</T>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={this.state.userSettingsForm.theme}
|
value={this.state.userSettingsForm.theme}
|
||||||
onChange={linkEvent(
|
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
|
||||||
this,
|
|
||||||
this.handleUserSettingsThemeChange
|
|
||||||
)}
|
|
||||||
class="ml-2 custom-select custom-select-sm w-auto"
|
class="ml-2 custom-select custom-select-sm w-auto"
|
||||||
>
|
>
|
||||||
<option disabled>
|
<option disabled>
|
||||||
|
@ -516,9 +504,7 @@ export class User extends Component<any, UserState> {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<form className="form-group">
|
<form className="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<label>
|
<label>
|
||||||
<T i18nKey="sort_type" class="mr-2">
|
<T i18nKey="sort_type" class="mr-2">
|
||||||
#
|
#
|
||||||
|
@ -528,10 +514,8 @@ export class User extends Component<any, UserState> {
|
||||||
type_={this.state.userSettingsForm.default_listing_type}
|
type_={this.state.userSettingsForm.default_listing_type}
|
||||||
onChange={this.handleUserSettingsListingTypeChange}
|
onChange={this.handleUserSettingsListingTypeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<form className="form-group">
|
<form className="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<label>
|
<label>
|
||||||
<T i18nKey="type" class="mr-2">
|
<T i18nKey="type" class="mr-2">
|
||||||
#
|
#
|
||||||
|
@ -541,11 +525,75 @@ export class User extends Component<any, UserState> {
|
||||||
sort={this.state.userSettingsForm.default_sort_type}
|
sort={this.state.userSettingsForm.default_sort_type}
|
||||||
onChange={this.handleUserSettingsSortTypeChange}
|
onChange={this.handleUserSettingsSortTypeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-lg-3 col-form-label">
|
||||||
|
<T i18nKey="email">#</T>
|
||||||
|
</label>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={i18n.t('optional')}
|
||||||
|
value={this.state.userSettingsForm.email}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsEmailChange
|
||||||
|
)}
|
||||||
|
minLength={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-lg-5 col-form-label">
|
||||||
|
<T i18nKey="new_password">#</T>
|
||||||
|
</label>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.userSettingsForm.new_password}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsNewPasswordChange
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-lg-5 col-form-label">
|
||||||
|
<T i18nKey="verify_password">#</T>
|
||||||
|
</label>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.userSettingsForm.new_password_verify}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsNewPasswordVerifyChange
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-lg-5 col-form-label">
|
||||||
|
<T i18nKey="old_password">#</T>
|
||||||
|
</label>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
value={this.state.userSettingsForm.old_password}
|
||||||
|
onInput={linkEvent(
|
||||||
|
this,
|
||||||
|
this.handleUserSettingsOldPasswordChange
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{WebSocketService.Instance.site.enable_nsfw && (
|
{WebSocketService.Instance.site.enable_nsfw && (
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
|
@ -561,14 +609,9 @@ export class User extends Component<any, UserState> {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-12">
|
<button type="submit" class="btn btn-block btn-secondary mr-4">
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-block btn-secondary mr-4"
|
|
||||||
>
|
|
||||||
{this.state.userSettingsLoading ? (
|
{this.state.userSettingsLoading ? (
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
<use xlinkHref="#icon-spinner"></use>
|
<use xlinkHref="#icon-spinner"></use>
|
||||||
|
@ -578,10 +621,8 @@ export class User extends Component<any, UserState> {
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<hr />
|
<hr />
|
||||||
<div class="form-group mb-0">
|
<div class="form-group mb-0">
|
||||||
<div class="col-12">
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-block btn-danger"
|
class="btn btn-block btn-danger"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
|
@ -630,7 +671,6 @@ export class User extends Component<any, UserState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -786,6 +826,38 @@ export class User extends Component<any, UserState> {
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleUserSettingsEmailChange(i: User, event: any) {
|
||||||
|
i.state.userSettingsForm.email = event.target.value;
|
||||||
|
if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
|
||||||
|
i.state.userSettingsForm.email = undefined;
|
||||||
|
}
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUserSettingsNewPasswordChange(i: User, event: any) {
|
||||||
|
i.state.userSettingsForm.new_password = event.target.value;
|
||||||
|
if (i.state.userSettingsForm.new_password == '') {
|
||||||
|
i.state.userSettingsForm.new_password = undefined;
|
||||||
|
}
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
|
||||||
|
i.state.userSettingsForm.new_password_verify = event.target.value;
|
||||||
|
if (i.state.userSettingsForm.new_password_verify == '') {
|
||||||
|
i.state.userSettingsForm.new_password_verify = undefined;
|
||||||
|
}
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUserSettingsOldPasswordChange(i: User, event: any) {
|
||||||
|
i.state.userSettingsForm.old_password = event.target.value;
|
||||||
|
if (i.state.userSettingsForm.old_password == '') {
|
||||||
|
i.state.userSettingsForm.old_password = undefined;
|
||||||
|
}
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
|
||||||
handleImageUpload(i: User, event: any) {
|
handleImageUpload(i: User, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
|
@ -856,6 +928,8 @@ export class User extends Component<any, UserState> {
|
||||||
if (msg.error) {
|
if (msg.error) {
|
||||||
alert(i18n.t(msg.error));
|
alert(i18n.t(msg.error));
|
||||||
this.state.deleteAccountLoading = false;
|
this.state.deleteAccountLoading = false;
|
||||||
|
this.state.avatarLoading = false;
|
||||||
|
this.state.userSettingsLoading = false;
|
||||||
if (msg.error == 'couldnt_find_that_username_or_email') {
|
if (msg.error == 'couldnt_find_that_username_or_email') {
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
}
|
}
|
||||||
|
@ -882,6 +956,7 @@ export class User extends Component<any, UserState> {
|
||||||
UserService.Instance.user.default_listing_type;
|
UserService.Instance.user.default_listing_type;
|
||||||
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
|
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
|
||||||
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
|
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
|
||||||
|
this.state.userSettingsForm.email = this.state.user.email;
|
||||||
}
|
}
|
||||||
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|
5
ui/src/interfaces.ts
vendored
5
ui/src/interfaces.ts
vendored
|
@ -87,6 +87,7 @@ export interface UserView {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
fedi_name: string;
|
fedi_name: string;
|
||||||
published: string;
|
published: string;
|
||||||
number_of_posts: number;
|
number_of_posts: number;
|
||||||
|
@ -481,6 +482,10 @@ export interface UserSettingsForm {
|
||||||
default_listing_type: ListingType;
|
default_listing_type: ListingType;
|
||||||
lang: string;
|
lang: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
email?: string;
|
||||||
|
new_password?: string;
|
||||||
|
new_password_verify?: string;
|
||||||
|
old_password?: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
ui/src/translations/en.ts
vendored
2
ui/src/translations/en.ts
vendored
|
@ -98,6 +98,7 @@ export const en = {
|
||||||
all: 'All',
|
all: 'All',
|
||||||
top: 'Top',
|
top: 'Top',
|
||||||
api: 'API',
|
api: 'API',
|
||||||
|
docs: 'Docs',
|
||||||
inbox: 'Inbox',
|
inbox: 'Inbox',
|
||||||
inbox_for: 'Inbox for <1>{{user}}</1>',
|
inbox_for: 'Inbox for <1>{{user}}</1>',
|
||||||
mark_all_as_read: 'mark all as read',
|
mark_all_as_read: 'mark all as read',
|
||||||
|
@ -118,6 +119,7 @@ export const en = {
|
||||||
unread_messages: 'Unread Messages',
|
unread_messages: 'Unread Messages',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
verify_password: 'Verify Password',
|
verify_password: 'Verify Password',
|
||||||
|
old_password: 'Old Password',
|
||||||
forgot_password: 'forgot password',
|
forgot_password: 'forgot password',
|
||||||
reset_password_mail_sent: 'Sent an Email to reset your password.',
|
reset_password_mail_sent: 'Sent an Email to reset your password.',
|
||||||
password_change: 'Password Change',
|
password_change: 'Password Change',
|
||||||
|
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
||||||
export let version: string = 'v0.5.9';
|
export let version: string = 'v0.5.14';
|
||||||
|
|
Loading…
Reference in a new issue