Merge branch 'master' into federation

This commit is contained in:
Felix Ableitner 2020-01-02 19:22:23 +01:00
commit 1c1f3d1316
57 changed files with 1245 additions and 929 deletions

1
.dockerignore vendored
View file

@ -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
View file

@ -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
View file

@ -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:

View file

@ -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

12
docker/dev/deploy.sh vendored
View file

@ -5,12 +5,14 @@ git checkout master
new_tag="$1" new_tag="$1"
git tag $new_tag git tag $new_tag
third_semver=$(echo $new_tag | cut -d "." -f 3)
# Setting the version on the front end # Setting the version on the front end
cd ../../ cd ../../
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts" echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
git add "ui/src/version.ts" git add "ui/src/version.ts"
# Setting the version on the backend # Setting the version on the backend
echo "pub const VERSION: &'static str = \"$(git describe --tags)\";" > "server/src/version.rs" echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
git add "server/src/version.rs" git add "server/src/version.rs"
cd docker/dev cd docker/dev
@ -38,14 +40,22 @@ docker push dessalines/lemmy:x64-$new_tag
# docker push dessalines/lemmy:armv7hf-$new_tag # docker push dessalines/lemmy:armv7hf-$new_tag
# aarch64 # aarch64
# Only do this on major releases (IE the third semver is 0)
if [ $third_semver -eq 0 ]; then
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../ docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
docker push dessalines/lemmy:arm64-$new_tag docker push dessalines/lemmy:arm64-$new_tag
fi
# Creating the manifest for the multi-arch build # Creating the manifest for the multi-arch build
if [ $third_semver -eq 0 ]; then
docker manifest create dessalines/lemmy:$new_tag \ docker manifest create dessalines/lemmy:$new_tag \
dessalines/lemmy:x64-$new_tag \ dessalines/lemmy:x64-$new_tag \
dessalines/lemmy:arm64-$new_tag dessalines/lemmy:arm64-$new_tag
else
docker manifest create dessalines/lemmy:$new_tag \
dessalines/lemmy:x64-$new_tag
fi
docker manifest push dessalines/lemmy:$new_tag docker manifest push dessalines/lemmy:$new_tag

View file

@ -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
View file

@ -0,0 +1 @@
book

6
docs/book.toml vendored Normal file
View 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
View 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
View 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
View 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.

1
docs/src/administration.md vendored Normal file
View file

@ -0,0 +1 @@
Information for Lemmy instance admins, and those who want to start an instance.

View 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.

View 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
```

View 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
```

View 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
View file

@ -0,0 +1 @@
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.

View 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).

View 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
```

View 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;

View 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;

View file

@ -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()),
}; };
} }

View file

@ -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

View file

@ -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()),
}; };
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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,

View file

@ -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
} }

View file

@ -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)]

View file

@ -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

View file

@ -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> {

View file

@ -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,

View file

@ -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;

View file

@ -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",
)?)
}

View 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())
}),
);
}
}

View file

@ -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;

View 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
View file

@ -0,0 +1,6 @@
pub mod federation;
pub mod feeds;
pub mod index;
pub mod nodeinfo;
pub mod webfinger;
pub mod websocket;

View file

@ -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())
} }

View file

@ -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));

View 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("");
});
}
}

View file

@ -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.

View file

@ -1 +1 @@
pub const VERSION: &'static str = "v0.5.9"; pub const VERSION: &str = "v0.5.14";

View file

@ -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 => {

View file

@ -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);
} }

View file

@ -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">

View file

@ -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) {

View file

@ -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);

View file

@ -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;
} }

View file

@ -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
View file

@ -1 +1 @@
export let version: string = 'v0.5.9'; export let version: string = 'v0.5.14';