Merge branch 'master' into federation

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

1
.dockerignore vendored
View file

@ -1,5 +1,4 @@
ui/node_modules
ui/dist
server/target
docs
.git

3
.travis.yml vendored
View file

@ -13,9 +13,12 @@ before_cache:
before_script:
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
- psql -c 'create database lemmy with owner lemmy;' -U postgres
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
before_install:
- cd server
script:
# Default checks, but fail if anything is detected
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
- cargo build
- diesel migration run
- 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)
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
@ -36,31 +36,17 @@ Front Page|Post
---|---
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
## 📝 Table of Contents
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
<!-- toc -->
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
- [Features](#features)
- [About](#about)
* [Why's it called Lemmy?](#whys-it-called-lemmy)
- [Install](#install)
* [Docker](#docker)
+ [Updating](#updating)
* [Ansible](#ansible)
* [Kubernetes](#kubernetes)
- [Develop](#develop)
* [Docker Development](#docker-development)
* [Local Development](#local-development)
+ [Requirements](#requirements)
+ [Set up Postgres DB](#set-up-postgres-db)
+ [Running](#running)
- [Configuration](#configuration)
- [Documentation](#documentation)
- [Support](#support)
- [Translations](#translations)
- [Credits](#credits)
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
<!-- tocstop -->
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
[Documentation](https://dev.lemmy.ml/docs/index.html)
## Features
@ -91,25 +77,13 @@ Front Page|Post
- Front end is `~80kB` gzipped.
- Supports arm64 / Raspberry Pi.
## About
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
### Why's it called Lemmy?
## Why's it called Lemmy?
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
## Install
### Docker
@ -121,7 +95,7 @@ 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 the .env if you want custom passwords
# Edit lemmy.hjson to do more configuration
docker-compose up -d
```
@ -157,88 +131,6 @@ nano inventory # enter your server, domain, contact email
ansible-playbook lemmy.yml --become
```
### Kubernetes
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
Setting this up will vary depending on your provider.
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
**Important** Running a database in Kubernetes will work, but is generally not recommended.
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
Now you can deploy:
```bash
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
# otherwise your resources will be created in the `default` namespace.
kubectl apply -f docker/k8s/db.yml
kubectl apply -f docker/k8s/pictshare.yml
kubectl apply -f docker/k8s/lemmy.yml
```
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
## Develop
### Docker Development
Run:
```bash
git clone https://github.com/dessalines/lemmy
cd lemmy/docker/dev
./docker_update.sh # This builds and runs it, updating for your changes
```
and go to http://localhost:8536.
### Local Development
#### Requirements
- [Rust](https://www.rust-lang.org/)
- [Yarn](https://yarnpkg.com/en/)
- [Postgres](https://www.postgresql.org/)
#### Set up Postgres DB
```bash
psql -c "create user lemmy with password 'password' superuser;" -U postgres
psql -c 'create database lemmy with owner lemmy;' -U postgres
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
```
#### Running
```bash
git clone https://github.com/dessalines/lemmy
cd lemmy
./install.sh
# For live coding, where both the front and back end, automagically reload on any save, do:
# cd ui && yarn start
# cd server && cargo watch -x run
```
## 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
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
--- | --- | ---
de | 97% | avatar,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
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
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
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
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
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
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
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
de | 96% | avatar,docs,old_password,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
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 | 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 | 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 | 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 | 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 | 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 | 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 | 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:

View file

@ -32,6 +32,14 @@ RUN cargo build --frozen --release
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
FROM ekidd/rust-musl-builder:1.38.0-openssl11 as docs
WORKDIR /app
COPY docs ./docs
RUN sudo chown -R rust:rust .
RUN mdbook build docs/
FROM alpine:3.10
# Install libpq for postgres
@ -40,6 +48,7 @@ RUN apk add libpq
# Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
COPY --from=docs /app/docs/book/ /app/dist/documentation/
COPY --from=node /app/ui/dist /app/dist
RUN addgroup -g 1000 lemmy

20
docker/dev/deploy.sh vendored
View file

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

View file

@ -11,7 +11,7 @@ services:
- lemmy_db:/var/lib/postgresql/data
restart: always
lemmy:
image: dessalines/lemmy:v0.5.9
image: dessalines/lemmy:v0.5.14
ports:
- "127.0.0.1:8536:8536"
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) {
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;
@ -59,12 +59,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban"))?;
return Err(APIError::err(&self.op, "community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
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) {
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
let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames {
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
if mention_user.is_ok() {
let mention_user_id = mention_user?.id;
@ -124,7 +124,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
};
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) {
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;
@ -163,17 +163,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) {
return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?;
return Err(APIError::err(&self.op, "no_comment_edit_allowed").into());
}
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban"))?;
return Err(APIError::err(&self.op, "community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&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) {
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
let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames {
let mention_user = User_::read_from_name(&conn, username_mention.to_string());
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
if mention_user.is_ok() {
let mention_user_id = mention_user?.id;
@ -255,7 +255,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -268,12 +268,12 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
if data.save {
match CommentSaved::save(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
};
} else {
match CommentSaved::unsave(&conn, &comment_saved_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
};
}
@ -293,7 +293,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
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
if data.score == -1 {
let site = SiteView::read(&conn)?;
if site.enable_downvotes == false {
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
if !site.enable_downvotes {
return Err(APIError::err(&self.op, "downvotes_disabled").into());
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban"))?;
return Err(APIError::err(&self.op, "community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
let like_form = CommentLikeForm {
@ -332,7 +332,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
if do_add {
let _inserted_like = match CommentLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
};
}

View file

@ -136,21 +136,24 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
let community_id = match data.id {
Some(id) => id,
None => {
match Community::read_from_name(&conn, data.name.to_owned().unwrap_or("main".to_string())) {
match Community::read_from_name(
&conn,
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
) {
Ok(community) => community.id,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
}
}
};
let community_view = match CommunityView::read(&conn, community_id, user_id) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
};
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
};
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) {
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)
|| has_slurs(&data.title)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs"))?;
return Err(APIError::err(&self.op, "no_slurs").into());
}
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
// 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) {
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 {
@ -220,10 +223,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"community_moderator_already_exists",
))?
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
};
@ -235,7 +235,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
let _inserted_community_follower =
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
};
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;
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 claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
// 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());
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 {
@ -296,7 +296,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
};
// Mod tables
@ -351,7 +351,7 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
let communities = CommunityQueryBuilder::create(&conn)
.sort(&sort)
.from_user_id(user_id)
.for_user(user_id)
.show_nsfw(show_nsfw)
.page(data.page)
.limit(data.limit)
@ -372,7 +372,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -385,12 +385,12 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
};
} else {
match CommunityFollower::ignore(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
};
}
@ -410,7 +410,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -418,7 +418,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
let communities: Vec<CommunityFollowerView> =
match CommunityFollowerView::for_user(&conn, user_id) {
Ok(communities) => communities,
Err(_e) => return Err(APIError::err(&self.op, "system_err_login"))?,
Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()),
};
// Return the jwt
@ -436,7 +436,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -449,12 +449,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
if data.ban {
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
};
} else {
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
};
}
@ -491,7 +491,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -505,20 +505,14 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"community_moderator_already_exists",
))?
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
};
} else {
match CommunityModerator::leave(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"community_moderator_already_exists",
))?
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
};
}
@ -548,7 +542,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -562,14 +556,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
admins.insert(0, creator_user);
// Make sure user is the creator, or an admin
if user_id != read_community.creator_id
&& !admins
.iter()
.map(|a| a.id)
.collect::<Vec<i32>>()
.contains(&user_id)
{
return Err(APIError::err(&self.op, "not_an_admin"))?;
if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
return Err(APIError::err(&self.op, "not_an_admin").into());
}
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) {
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.
@ -610,10 +598,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
match CommunityModerator::join(&conn, &community_moderator_form) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"community_moderator_already_exists",
))?
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
};
}
@ -629,12 +614,12 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
Ok(community) => community,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
};
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
};
// Return the jwt

View file

@ -93,23 +93,23 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "no_slurs"))?;
return Err(APIError::err(&self.op, "no_slurs").into());
}
let user_id = claims.id;
// Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban"))?;
return Err(APIError::err(&self.op, "community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
let post_form = PostForm {
@ -128,7 +128,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let inserted_post = match Post::create(&conn, &post_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()),
};
// 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
let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
};
// Refetch the view
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
};
Ok(PostResponse {
@ -175,7 +175,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
let post_view = match PostView::read(&conn, data.id, user_id) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
};
let comments = CommentQueryBuilder::create(&conn)
@ -243,7 +243,7 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
.list()
{
Ok(posts) => posts,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()),
};
Ok(GetPostsResponse {
@ -260,7 +260,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
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
if data.score == -1 {
let site = SiteView::read(&conn)?;
if site.enable_downvotes == false {
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
if !site.enable_downvotes {
return Err(APIError::err(&self.op, "downvotes_disabled").into());
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err(&self.op, "community_ban"))?;
return Err(APIError::err(&self.op, "community_ban").into());
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
return Err(APIError::err(&self.op, "site_ban"))?;
return Err(APIError::err(&self.op, "site_ban").into());
}
let like_form = PostLikeForm {
@ -294,17 +294,17 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
PostLike::remove(&conn, &like_form)?;
// Only add the like if the score isnt 0
let do_add = &like_form.score != &0 && (&like_form.score == &1 || &like_form.score == &-1);
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
};
}
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
};
// just output the score
@ -319,14 +319,14 @@ impl Perform<PostResponse> for Oper<EditPost> {
fn perform(&self) -> Result<PostResponse, Error> {
let data: &EditPost = &self.data;
if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
return Err(APIError::err(&self.op, "no_slurs"))?;
return Err(APIError::err(&self.op, "no_slurs").into());
}
let conn = establish_connection();
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
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());
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
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
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 {
@ -370,7 +370,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
};
// Mod tables
@ -418,7 +418,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -431,12 +431,12 @@ impl Perform<PostResponse> for Oper<SavePost> {
if data.save {
match PostSaved::save(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
};
} else {
match PostSaved::unsave(&conn, &post_saved_form) {
Ok(post) => post,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post"))?,
Err(_e) => return Err(APIError::err(&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
let mut removed_communities = Vec::new();
let mut banned = Vec::new();
let mut added = Vec::new();
if data.community_id.is_none() {
removed_communities =
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?;
banned = ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?;
added = ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?;
}
let (removed_communities, banned, added) = if data.community_id.is_none() {
(
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
)
} else {
(Vec::new(), Vec::new(), Vec::new())
};
// Return the jwt
Ok(GetModlogResponse {
@ -194,20 +193,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
if has_slurs(&data.name)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs"))?;
return Err(APIError::err(&self.op, "no_slurs").into());
}
let user_id = claims.id;
// Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin"))?;
return Err(APIError::err(&self.op, "not_an_admin").into());
}
let site_form = SiteForm {
@ -222,7 +221,7 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
match Site::create(&conn, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()),
};
let site_view = SiteView::read(&conn)?;
@ -241,20 +240,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
if has_slurs(&data.name)
|| (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
{
return Err(APIError::err(&self.op, "no_slurs"))?;
return Err(APIError::err(&self.op, "no_slurs").into());
}
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "not_an_admin"))?;
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
}
let found_site = Site::read(&conn, 1)?;
@ -271,7 +270,7 @@ impl Perform<SiteResponse> for Oper<EditSite> {
match Site::update(&conn, 1, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
};
let site_view = SiteView::read(&conn)?;
@ -426,7 +425,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -435,7 +434,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
// Make sure user is the creator
if read_site.creator_id != user_id {
return Err(APIError::err(&self.op, "not_an_admin"))?;
return Err(APIError::err(&self.op, "not_an_admin").into());
}
let site_form = SiteForm {
@ -450,7 +449,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
match Site::update(&conn, 1, &site_form) {
Ok(site) => site,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
};
// Mod tables

View file

@ -28,6 +28,10 @@ pub struct SaveUserSettings {
default_listing_type: i16,
lang: String,
avatar: Option<String>,
email: Option<String>,
new_password: Option<String>,
new_password_verify: Option<String>,
old_password: Option<String>,
auth: String,
}
@ -168,18 +172,13 @@ impl Perform<LoginResponse> for Oper<Login> {
// Fetch that username / email
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"couldnt_find_that_username_or_email",
))?
}
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
};
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect"))?;
return Err(APIError::err(&self.op, "password_incorrect").into());
}
// Return the jwt
@ -198,22 +197,22 @@ impl Perform<LoginResponse> for Oper<Register> {
// Make sure site has open registration
if let Ok(site) = SiteView::read(&conn) {
if !site.open_registration {
return Err(APIError::err(&self.op, "registration_closed"))?;
return Err(APIError::err(&self.op, "registration_closed").into());
}
}
// Make sure passwords match
if &data.password != &data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
if data.password != data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match").into());
}
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
if data.admin && UserView::admins(&conn)?.len() > 0 {
return Err(APIError::err(&self.op, "admin_already_created"))?;
if data.admin && !UserView::admins(&conn)?.is_empty() {
return Err(APIError::err(&self.op, "admin_already_created").into());
}
// Register the new user
@ -237,7 +236,7 @@ impl Perform<LoginResponse> for Oper<Register> {
// Create the user
let inserted_user = match User_::register(&conn, &user_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "user_already_exists").into()),
};
// Create the main community if it doesn't exist
@ -268,7 +267,7 @@ impl Perform<LoginResponse> for Oper<Register> {
let _inserted_community_follower =
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?,
Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
};
// 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) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"community_moderator_already_exists",
))?
return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
}
};
}
@ -305,19 +301,52 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
let read_user = User_::read(&conn, user_id)?;
let email = match &data.email {
Some(email) => Some(email.to_owned()),
None => read_user.email,
};
let password_encrypted = match &data.new_password {
Some(new_password) => {
match &data.new_password_verify {
Some(new_password_verify) => {
// Make sure passwords match
if new_password != new_password_verify {
return Err(APIError::err(&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 {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
email,
avatar: data.avatar.to_owned(),
password_encrypted: read_user.password_encrypted,
password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
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) {
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
@ -372,14 +401,14 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
None => {
match User_::read_from_name(
&conn,
data.username.to_owned().unwrap_or("admin".to_string()),
data
.username
.to_owned()
.unwrap_or_else(|| "admin".to_string()),
) {
Ok(user) => user.id,
Err(_e) => {
return Err(APIError::err(
&self.op,
"couldnt_find_that_username_or_email",
))?
return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into())
}
}
}
@ -441,14 +470,14 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "not_an_admin"))?;
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
}
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) {
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
@ -504,14 +533,14 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
// Make sure user is an admin
if UserView::read(&conn, user_id)?.admin == false {
return Err(APIError::err(&self.op, "not_an_admin"))?;
if !UserView::read(&conn, user_id)?.admin {
return Err(APIError::err(&self.op, "not_an_admin").into());
}
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) {
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
@ -571,7 +600,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -599,7 +628,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -627,7 +656,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -643,7 +672,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
let _updated_user_mention =
match UserMention::update(&conn, user_mention.id, &user_mention_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
};
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) {
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;
@ -687,7 +716,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
};
}
@ -708,7 +737,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
let _updated_mention =
match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
Ok(mention) => mention,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
};
}
@ -726,7 +755,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?,
Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
};
let user_id = claims.id;
@ -736,7 +765,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect"))?;
return Err(APIError::err(&self.op, "password_incorrect").into());
}
// Comments
@ -759,7 +788,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?,
Err(_e) => return Err(APIError::err(&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) {
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
let user: User_ = match User_::find_by_email(&conn, &data.email) {
Ok(user) => user,
Err(_e) => {
return Err(APIError::err(
&self.op,
"couldnt_find_that_username_or_email",
))?
}
Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
};
// 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);
match send_email(subject, user_email, &user.name, html) {
Ok(_o) => _o,
Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?,
Err(_e) => return Err(APIError::err(&self.op, &_e).into()),
};
Ok(PasswordResetResponse {
@ -846,34 +870,14 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
// Make sure passwords match
if &data.password != &data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
if data.password != data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match").into());
}
// Fetch the user
let read_user = User_::read(&conn, user_id)?;
// Update the user with the new password
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
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) {
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
};
// Return the jwt

View file

@ -60,10 +60,7 @@ impl Community {
let mut collection = UnorderedCollection::default();
collection.object_props.set_context_object(context()).ok();
collection
.object_props
.set_id_string(base_url.to_string())
.ok();
collection.object_props.set_id_string(base_url).ok();
let connection = establish_connection();
//As we are an object, we validated that the community id was valid

View file

@ -9,7 +9,7 @@ impl Post {
let mut page = Page::default();
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();
if let Some(body) = &self.body {

View file

@ -55,11 +55,12 @@ pub fn get_remote_community(identifier: String) -> Result<GetCommunityResponse,
category_id: -1,
creator_id: -1,
removed: false,
published: naive_now(), // TODO: community.object_props.published
published: naive_now(), // TODO: community.object_props.published
updated: Some(naive_now()), // TODO: community.object_props.updated
deleted: false,
nsfw: false,
creator_name: "".to_string(),
creator_avatar: None,
category_name: "".to_string(),
number_of_subscribers: -1,
number_of_posts: -1,

View file

@ -124,7 +124,7 @@ impl<'a> CommunityQueryBuilder<'a> {
self
}
pub fn from_user_id<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
pub fn for_user<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
self.from_user_id = from_user_id.get_optional();
self
}

View file

@ -101,13 +101,13 @@ pub trait MaybeOptional<T> {
impl<T> MaybeOptional<T> for T {
fn get_optional(self) -> Option<T> {
return Some(self);
Some(self)
}
}
impl<T> MaybeOptional<T> for Option<T> {
fn get_optional(self) -> Option<T> {
return self;
self
}
}
@ -118,12 +118,12 @@ lazy_static! {
Pool::builder()
.max_size(Settings::get().database.pool_size)
.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>> {
return PG_POOL.get().unwrap();
PG_POOL.get().unwrap()
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]

View file

@ -189,12 +189,9 @@ impl<'a> PostQueryBuilder<'a> {
let mut query = self.query;
match self.listing_type {
ListingType::Subscribed => {
query = query.filter(subscribed.eq(true));
}
_ => {}
};
if let ListingType::Subscribed = self.listing_type {
query = query.filter(subscribed.eq(true));
}
query = match self.sort {
SortType::Hot => query

View file

@ -75,14 +75,13 @@ impl User_ {
pub fn update_password(
conn: &PgConnection,
user_id: i32,
form: &UserForm,
new_password: &str,
) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
Self::update(&conn, user_id, &edited_user)
diesel::update(user_.find(user_id))
.set(password_encrypted.eq(password_hash))
.get_result::<Self>(conn)
}
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {

View file

@ -7,6 +7,7 @@ table! {
id -> Int4,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
@ -26,6 +27,7 @@ pub struct UserView {
pub id: i32,
pub name: String,
pub avatar: Option<String>,
pub email: Option<String>,
pub fedi_name: String,
pub admin: bool,
pub banned: bool,

View file

@ -25,12 +25,10 @@ pub extern crate strum;
pub mod api;
pub mod apub;
pub mod db;
pub mod feeds;
pub mod nodeinfo;
pub mod routes;
pub mod schema;
pub mod settings;
pub mod version;
pub mod webfinger;
pub mod websocket;
use crate::settings::Settings;

View file

@ -2,294 +2,40 @@ extern crate lemmy_server;
#[macro_use]
extern crate diesel_migrations;
use actix::prelude::*;
use actix_files::NamedFile;
use actix_web::web::Query;
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::feeds;
use lemmy_server::nodeinfo;
use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
use lemmy_server::settings::Settings;
use lemmy_server::webfinger;
use lemmy_server::websocket::server::*;
use std::time::{Duration, Instant};
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() {
let _ = env_logger::init();
env_logger::init();
let sys = actix::System::new("lemmy");
// Run the migrations from code
let conn = establish_connection();
embedded_migrations::run(&conn).unwrap();
// Start chat server actor in separate thread
let server = ChatServer::default().start();
let settings = Settings::get();
// Create Http server with websocket support
HttpServer::new(move || {
let app = App::new()
.data(server.clone())
// Front end routes
App::new()
.configure(federation::config)
.configure(feeds::config)
.configure(index::config)
.configure(nodeinfo::config)
.configure(webfinger::config)
.configure(websocket::config)
.service(actix_files::Files::new(
"/static",
settings.front_end_dir.to_owned(),
))
.route("/", web::get().to(index))
.route(
"/home/type/{type}/sort/{sort}/page/{page}",
web::get().to(index),
)
.route("/login", web::get().to(index))
.route("/create_post", web::get().to(index))
.route("/create_community", web::get().to(index))
.route("/communities/page/{page}", web::get().to(index))
.route("/communities", web::get().to(index))
.route("/post/{id}/comment/{id2}", web::get().to(index))
.route("/post/{id}", web::get().to(index))
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
.route("/c/{name}", web::get().to(index))
.route("/community/{id}", web::get().to(index))
.route(
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
web::get().to(index),
)
.route("/u/{username}", web::get().to(index))
.route("/user/{id}", web::get().to(index))
.route("/inbox", web::get().to(index))
.route("/modlog/community/{community_id}", web::get().to(index))
.route("/modlog", web::get().to(index))
.route("/setup", web::get().to(index))
.route(
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
web::get().to(index),
)
.route("/search", web::get().to(index))
.route("/sponsors", web::get().to(index))
.route("/password_change/{token}", web::get().to(index))
// Websocket
.service(web::resource("/api/v1/ws").to(chat_route))
// NodeInfo
.route("/nodeinfo/2.0.json", web::get().to(nodeinfo::node_info))
.route(
"/.well-known/nodeinfo",
web::get().to(nodeinfo::node_info_well_known),
)
// RSS
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
// 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
}
.service(actix_files::Files::new(
"/docs",
settings.front_end_dir.to_owned() + "/documentation",
))
})
.bind((settings.bind, settings.port))
.unwrap()
@ -299,9 +45,3 @@ fn main() {
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::post_view::{PostQueryBuilder, PostView};
use crate::db::site_view::SiteView;
use crate::db::user::User_;
use crate::db::user::{Claims, User_};
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
use crate::db::{establish_connection, ListingType, SortType};
use crate::Settings;
use actix_web::body::Body;
use actix_web::{web, HttpResponse, Result};
use chrono::{DateTime, Utc};
use failure::Error;
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::Deserialize;
@ -29,7 +30,14 @@ enum RequestType {
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) {
Ok(sort_type) => sort_type,
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) {
Ok(sort_type) => sort_type,
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> {
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)
}
@ -162,7 +173,7 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result<String, Error> {
let conn = establish_connection();
let site_view = SiteView::read(&conn)?;
let user_id = db::user::Claims::decode(&jwt)?.claims.id;
let user_id = Claims::decode(&jwt)?.claims.id;
let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Subscribed)
@ -189,7 +200,7 @@ fn get_feed_inbox(jwt: String) -> Result<String, Error> {
let conn = establish_connection();
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;

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::Settings;
use actix_web::body::Body;
use actix_web::web;
use actix_web::HttpResponse;
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> {
let json = json!({
"links": {
@ -14,12 +21,12 @@ pub fn node_info_well_known() -> HttpResponse<Body> {
}
});
return HttpResponse::Ok()
HttpResponse::Ok()
.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 site_view = match SiteView::read(&conn) {
Ok(site_view) => site_view,
@ -43,10 +50,10 @@ pub fn node_info() -> HttpResponse<Body> {
},
"localPosts": site_view.number_of_posts,
"localComments": site_view.number_of_comments,
"openRegistrations": true,
"openRegistrations": site_view.open_registration,
}
});
return HttpResponse::Ok()
HttpResponse::Ok()
.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::Settings;
use actix_web::body::Body;
use actix_web::web;
use actix_web::web::Query;
use actix_web::HttpResponse;
use regex::Regex;
@ -13,6 +14,15 @@ pub struct Params {
resource: String,
}
pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation_enabled {
cfg.route(
".well-known/webfinger",
web::get().to(get_webfinger_response),
);
}
}
lazy_static! {
static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!(
"^group:([a-z0-9_]{{3, 20}})@{}$",
@ -27,7 +37,7 @@ lazy_static! {
///
/// You can also view the webfinger response that Mastodon sends:
/// 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
.captures(&info.resource)
.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! {
static ref SETTINGS: Settings = {
return match Settings::init() {
match Settings::init() {
Ok(c) => c,
Err(e) => panic!("{}", e),
};
}
};
}
@ -77,7 +77,7 @@ impl Settings {
// https://github.com/mehcode/config-rs/issues/73
s.merge(Environment::with_prefix("LEMMY").separator("__"))?;
return s.try_into();
s.try_into()
}
/// 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 {
sessions: HashMap::new(),
rate_limits: HashMap::new(),
rooms: rooms,
rooms,
rng: rand::thread_rng(),
}
}
@ -100,8 +100,8 @@ impl Default for ChatServer {
impl ChatServer {
/// Send message to all users in the room
fn send_room_message(&self, room: &i32, message: &str, skip_id: usize) {
if let Some(sessions) = self.rooms.get(room) {
fn send_room_message(&self, room: i32, message: &str, skip_id: usize) {
if let Some(sessions) = self.rooms.get(&room) {
for id in sessions {
if *id != skip_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) {
// remove session from all rooms
for (_n, sessions) in &mut self.rooms {
for sessions in self.rooms.values_mut() {
sessions.remove(&id);
}
@ -123,12 +123,12 @@ impl ChatServer {
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(
&self,
community_id: &i32,
community_id: i32,
message: &str,
skip_id: usize,
) -> Result<(), Error> {
@ -139,12 +139,12 @@ impl ChatServer {
let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(*community_id)
.for_community_id(community_id)
.limit(9999)
.list()?;
for post in posts {
self.send_room_message(&post.id, message, skip_id);
self.send_room_message(post.id, message, skip_id);
}
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> {
if let Some(info) = self.sessions.get(&id) {
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) {
@ -195,10 +196,13 @@ impl ChatServer {
"Rate limited IP: {}, time_passed: {}, allowance: {}",
&info.ip, time_passed, rate_limit.allowance
);
Err(APIError {
op: "Rate Limit".to_string(),
message: format!("Too many requests. {} per {} seconds", rate, per),
})?
Err(
APIError {
op: "Rate Limit".to_string(),
message: format!("Too many requests. {} per {} seconds", rate, per),
}
.into(),
)
} else {
rate_limit.allowance -= 1.0;
Ok(())
@ -265,7 +269,7 @@ impl Handler<Disconnect> for ChatServer {
// remove address
if self.sessions.remove(&msg.id).is_some() {
// remove session from all rooms
for (_id, sessions) in &mut self.rooms {
for sessions in self.rooms.values_mut() {
if sessions.remove(&msg.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 op = &json["op"].as_str().ok_or(APIError {
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)?;
@ -396,7 +400,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
community_sent.community.user_id = None;
community_sent.community.subscribed = None;
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)?)
}
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 res = Oper::new(user_operation, ban_from_community).perform()?;
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)
}
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 res = Oper::new(user_operation, mod_add_to_community).perform()?;
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)
}
UserOperation::ListCategories => {
@ -459,7 +463,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
let mut post_sent = res.clone();
post_sent.post.my_vote = None;
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)?)
}
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.user_id = None;
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)?)
}
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.user_id = None;
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)?)
}
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.user_id = None;
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)?)
}
UserOperation::GetModlog => {

View file

@ -42,6 +42,8 @@ interface CommentNodeState {
banType: BanType;
showConfirmTransferSite: boolean;
showConfirmTransferCommunity: boolean;
showConfirmAppointAsMod: boolean;
showConfirmAppointAsAdmin: boolean;
collapsed: boolean;
viewSource: boolean;
}
@ -71,6 +73,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
viewSource: false,
showConfirmTransferSite: false,
showConfirmTransferCommunity: false,
showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false,
};
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">
{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 && (
<>
<li className="list-inline-item">
@ -246,28 +262,51 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</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 */}
{(this.canMod || this.canAdmin) && (
<li className="list-inline-item">
{!node.comment.removed ? (
<span
class="pointer"
onClick={linkEvent(this, this.handleModRemoveShow)}
>
<T i18nKey="remove">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModRemoveSubmit
)}
>
<T i18nKey="restore">#</T>
</span>
)}
</li>
<>
<li className="list-inline-item"></li>
<li className="list-inline-item">
{!node.comment.removed ? (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModRemoveShow
)}
>
<T i18nKey="remove">#</T>
</span>
) : (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleModRemoveSubmit
)}
>
<T i18nKey="restore">#</T>
</span>
)}
</li>
</>
)}
{/* Mods can ban from community, and appoint as mods to community */}
{this.canMod && (
@ -299,17 +338,43 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)}
{!node.comment.banned_from_community && (
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(
this,
this.handleAddModToCommunity
)}
>
{this.isMod
? i18n.t('remove_as_mod')
: i18n.t('appoint_as_mod')}
</span>
{!this.state.showConfirmAppointAsMod ? (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleShowConfirmAppointAsMod
)}
>
{this.isMod
? i18n.t('remove_as_mod')
: i18n.t('appoint_as_mod')}
</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>
)}
</>
@ -381,14 +446,40 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)}
{!node.comment.banned && (
<li className="list-inline-item">
<span
class="pointer"
onClick={linkEvent(this, this.handleAddAdmin)}
>
{this.isAdmin
? i18n.t('remove_as_admin')
: i18n.t('appoint_as_admin')}
</span>
{!this.state.showConfirmAppointAsAdmin ? (
<span
class="pointer"
onClick={linkEvent(
this,
this.handleShowConfirmAppointAsAdmin
)}
>
{this.isAdmin
? i18n.t('remove_as_admin')
: i18n.t('appoint_as_admin')}
</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>
)}
</>
@ -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>
</div>
)}
@ -725,13 +788,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleModBanFromCommunityShow(i: CommentNode) {
i.state.showBanDialog = true;
i.state.showBanDialog = !i.state.showBanDialog;
i.state.banType = BanType.Community;
i.setState(i.state);
}
handleModBanShow(i: CommentNode) {
i.state.showBanDialog = true;
i.state.showBanDialog = !i.state.showBanDialog;
i.state.banType = BanType.Site;
i.setState(i.state);
}
@ -784,6 +847,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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) {
let form: AddModToCommunityForm = {
user_id: i.props.node.comment.creator_id,
@ -791,6 +864,17 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
added: !i.isMod,
};
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);
}
@ -800,6 +884,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
added: !i.isAdmin,
};
WebSocketService.Instance.addAdmin(form);
i.state.showConfirmAppointAsAdmin = false;
i.setState(i.state);
}

View file

@ -23,8 +23,8 @@ export class Footer extends Component<any, any> {
</Link>
</li>
<li class="nav-item">
<a class="nav-link" href={`${repoUrl}/blob/master/docs/api.md`}>
<T i18nKey="api">#</T>
<a class="nav-link" href={'/docs/index.html'}>
<T i18nKey="docs">#</T>
</a>
</li>
<li class="nav-item">

View file

@ -74,6 +74,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
constructor(props: any, context: any) {
super(props, context);
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
this.state = this.emptyState;
@ -350,9 +352,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePostUrlChange(i: PostForm, event: any) {
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 = {
q: i.state.postForm.url,
q: this.state.postForm.url,
type_: SearchType[SearchType.Url],
sort: SortType[SortType.TopAll],
page: 1,
@ -362,36 +369,39 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
WebSocketService.Instance.search(form);
// Fetch the page title
getPageTitle(i.state.postForm.url).then(d => {
i.state.suggestedTitle = d;
i.setState(i.state);
getPageTitle(this.state.postForm.url).then(d => {
this.state.suggestedTitle = d;
this.setState(this.state);
});
} else {
i.state.suggestedTitle = undefined;
i.state.crossPosts = [];
this.state.suggestedTitle = undefined;
this.state.crossPosts = [];
}
i.setState(i.state);
}
handlePostNameChange(i: PostForm, event: any) {
i.state.postForm.name = event.target.value;
i.setState(i.state);
i.fetchSimilarPosts();
}
fetchSimilarPosts() {
let form: SearchForm = {
q: i.state.postForm.name,
q: this.state.postForm.name,
type_: SearchType[SearchType.Posts],
sort: SortType[SortType.TopAll],
community_id: i.state.postForm.community_id,
community_id: this.state.postForm.community_id,
page: 1,
limit: 6,
};
if (i.state.postForm.name !== '') {
if (this.state.postForm.name !== '') {
WebSocketService.Instance.search(form);
} else {
i.state.suggestedPosts = [];
this.state.suggestedPosts = [];
}
i.setState(i.state);
this.setState(this.state);
}
handlePostBodyChange(i: PostForm, event: any) {

View file

@ -99,7 +99,6 @@ export class User extends Component<any, UserState> {
default_sort_type: null,
default_listing_type: null,
lang: null,
avatar: null,
auth: null,
},
userSettingsLoading: null,
@ -437,199 +436,240 @@ export class User extends Component<any, UserState> {
</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group">
<div class="col-12">
<label>
<T i18nKey="avatar">#</T>
</label>
<form class="d-inline">
<label
htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold"
>
<img
height="80"
width="80"
src={
this.state.userSettingsForm.avatar
? this.state.userSettingsForm.avatar
: 'https://via.placeholder.com/300/000?text=Avatar'
}
class="rounded-circle"
/>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
<label>
<T i18nKey="avatar">#</T>
</label>
<form class="d-inline">
<label
htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold"
>
<img
height="80"
width="80"
src={
this.state.userSettingsForm.avatar
? this.state.userSettingsForm.avatar
: 'https://via.placeholder.com/300/000?text=Avatar'
}
class="rounded-circle"
/>
</form>
</div>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
</div>
<div class="form-group">
<div class="col-12">
<label>
<label>
<T i18nKey="language">#</T>
</label>
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="language">#</T>
</label>
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(
this,
this.handleUserSettingsLangChange
)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="language">#</T>
</option>
<option value="browser">
<T i18nKey="browser_default">#</T>
</option>
<option disabled></option>
{languages.map(lang => (
<option value={lang.code}>{lang.name}</option>
))}
</select>
</div>
</option>
<option value="browser">
<T i18nKey="browser_default">#</T>
</option>
<option disabled></option>
{languages.map(lang => (
<option value={lang.code}>{lang.name}</option>
))}
</select>
</div>
<div class="form-group">
<div class="col-12">
<label>
<label>
<T i18nKey="theme">#</T>
</label>
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="theme">#</T>
</label>
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(
this,
this.handleUserSettingsThemeChange
)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="theme">#</T>
</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</div>
</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</div>
<form className="form-group">
<div class="col-12">
<label>
<T i18nKey="sort_type" class="mr-2">
#
</T>
</label>
<ListingTypeSelect
type_={this.state.userSettingsForm.default_listing_type}
onChange={this.handleUserSettingsListingTypeChange}
/>
</div>
<label>
<T i18nKey="sort_type" class="mr-2">
#
</T>
</label>
<ListingTypeSelect
type_={this.state.userSettingsForm.default_listing_type}
onChange={this.handleUserSettingsListingTypeChange}
/>
</form>
<form className="form-group">
<div class="col-12">
<label>
<T i18nKey="type" class="mr-2">
#
</T>
</label>
<SortSelect
sort={this.state.userSettingsForm.default_sort_type}
onChange={this.handleUserSettingsSortTypeChange}
<label>
<T i18nKey="type" class="mr-2">
#
</T>
</label>
<SortSelect
sort={this.state.userSettingsForm.default_sort_type}
onChange={this.handleUserSettingsSortTypeChange}
/>
</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>
</form>
</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 && (
<div class="form-group">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
</div>
)}
<div class="form-group">
<div class="col-12">
<button
type="submit"
class="btn btn-block btn-secondary mr-4"
>
{this.state.userSettingsLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
<button type="submit" class="btn btn-block btn-secondary mr-4">
{this.state.userSettingsLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
<hr />
<div class="form-group mb-0">
<div class="col-12">
<button
class="btn btn-block btn-danger"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="delete_account">#</T>
</button>
{this.state.deleteAccountShowConfirm && (
<>
<div class="my-2 alert alert-danger" role="alert">
<T i18nKey="delete_account_confirm">#</T>
</div>
<input
type="password"
value={this.state.deleteAccountForm.password}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
class="form-control my-2"
/>
<button
class="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('delete'))
)}
</button>
<button
class="btn btn-secondary"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="cancel">#</T>
</button>
</>
<button
class="btn btn-block btn-danger"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
</div>
>
<T i18nKey="delete_account">#</T>
</button>
{this.state.deleteAccountShowConfirm && (
<>
<div class="my-2 alert alert-danger" role="alert">
<T i18nKey="delete_account_confirm">#</T>
</div>
<input
type="password"
value={this.state.deleteAccountForm.password}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
class="form-control my-2"
/>
<button
class="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('delete'))
)}
</button>
<button
class="btn btn-secondary"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="cancel">#</T>
</button>
</>
)}
</div>
</form>
</div>
@ -786,6 +826,38 @@ export class User extends Component<any, UserState> {
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) {
event.preventDefault();
let file = event.target.files[0];
@ -856,6 +928,8 @@ export class User extends Component<any, UserState> {
if (msg.error) {
alert(i18n.t(msg.error));
this.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
@ -882,6 +956,7 @@ export class User extends Component<any, UserState> {
UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
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}`;
window.scrollTo(0, 0);

View file

@ -87,6 +87,7 @@ export interface UserView {
id: number;
name: string;
avatar?: string;
email?: string;
fedi_name: string;
published: string;
number_of_posts: number;
@ -481,6 +482,10 @@ export interface UserSettingsForm {
default_listing_type: ListingType;
lang: string;
avatar?: string;
email?: string;
new_password?: string;
new_password_verify?: string;
old_password?: string;
auth: string;
}

View file

@ -98,6 +98,7 @@ export const en = {
all: 'All',
top: 'Top',
api: 'API',
docs: 'Docs',
inbox: 'Inbox',
inbox_for: 'Inbox for <1>{{user}}</1>',
mark_all_as_read: 'mark all as read',
@ -118,6 +119,7 @@ export const en = {
unread_messages: 'Unread Messages',
password: 'Password',
verify_password: 'Verify Password',
old_password: 'Old Password',
forgot_password: 'forgot password',
reset_password_mail_sent: 'Sent an Email to reset your password.',
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';