mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-26 06:11:26 +00:00
Merge branch 'master' into federation
This commit is contained in:
commit
e09a035373
57 changed files with 1245 additions and 929 deletions
1
.dockerignore
vendored
1
.dockerignore
vendored
|
@ -1,5 +1,4 @@
|
|||
ui/node_modules
|
||||
ui/dist
|
||||
server/target
|
||||
docs
|
||||
.git
|
||||
|
|
3
.travis.yml
vendored
3
.travis.yml
vendored
|
@ -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
149
README.md
vendored
|
@ -9,7 +9,7 @@
|
|||
|
||||
[![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy)
|
||||
[![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy)
|
||||
![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)
|
||||
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
|
||||
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||
|
@ -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:
|
||||
|
||||
|
|
9
docker/dev/Dockerfile
vendored
9
docker/dev/Dockerfile
vendored
|
@ -32,6 +32,14 @@ RUN cargo build --frozen --release
|
|||
# Get diesel-cli on there just in case
|
||||
# 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
20
docker/dev/deploy.sh
vendored
|
@ -5,12 +5,14 @@ git checkout master
|
|||
new_tag="$1"
|
||||
git tag $new_tag
|
||||
|
||||
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||
|
||||
# Setting the version on the front end
|
||||
cd ../../
|
||||
echo "export let version: string = '$(git describe --tags)';" > "ui/src/version.ts"
|
||||
git add "ui/src/version.ts"
|
||||
# Setting the version on the backend
|
||||
echo "pub const VERSION: &'static str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||
echo "pub const VERSION: &str = \"$(git describe --tags)\";" > "server/src/version.rs"
|
||||
git add "server/src/version.rs"
|
||||
|
||||
cd docker/dev
|
||||
|
@ -38,14 +40,22 @@ docker push dessalines/lemmy:x64-$new_tag
|
|||
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||
|
||||
# aarch64
|
||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||
docker push dessalines/lemmy:arm64-$new_tag
|
||||
# Only do this on major releases (IE the third semver is 0)
|
||||
if [ $third_semver -eq 0 ]; then
|
||||
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||
docker push dessalines/lemmy:arm64-$new_tag
|
||||
fi
|
||||
|
||||
# Creating the manifest for the multi-arch build
|
||||
docker manifest create dessalines/lemmy:$new_tag \
|
||||
if [ $third_semver -eq 0 ]; then
|
||||
docker manifest create dessalines/lemmy:$new_tag \
|
||||
dessalines/lemmy:x64-$new_tag \
|
||||
dessalines/lemmy:arm64-$new_tag
|
||||
else
|
||||
docker manifest create dessalines/lemmy:$new_tag \
|
||||
dessalines/lemmy:x64-$new_tag
|
||||
fi
|
||||
|
||||
docker manifest push dessalines/lemmy:$new_tag
|
||||
|
||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -11,7 +11,7 @@ services:
|
|||
- lemmy_db:/var/lib/postgresql/data
|
||||
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
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
book
|
6
docs/book.toml
vendored
Normal file
6
docs/book.toml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[book]
|
||||
authors = ["Felix Ableitner"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Lemmy Documentation"
|
16
docs/src/SUMMARY.md
vendored
Normal file
16
docs/src/SUMMARY.md
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Summary
|
||||
|
||||
- [About](about.md)
|
||||
- [Features](about_features.md)
|
||||
- [Goals](about_goals.md)
|
||||
- [Post and Comment Ranking](about_ranking.md)
|
||||
- [Administration](administration.md)
|
||||
- [Install with Docker](administration_install_docker.md)
|
||||
- [Install with Ansible](administration_install_ansible.md)
|
||||
- [Install with Kubernetes](administration_install_kubernetes.md)
|
||||
- [Configuration](administration_configuration.md)
|
||||
- [Contributing](contributing.md)
|
||||
- [Docker Development](contributing_docker_development.md)
|
||||
- [Local Development](contributing_local_development.md)
|
||||
- [Websocket API](contributing_websocket_api.md)
|
||||
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
20
docs/src/about.md
vendored
Normal file
20
docs/src/about.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Lemmy - A link aggregator / reddit clone for the fediverse.
|
||||
|
||||
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only*
|
||||
|
||||
[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||
|
||||
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||
|
||||
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||
|
||||
### Why's it called Lemmy?
|
||||
|
||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||
|
||||
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/).
|
27
docs/src/about_features.md
vendored
Normal file
27
docs/src/about_features.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Features
|
||||
- Open source, [AGPL License](/LICENSE).
|
||||
- Self hostable, easy to deploy.
|
||||
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||
- Clean, mobile-friendly interface.
|
||||
- Live-updating Comment threads.
|
||||
- Full vote scores `(+/-)` like old reddit.
|
||||
- Themes, including light, dark, and solarized.
|
||||
- Emojis with autocomplete support. Start typing `:`
|
||||
- User tagging using `@`, Community tagging using `#`.
|
||||
- Notifications, on comment replies and when you're tagged.
|
||||
- i18n / internationalization support.
|
||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||
- Cross-posting support.
|
||||
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||
- Moderation abilities.
|
||||
- Public Moderation Logs.
|
||||
- Both site admins, and community moderators, who can appoint other moderators.
|
||||
- Can lock, remove, and restore posts and comments.
|
||||
- Can ban and unban users from communities and the site.
|
||||
- Can transfer site and communities to others.
|
||||
- Can fully erase your data, replacing all posts and comments.
|
||||
- NSFW post / community support.
|
||||
- High performance.
|
||||
- Server is written in rust.
|
||||
- Front end is `~80kB` gzipped.
|
||||
- Supports arm64 / Raspberry Pi.
|
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/goals.md → docs/src/about_goals.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
0
docs/ranking.md → docs/src/about_ranking.md
vendored
1
docs/src/administration.md
vendored
Normal file
1
docs/src/administration.md
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Information for Lemmy instance admins, and those who want to start an instance.
|
6
docs/src/administration_configuration.md
vendored
Normal file
6
docs/src/administration_configuration.md
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file.
|
||||
|
||||
Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with
|
||||
`LEMMY__DATABASE__POOL_SIZE=10`.
|
||||
|
||||
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once.
|
11
docs/src/administration_install_ansible.md
vendored
Normal file
11
docs/src/administration_install_ansible.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
|
||||
|
||||
Then run the following commands on your local computer:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dessalines/lemmy.git
|
||||
cd lemmy/ansible/
|
||||
cp inventory.example inventory
|
||||
nano inventory # enter your server, domain, contact email
|
||||
ansible-playbook lemmy.yml --become
|
||||
```
|
28
docs/src/administration_install_docker.md
vendored
Normal file
28
docs/src/administration_install_docker.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
||||
|
||||
```bash
|
||||
mkdir lemmy/
|
||||
cd lemmy/
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||
# Edit lemmy.hjson to do more configuration
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
and go to http://localhost:8536.
|
||||
|
||||
[A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
||||
# Replace the {{ vars }}
|
||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||
```
|
||||
#### Updating
|
||||
|
||||
To update to the newest version, run:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||
docker-compose up -d
|
||||
```
|
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
22
docs/src/administration_install_kubernetes.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/).
|
||||
Setting this up will vary depending on your provider.
|
||||
To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).
|
||||
|
||||
Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`.
|
||||
You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s.
|
||||
By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes.
|
||||
|
||||
**Important** Running a database in Kubernetes will work, but is generally not recommended.
|
||||
If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).
|
||||
|
||||
Now you can deploy:
|
||||
|
||||
```bash
|
||||
# Add `-n foo` if you want to deploy into a specific namespace `foo`;
|
||||
# otherwise your resources will be created in the `default` namespace.
|
||||
kubectl apply -f docker/k8s/db.yml
|
||||
kubectl apply -f docker/k8s/pictshare.yml
|
||||
kubectl apply -f docker/k8s/lemmy.yml
|
||||
```
|
||||
|
||||
If you used a `LoadBalancer`, you should see it in your cloud provider's console.
|
1
docs/src/contributing.md
vendored
Normal file
1
docs/src/contributing.md
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.
|
11
docs/src/contributing_docker_development.md
vendored
Normal file
11
docs/src/contributing_docker_development.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
Run:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dessalines/lemmy
|
||||
cd lemmy/docker/dev
|
||||
./docker_update.sh # This builds and runs it, updating for your changes
|
||||
```
|
||||
|
||||
and go to http://localhost:8536.
|
||||
|
||||
Note that compile times are relatively long with Docker, because builds can't be properly cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
|
24
docs/src/contributing_local_development.md
vendored
Normal file
24
docs/src/contributing_local_development.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
#### Requirements
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [Yarn](https://yarnpkg.com/en/)
|
||||
- [Postgres](https://www.postgresql.org/)
|
||||
|
||||
#### Set up Postgres DB
|
||||
|
||||
```bash
|
||||
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
```
|
||||
|
||||
#### Running
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dessalines/lemmy
|
||||
cd lemmy
|
||||
./install.sh
|
||||
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||
# cd ui && yarn start
|
||||
# cd server && cargo watch -x run
|
||||
```
|
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
-- user
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
-- user
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
email,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
|
@ -51,7 +51,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -60,6 +60,7 @@ pub fn get_remote_community(identifier: String) -> Result<GetCommunityResponse,
|
|||
deleted: false,
|
||||
nsfw: false,
|
||||
creator_name: "".to_string(),
|
||||
creator_avatar: None,
|
||||
category_name: "".to_string(),
|
||||
number_of_subscribers: -1,
|
||||
number_of_posts: -1,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -189,12 +189,9 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
|
||||
let mut query = self.query;
|
||||
|
||||
match self.listing_type {
|
||||
ListingType::Subscribed => {
|
||||
if let ListingType::Subscribed = self.listing_type {
|
||||
query = query.filter(subscribed.eq(true));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
query = match self.sort {
|
||||
SortType::Hot => query
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
)?)
|
||||
}
|
||||
|
|
38
server/src/routes/federation.rs
Normal file
38
server/src/routes/federation.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::api::community::ListCommunities;
|
||||
use crate::api::Perform;
|
||||
use crate::api::{Oper, UserOperation};
|
||||
use crate::apub;
|
||||
use crate::settings::Settings;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
if Settings::get().federation_enabled {
|
||||
println!("federation enabled, host is {}", Settings::get().hostname);
|
||||
cfg
|
||||
.route(
|
||||
"/federation/c/{community_name}",
|
||||
web::get().to(apub::community::get_apub_community),
|
||||
)
|
||||
.route(
|
||||
"/federation/c/{community_name}/followers",
|
||||
web::get().to(apub::community::get_apub_community_followers),
|
||||
)
|
||||
.route(
|
||||
"/federation/u/{user_name}",
|
||||
web::get().to(apub::user::get_apub_user),
|
||||
)
|
||||
// TODO: this is a very quick and dirty implementation for http api calls
|
||||
.route(
|
||||
"/api/v1/communities/list",
|
||||
web::get().to(|query: Query<ListCommunities>| {
|
||||
let res = Oper::new(UserOperation::ListCommunities, query.into_inner())
|
||||
.perform()
|
||||
.unwrap();
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(serde_json::to_string(&res).unwrap())
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,12 +5,13 @@ use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
|
|||
use crate::db::community::Community;
|
||||
use crate::db::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;
|
||||
|
45
server/src/routes/index.rs
Normal file
45
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use crate::settings::Settings;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::web;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
.route("/", web::get().to(index))
|
||||
.route(
|
||||
"/home/type/{type}/sort/{sort}/page/{page}",
|
||||
web::get().to(index),
|
||||
)
|
||||
.route("/login", web::get().to(index))
|
||||
.route("/create_post", web::get().to(index))
|
||||
.route("/create_community", web::get().to(index))
|
||||
.route("/communities/page/{page}", web::get().to(index))
|
||||
.route("/communities", web::get().to(index))
|
||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||
.route("/post/{id}", web::get().to(index))
|
||||
.route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
|
||||
.route("/c/{name}", web::get().to(index))
|
||||
.route("/community/{id}", web::get().to(index))
|
||||
.route(
|
||||
"/u/{username}/view/{view}/sort/{sort}/page/{page}",
|
||||
web::get().to(index),
|
||||
)
|
||||
.route("/u/{username}", web::get().to(index))
|
||||
.route("/user/{id}", web::get().to(index))
|
||||
.route("/inbox", web::get().to(index))
|
||||
.route("/modlog/community/{community_id}", web::get().to(index))
|
||||
.route("/modlog", web::get().to(index))
|
||||
.route("/setup", web::get().to(index))
|
||||
.route(
|
||||
"/search/q/{q}/type/{type}/sort/{sort}/page/{page}",
|
||||
web::get().to(index),
|
||||
)
|
||||
.route("/search", web::get().to(index))
|
||||
.route("/sponsors", web::get().to(index))
|
||||
.route("/password_change/{token}", web::get().to(index));
|
||||
}
|
||||
|
||||
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
||||
Ok(NamedFile::open(
|
||||
Settings::get().front_end_dir.to_owned() + "/index.html",
|
||||
)?)
|
||||
}
|
6
server/src/routes/mod.rs
Normal file
6
server/src/routes/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod federation;
|
||||
pub mod feeds;
|
||||
pub mod index;
|
||||
pub mod nodeinfo;
|
||||
pub mod webfinger;
|
||||
pub mod websocket;
|
|
@ -3,9 +3,16 @@ use crate::db::site_view::SiteView;
|
|||
use crate::version;
|
||||
use crate::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())
|
||||
}
|
|
@ -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));
|
179
server/src/routes/websocket.rs
Normal file
179
server/src/routes/websocket.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use crate::websocket::server::*;
|
||||
use actix::prelude::*;
|
||||
use actix_web::web;
|
||||
use actix_web::*;
|
||||
use actix_web_actors::ws;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
// Start chat server actor in separate thread
|
||||
let server = ChatServer::default().start();
|
||||
cfg
|
||||
.data(server)
|
||||
.service(web::resource("/api/v1/ws").to(chat_route));
|
||||
}
|
||||
|
||||
/// How often heartbeat pings are sent
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
/// How long before lack of client response causes a timeout
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Entry point for our route
|
||||
fn chat_route(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
chat_server: web::Data<Addr<ChatServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
WSSession {
|
||||
cs_addr: chat_server.get_ref().to_owned(),
|
||||
id: 0,
|
||||
hb: Instant::now(),
|
||||
ip: req
|
||||
.connection_info()
|
||||
.remote()
|
||||
.unwrap_or("127.0.0.1:12345")
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or("127.0.0.1")
|
||||
.to_string(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
}
|
||||
|
||||
struct WSSession {
|
||||
cs_addr: Addr<ChatServer>,
|
||||
/// unique session id
|
||||
id: usize,
|
||||
ip: String,
|
||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||
/// otherwise we drop connection.
|
||||
hb: Instant,
|
||||
}
|
||||
|
||||
impl Actor for WSSession {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
/// Method is called on actor start.
|
||||
/// We register ws session with ChatServer
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// we'll start heartbeat process on session start.
|
||||
self.hb(ctx);
|
||||
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// before processing any other events.
|
||||
// across all routes within application
|
||||
let addr = ctx.address();
|
||||
self
|
||||
.cs_addr
|
||||
.send(Connect {
|
||||
addr: addr.recipient(),
|
||||
ip: self.ip.to_owned(),
|
||||
})
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(res) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
fut::ok(())
|
||||
})
|
||||
.wait(ctx);
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
||||
// notify chat server
|
||||
self.cs_addr.do_send(Disconnect {
|
||||
id: self.id,
|
||||
ip: self.ip.to_owned(),
|
||||
});
|
||||
Running::Stop
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle messages from chat server, we simply send it to peer websocket
|
||||
/// These are room messages, IE sent to others in the room
|
||||
impl Handler<WSMessage> for WSSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
|
||||
// println!("id: {} msg: {}", self.id, msg.0);
|
||||
ctx.text(msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket message handler
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg);
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(text) => {
|
||||
let m = text.trim().to_owned();
|
||||
println!("WEBSOCKET MESSAGE: {:?} from id: {}", &m, self.id);
|
||||
|
||||
self
|
||||
.cs_addr
|
||||
.send(StandardMessage {
|
||||
id: self.id,
|
||||
msg: m,
|
||||
})
|
||||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(res) => ctx.text(res),
|
||||
Err(e) => {
|
||||
eprintln!("{}", &e);
|
||||
}
|
||||
}
|
||||
fut::ok(())
|
||||
})
|
||||
.wait(ctx);
|
||||
}
|
||||
ws::Message::Binary(_bin) => println!("Unexpected binary"),
|
||||
ws::Message::Close(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WSSession {
|
||||
/// helper method that sends ping to client every second.
|
||||
///
|
||||
/// also this method checks heartbeats from client
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
// heartbeat timed out
|
||||
println!("Websocket Client heartbeat failed, disconnecting!");
|
||||
|
||||
// notify chat server
|
||||
act.cs_addr.do_send(Disconnect {
|
||||
id: act.id,
|
||||
ip: act.ip.to_owned(),
|
||||
});
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping("");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -51,10 +51,10 @@ pub struct Database {
|
|||
|
||||
lazy_static! {
|
||||
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.
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &'static str = "v0.5.9";
|
||||
pub const VERSION: &str = "v0.5.14";
|
||||
|
|
|
@ -92,7 +92,7 @@ impl Default for ChatServer {
|
|||
ChatServer {
|
||||
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 {
|
||||
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 => {
|
||||
|
|
151
ui/src/components/comment-node.tsx
vendored
151
ui/src/components/comment-node.tsx
vendored
|
@ -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,13 +262,35 @@ 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">•</li>
|
||||
<li className="list-inline-item">
|
||||
{!node.comment.removed ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleModRemoveShow
|
||||
)}
|
||||
>
|
||||
<T i18nKey="remove">#</T>
|
||||
</span>
|
||||
|
@ -268,6 +306,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</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">
|
||||
{!this.state.showConfirmAppointAsMod ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(
|
||||
this,
|
||||
this.handleAddModToCommunity
|
||||
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">
|
||||
{!this.state.showConfirmAppointAsAdmin ? (
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleAddAdmin)}
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
4
ui/src/components/footer.tsx
vendored
4
ui/src/components/footer.tsx
vendored
|
@ -23,8 +23,8 @@ export class Footer extends Component<any, any> {
|
|||
</Link>
|
||||
</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">
|
||||
|
|
38
ui/src/components/post-form.tsx
vendored
38
ui/src/components/post-form.tsx
vendored
|
@ -74,6 +74,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
|
||||
constructor(props: any, context: any) {
|
||||
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) {
|
||||
|
|
133
ui/src/components/user.tsx
vendored
133
ui/src/components/user.tsx
vendored
|
@ -99,7 +99,6 @@ export class User extends Component<any, UserState> {
|
|||
default_sort_type: null,
|
||||
default_listing_type: null,
|
||||
lang: null,
|
||||
avatar: null,
|
||||
auth: null,
|
||||
},
|
||||
userSettingsLoading: null,
|
||||
|
@ -437,7 +436,6 @@ 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>
|
||||
|
@ -468,18 +466,13 @@ export class User extends Component<any, UserState> {
|
|||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-12">
|
||||
<label>
|
||||
<T i18nKey="language">#</T>
|
||||
</label>
|
||||
<select
|
||||
value={this.state.userSettingsForm.lang}
|
||||
onChange={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsLangChange
|
||||
)}
|
||||
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
|
||||
class="ml-2 custom-select custom-select-sm w-auto"
|
||||
>
|
||||
<option disabled>
|
||||
|
@ -494,18 +487,13 @@ export class User extends Component<any, UserState> {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-12">
|
||||
<label>
|
||||
<T i18nKey="theme">#</T>
|
||||
</label>
|
||||
<select
|
||||
value={this.state.userSettingsForm.theme}
|
||||
onChange={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsThemeChange
|
||||
)}
|
||||
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
|
||||
class="ml-2 custom-select custom-select-sm w-auto"
|
||||
>
|
||||
<option disabled>
|
||||
|
@ -516,9 +504,7 @@ export class User extends Component<any, UserState> {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<form className="form-group">
|
||||
<div class="col-12">
|
||||
<label>
|
||||
<T i18nKey="sort_type" class="mr-2">
|
||||
#
|
||||
|
@ -528,10 +514,8 @@ export class User extends Component<any, UserState> {
|
|||
type_={this.state.userSettingsForm.default_listing_type}
|
||||
onChange={this.handleUserSettingsListingTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<form className="form-group">
|
||||
<div class="col-12">
|
||||
<label>
|
||||
<T i18nKey="type" class="mr-2">
|
||||
#
|
||||
|
@ -541,11 +525,75 @@ export class User extends Component<any, UserState> {
|
|||
sort={this.state.userSettingsForm.default_sort_type}
|
||||
onChange={this.handleUserSettingsSortTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label">
|
||||
<T i18nKey="email">#</T>
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
placeholder={i18n.t('optional')}
|
||||
value={this.state.userSettingsForm.email}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsEmailChange
|
||||
)}
|
||||
minLength={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<T i18nKey="new_password">#</T>
|
||||
</label>
|
||||
<div class="col-lg-7">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
value={this.state.userSettingsForm.new_password}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsNewPasswordChange
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<T i18nKey="verify_password">#</T>
|
||||
</label>
|
||||
<div class="col-lg-7">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
value={this.state.userSettingsForm.new_password_verify}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsNewPasswordVerifyChange
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<T i18nKey="old_password">#</T>
|
||||
</label>
|
||||
<div class="col-lg-7">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
value={this.state.userSettingsForm.old_password}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsOldPasswordChange
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{WebSocketService.Instance.site.enable_nsfw && (
|
||||
<div class="form-group">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
|
@ -561,14 +609,9 @@ export class User extends Component<any, UserState> {
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group">
|
||||
<div class="col-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-block btn-secondary mr-4"
|
||||
>
|
||||
<button type="submit" class="btn btn-block btn-secondary mr-4">
|
||||
{this.state.userSettingsLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
|
@ -578,10 +621,8 @@ export class User extends Component<any, UserState> {
|
|||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-12">
|
||||
<button
|
||||
class="btn btn-block btn-danger"
|
||||
onClick={linkEvent(
|
||||
|
@ -630,7 +671,6 @@ export class User extends Component<any, UserState> {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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);
|
||||
|
|
5
ui/src/interfaces.ts
vendored
5
ui/src/interfaces.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
2
ui/src/translations/en.ts
vendored
2
ui/src/translations/en.ts
vendored
|
@ -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
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
|||
export let version: string = 'v0.5.9';
|
||||
export let version: string = 'v0.5.14';
|
||||
|
|
Loading…
Reference in a new issue