Compare commits
74 commits
master
...
cache-cont
Author | SHA1 | Date | |
---|---|---|---|
cb928b037e | |||
02bcbc42d6 | |||
66c95993dc | |||
|
8079b6faef | ||
|
fe264a2f30 | ||
|
2cb57b833d | ||
|
588010ea88 | ||
|
2d95db8a7d | ||
|
bd99f4994a | ||
|
813b053b5f | ||
|
0f09171d68 | ||
|
7b492cc477 | ||
|
912871d0ac | ||
|
07d7664a38 | ||
|
02cf67de4a | ||
|
4157bf9a02 | ||
|
1180b89268 | ||
|
d22bbafebb | ||
|
e3d4f9418e | ||
|
beb63aedc8 | ||
|
e339f90737 | ||
|
8c1316aa96 | ||
|
ec146a0dea | ||
|
c939c15530 | ||
c01b40c517 | |||
ddd4baf103 | |||
|
a998bfc1f5 | ||
|
3c6eb37a1b | ||
|
2512babff1 | ||
|
f71d19729a | ||
|
04da8146ba | ||
|
b63aabfdc2 | ||
4b6bba0e7b | |||
|
a95704d5fc | ||
|
dbd1d8faa5 | ||
|
868ba5b64c | ||
49de4ccbd9 | |||
af83ec951f | |||
|
b365dd2349 | ||
|
9e20ddbfa4 | ||
|
22904e1c66 | ||
f7156bdac3 | |||
|
b6c297766b | ||
|
a6bc0edc91 | ||
|
ddb512b1ae | ||
|
88fed73ea3 | ||
|
51c8735682 | ||
|
94c4504b33 | ||
|
c06d01f753 | ||
|
daf22a12d9 | ||
|
dc1fc1e04c | ||
|
18d4b3d2aa | ||
|
3a85515bd5 | ||
|
b4b8e9d7f5 | ||
|
807dd8d82c | ||
|
c31fe3857c | ||
|
14418c5a0d | ||
|
0799ae1a1f | ||
|
8a9f1dbb59 | ||
|
a42e2af203 | ||
|
8bf5d0cca6 | ||
|
6b68d54e35 | ||
|
7d291ee95a | ||
|
6248392992 | ||
f18ebed740 | |||
10da3f2554 | |||
8fb34843aa | |||
a882fbea97 | |||
ae3fccf701 | |||
f7333705dc | |||
140eff181c | |||
2c26cc26b8 | |||
bad4868a10 | |||
|
844a97a6a5 |
110 changed files with 4967 additions and 3655 deletions
1
.dockerignore
vendored
1
.dockerignore
vendored
|
@ -1,5 +1,4 @@
|
|||
ui/node_modules
|
||||
ui/dist
|
||||
server/target
|
||||
docs
|
||||
.git
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
ansible/inventory
|
||||
ansible/passwords/
|
||||
docker/lemmy_mine.hjson
|
||||
build/
|
||||
.idea/
|
||||
|
|
22
.travis.yml
vendored
22
.travis.yml
vendored
|
@ -5,21 +5,27 @@ matrix:
|
|||
allow_failures:
|
||||
- rust: nightly
|
||||
fast_finish: true
|
||||
cache:
|
||||
directories:
|
||||
- /home/travis/.cargo
|
||||
cache: cargo
|
||||
before_cache:
|
||||
- rm -rf /home/travis/.cargo/registry
|
||||
- rm -rfv target/debug/incremental/lemmy_server-*
|
||||
- rm -rfv target/debug/.fingerprint/lemmy_server-*
|
||||
- rm -rfv target/debug/build/lemmy_server-*
|
||||
- rm -rfv target/debug/deps/lemmy_server-*
|
||||
- rm -rfv target/debug/lemmy_server.d
|
||||
- cargo clean
|
||||
before_script:
|
||||
- psql -c "create user rrr with password 'rrr' superuser;" -U postgres
|
||||
- psql -c 'create database rrr with owner rrr;' -U postgres
|
||||
- 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:
|
||||
- diesel migration run
|
||||
# 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
|
||||
env:
|
||||
- DATABASE_URL=postgres://rrr:rrr@localhost/rrr
|
||||
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
|
141
README.md
vendored
141
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,30 +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)
|
||||
- [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
|
||||
|
||||
|
@ -90,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
|
||||
|
@ -119,8 +94,8 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
|
|||
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/prod/.env
|
||||
# Edit the .env if you want custom passwords
|
||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||
# Edit lemmy.hjson to do more configuration
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
|
@ -156,79 +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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
@ -247,16 +149,15 @@ If you'd like to add translations, take a look a look at the [English translatio
|
|||
|
||||
lang | done | missing
|
||||
--- | --- | ---
|
||||
de | 100% |
|
||||
eo | 86% | number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme,are_you_sure,yes,no
|
||||
es | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
fr | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
it | 96% | archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
nl | 88% | preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,theme
|
||||
ru | 82% | cross_posts,cross_post,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||
sv | 95% | archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default
|
||||
zh | 80% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
|
||||
|
||||
de | 95% | avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||
eo | 82% | number_of_communities,preview,upload_image,avatar,show_avatars,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,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
|
||||
es | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||
fr | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||
it | 91% | avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||
nl | 84% | preview,upload_image,avatar,show_avatars,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,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
|
||||
ru | 78% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,show_avatars,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,send_notifications_to_email,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 | 90% | avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
|
||||
zh | 76% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,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:
|
||||
|
||||
|
|
10
ansible/lemmy.yml
vendored
10
ansible/lemmy.yml
vendored
|
@ -32,21 +32,13 @@
|
|||
- name: add all template files
|
||||
template: src={{item.src}} dest={{item.dest}}
|
||||
with_items:
|
||||
- { src: 'templates/env', dest: '/lemmy/.env' }
|
||||
- { src: '../docker/prod/docker-compose.yml', dest: '/lemmy/docker-compose.yml' }
|
||||
- { src: 'templates/config.hjson', dest: '/lemmy/lemmy.hjson' }
|
||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf' }
|
||||
vars:
|
||||
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
||||
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
||||
|
||||
- name: set env file permissions
|
||||
file:
|
||||
path: "/lemmy/.env"
|
||||
state: touch
|
||||
mode: 0600
|
||||
access_time: preserve
|
||||
modification_time: preserve
|
||||
|
||||
- name: enable and start docker service
|
||||
systemd:
|
||||
name: docker
|
||||
|
|
15
ansible/templates/config.hjson
vendored
Normal file
15
ansible/templates/config.hjson
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
database: {
|
||||
password: "{{ postgres_password }}"
|
||||
host: "lemmy_db"
|
||||
}
|
||||
hostname: "{{ domain }}"
|
||||
jwt_secret: "{{ jwt_password }}"
|
||||
front_end_dir: "/app/dist"
|
||||
email: {
|
||||
smtp_server: "{{ smtp_server }}"
|
||||
smtp_login: "{{ smtp_login }}"
|
||||
smtp_password: "{{ smtp_password }}"
|
||||
smtp_from_address: "{{ smtp_from_address }}"
|
||||
}
|
||||
}
|
14
ansible/templates/env
vendored
14
ansible/templates/env
vendored
|
@ -1,14 +0,0 @@
|
|||
DOMAIN={{ domain }}
|
||||
DATABASE_PASSWORD={{ postgres_password }}
|
||||
DATABASE_URL=postgres://lemmy:{{ postgres_password }}@lemmy_db:5432/lemmy
|
||||
JWT_SECRET={{ jwt_password }}
|
||||
RATE_LIMIT_MESSAGE=30
|
||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
||||
RATE_LIMIT_POST=3
|
||||
RATE_LIMIT_POST_PER_SECOND=600
|
||||
RATE_LIMIT_REGISTER=3
|
||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
||||
SMTP_SERVER={{ smtp_server }}
|
||||
SMTP_LOGIN={{ smtp_login }}
|
||||
SMTP_PASSWORD={{ smtp_password }}
|
||||
SMTP_FROM_ADDRESS={{ smtp_from_address }}
|
17
docker/dev/.env
vendored
17
docker/dev/.env
vendored
|
@ -1,17 +0,0 @@
|
|||
DOMAIN=my_domain
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
|
||||
JWT_SECRET=changeme
|
||||
|
||||
RATE_LIMIT_MESSAGE=30
|
||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
||||
RATE_LIMIT_POST=6
|
||||
RATE_LIMIT_POST_PER_SECOND=600
|
||||
RATE_LIMIT_REGISTER=3
|
||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
||||
|
||||
# Optional email fields
|
||||
SMTP_SERVER=
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>
|
11
docker/dev/Dockerfile
vendored
11
docker/dev/Dockerfile
vendored
|
@ -32,14 +32,25 @@ 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
|
||||
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
|
||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||
RUN chown lemmy:lemmy /app/lemmy
|
||||
|
|
1
docker/dev/Dockerfile.aarch64
vendored
1
docker/dev/Dockerfile.aarch64
vendored
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
|||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
COPY --from=node /app/ui/dist /app/dist
|
||||
|
||||
|
|
1
docker/dev/Dockerfile.armv7hf
vendored
1
docker/dev/Dockerfile.armv7hf
vendored
|
@ -69,6 +69,7 @@ RUN addgroup --gid 1000 lemmy
|
|||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
COPY --from=node /app/ui/dist /app/dist
|
||||
|
||||
|
|
2
docker/dev/Dockerfile.libc
vendored
2
docker/dev/Dockerfile.libc
vendored
|
@ -65,8 +65,10 @@ RUN addgroup --gid 1000 lemmy
|
|||
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||
|
||||
# Copy resources
|
||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||
COPY --from=rust /app/server/ready /app/lemmy
|
||||
COPY --from=node /app/ui/dist /app/dist
|
||||
|
||||
RUN chown lemmy:lemmy /app/lemmy
|
||||
USER lemmy
|
||||
EXPOSE 8536
|
||||
|
|
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
|
||||
|
||||
|
|
19
docker/dev/docker-compose.yml
vendored
19
docker/dev/docker-compose.yml
vendored
|
@ -5,7 +5,7 @@ services:
|
|||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
|
@ -16,22 +16,9 @@ services:
|
|||
dockerfile: docker/dev/Dockerfile
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
environment:
|
||||
- LEMMY_FRONT_END_DIR=/app/dist
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- HOSTNAME=${DOMAIN}
|
||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
||||
- SMTP_SERVER=${SMTP_SERVER}
|
||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
||||
restart: always
|
||||
volumes:
|
||||
- ../lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
lemmy_pictshare:
|
||||
|
|
6
docker/k8s/lemmy.yml
vendored
6
docker/k8s/lemmy.yml
vendored
|
@ -14,13 +14,13 @@ spec:
|
|||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: DATABASE_URL
|
||||
- name: LEMMY_DATABASE_URL
|
||||
# example: 'postgres://lemmy:password@db:5432/lemmy'
|
||||
value: CHANGE_ME
|
||||
- name: HOSTNAME
|
||||
- name: LEMMY_HOSTNAME
|
||||
# example: 'lemmy.example.com'
|
||||
value: CHANGE_ME
|
||||
- name: JWT_SECRET
|
||||
- name: LEMMY_JWT_SECRET
|
||||
# example: 'very-super-good-secret'
|
||||
value: CHANGE_ME
|
||||
- name: LEMMY_FRONT_END_DIR
|
||||
|
|
56
docker/lemmy.hjson
vendored
Normal file
56
docker/lemmy.hjson
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
database: {
|
||||
# username to connect to postgres
|
||||
user: "lemmy"
|
||||
# password to connect to postgres
|
||||
password: "password"
|
||||
# host where postgres is running
|
||||
host: "lemmy_db"
|
||||
# port where postgres can be accessed
|
||||
port: 5432
|
||||
# name of the postgres database for lemmy
|
||||
database: "lemmy"
|
||||
# maximum number of active sql connections
|
||||
pool_size: 5
|
||||
}
|
||||
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||
hostname: "rrr"
|
||||
# address where lemmy should listen for incoming requests
|
||||
bind: "0.0.0.0"
|
||||
# port where lemmy should listen for incoming requests
|
||||
port: 8536
|
||||
# json web token for authorization between server and client
|
||||
jwt_secret: "changeme"
|
||||
# The dir for the front end
|
||||
front_end_dir: "/app/dist"
|
||||
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
|
||||
# cause problems like remote instances fetching and permanently storing bad data.
|
||||
federation_enabled: false
|
||||
# rate limits for various user actions, by user ip
|
||||
rate_limit: {
|
||||
# maximum number of messages created in interval
|
||||
message: 30
|
||||
# interval length for message limit
|
||||
message_per_second: 60
|
||||
# maximum number of posts created in interval
|
||||
post: 6
|
||||
# interval length for post limit
|
||||
post_per_second: 600
|
||||
# maximum number of registrations in interval
|
||||
register: 3
|
||||
# interval length for registration limit
|
||||
register_per_second: 3600
|
||||
}
|
||||
# # email sending configuration
|
||||
# email: {
|
||||
# # hostname of the smtp server
|
||||
# smtp_server: ""
|
||||
# # login name for smtp server
|
||||
# smtp_login: ""
|
||||
# # password to login to the smtp server
|
||||
# smtp_password: ""
|
||||
# # address to send emails from, eg "info@your-instance.com"
|
||||
# smtp_from_address: ""
|
||||
# }
|
||||
}
|
||||
|
17
docker/prod/.env
vendored
17
docker/prod/.env
vendored
|
@ -1,17 +0,0 @@
|
|||
DOMAIN=my_domain
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
|
||||
JWT_SECRET=changeme
|
||||
|
||||
RATE_LIMIT_MESSAGE=30
|
||||
RATE_LIMIT_MESSAGE_PER_SECOND=60
|
||||
RATE_LIMIT_POST=6
|
||||
RATE_LIMIT_POST_PER_SECOND=600
|
||||
RATE_LIMIT_REGISTER=3
|
||||
RATE_LIMIT_REGISTER_PER_SECOND=3600
|
||||
|
||||
# Optional email fields
|
||||
SMTP_SERVER=
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>
|
21
docker/prod/docker-compose.yml
vendored
21
docker/prod/docker-compose.yml
vendored
|
@ -5,31 +5,18 @@ services:
|
|||
image: postgres:12-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=lemmy
|
||||
volumes:
|
||||
- lemmy_db:/var/lib/postgresql/data
|
||||
restart: always
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.5.0.3
|
||||
image: dessalines/lemmy:v0.5.17
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
environment:
|
||||
- LEMMY_FRONT_END_DIR=/app/dist
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- HOSTNAME=${DOMAIN}
|
||||
- RATE_LIMIT_MESSAGE=${RATE_LIMIT_MESSAGE}
|
||||
- RATE_LIMIT_MESSAGE_PER_SECOND=${RATE_LIMIT_MESSAGE_PER_SECOND}
|
||||
- RATE_LIMIT_POST=${RATE_LIMIT_POST}
|
||||
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
|
||||
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
|
||||
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
|
||||
- SMTP_SERVER=${SMTP_SERVER}
|
||||
- SMTP_LOGIN=${SMTP_LOGIN}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
||||
restart: always
|
||||
volumes:
|
||||
- ./lemmy.hjson:/config/config.hjson:ro
|
||||
depends_on:
|
||||
- lemmy_db
|
||||
lemmy_pictshare:
|
||||
|
|
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
|
||||
```
|
2346
server/Cargo.lock
generated
vendored
2346
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
27
server/Cargo.toml
vendored
27
server/Cargo.toml
vendored
|
@ -5,28 +5,31 @@ authors = ["Dessalines <happydooby@gmail.com>"]
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono"] }
|
||||
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.14.1"
|
||||
bcrypt = "0.5.0"
|
||||
activitypub = "0.1.5"
|
||||
dotenv = "0.15.0"
|
||||
bcrypt = "0.6.1"
|
||||
activitypub = "0.2.0"
|
||||
chrono = { version = "0.4.7", features = ["serde"] }
|
||||
failure = "0.1.5"
|
||||
serde_json = { version = "1.0.40", features = ["preserve_order"]}
|
||||
serde = { version = "1.0.94", features = ["derive"] }
|
||||
actix = "0.8.3"
|
||||
actix-web = "1.0"
|
||||
actix-files = "0.1.3"
|
||||
actix-web-actors = "1.0"
|
||||
env_logger = "0.6.2"
|
||||
actix = "0.9.0"
|
||||
actix-web = "2.0.0"
|
||||
actix-files = "0.2.1"
|
||||
actix-web-actors = "2.0.0"
|
||||
actix-rt = "1.0.0"
|
||||
env_logger = "0.7.1"
|
||||
rand = "0.7.0"
|
||||
strum = "0.15.0"
|
||||
strum_macros = "0.15.0"
|
||||
strum = "0.17.1"
|
||||
strum_macros = "0.17.1"
|
||||
jsonwebtoken = "6.0.1"
|
||||
regex = "1.1.9"
|
||||
lazy_static = "1.3.0"
|
||||
lettre = "0.9.2"
|
||||
lettre_email = "0.9.2"
|
||||
rust-crypto = "^0.2"
|
||||
sha2 = "0.8.0"
|
||||
rss = "1.8.0"
|
||||
htmlescape = "0.3.1"
|
||||
config = "0.10.1"
|
||||
hjson = "0.8.2"
|
||||
|
|
7
server/clean.sh
vendored
Executable file
7
server/clean.sh
vendored
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
cargo update
|
||||
cargo fmt
|
||||
cargo check
|
||||
cargo clippy
|
||||
cargo outdated -R
|
56
server/config/defaults.hjson
vendored
Normal file
56
server/config/defaults.hjson
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
# settings related to the postgresql database
|
||||
database: {
|
||||
# username to connect to postgres
|
||||
user: "lemmy"
|
||||
# password to connect to postgres
|
||||
password: "password"
|
||||
# host where postgres is running
|
||||
host: "localhost"
|
||||
# port where postgres can be accessed
|
||||
port: 5432
|
||||
# name of the postgres database for lemmy
|
||||
database: "lemmy"
|
||||
# maximum number of active sql connections
|
||||
pool_size: 5
|
||||
}
|
||||
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||
hostname: "rrr"
|
||||
# address where lemmy should listen for incoming requests
|
||||
bind: "0.0.0.0"
|
||||
# port where lemmy should listen for incoming requests
|
||||
port: 8536
|
||||
# json web token for authorization between server and client
|
||||
jwt_secret: "changeme"
|
||||
# The dir for the front end
|
||||
front_end_dir: "../ui/dist"
|
||||
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
|
||||
# cause problems like remote instances fetching and permanently storing bad data.
|
||||
federation_enabled: false
|
||||
# rate limits for various user actions, by user ip
|
||||
rate_limit: {
|
||||
# maximum number of messages created in interval
|
||||
message: 30
|
||||
# interval length for message limit
|
||||
message_per_second: 60
|
||||
# maximum number of posts created in interval
|
||||
post: 6
|
||||
# interval length for post limit
|
||||
post_per_second: 600
|
||||
# maximum number of registrations in interval
|
||||
register: 3
|
||||
# interval length for registration limit
|
||||
register_per_second: 3600
|
||||
}
|
||||
# # email sending configuration
|
||||
# email: {
|
||||
# # hostname of the smtp server
|
||||
# smtp_server: ""
|
||||
# # login name for smtp server
|
||||
# smtp_login: ""
|
||||
# # password to login to the smtp server
|
||||
# smtp_password: ""
|
||||
# # address to send emails from, eg "info@your-instance.com"
|
||||
# smtp_from_address: ""
|
||||
# }
|
||||
}
|
224
server/migrations/2019-12-29-164820_add_avatar/down.sql
vendored
Normal file
224
server/migrations/2019-12-29-164820_add_avatar/down.sql
vendored
Normal file
|
@ -0,0 +1,224 @@
|
|||
-- the views
|
||||
drop view user_mention_view;
|
||||
drop view reply_view;
|
||||
drop view comment_view;
|
||||
drop view user_view;
|
||||
|
||||
-- user
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
-- post
|
||||
-- Recreate the view
|
||||
drop view post_view;
|
||||
create view post_view as
|
||||
with all_post as
|
||||
(
|
||||
select
|
||||
p.*,
|
||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||
(select name from community where p.community_id = community.id) as community_name,
|
||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||
coalesce(sum(pl.score), 0) as score,
|
||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||
from post p
|
||||
left join post_like pl on p.id = pl.post_id
|
||||
group by p.id
|
||||
)
|
||||
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
||||
-- community
|
||||
|
||||
drop view community_view;
|
||||
create view community_view as
|
||||
with all_community as
|
||||
(
|
||||
select *,
|
||||
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
||||
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
||||
from community c
|
||||
)
|
||||
|
||||
select
|
||||
ac.*,
|
||||
u.id as user_id,
|
||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||
from user_ u
|
||||
cross join all_community ac
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ac.*,
|
||||
null as user_id,
|
||||
null as subscribed
|
||||
from all_community ac
|
||||
;
|
||||
|
||||
-- Reply and comment view
|
||||
create view comment_view as
|
||||
with all_comment as
|
||||
(
|
||||
select
|
||||
c.*,
|
||||
(select community_id from post p where p.id = c.post_id),
|
||||
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||
coalesce(sum(cl.score), 0) as score,
|
||||
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||
from comment c
|
||||
left join comment_like cl on c.id = cl.comment_id
|
||||
group by c.id
|
||||
)
|
||||
|
||||
select
|
||||
ac.*,
|
||||
u.id as user_id,
|
||||
coalesce(cl.score, 0) as my_vote,
|
||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||
from user_ u
|
||||
cross join all_comment ac
|
||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ac.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as saved
|
||||
from all_comment ac
|
||||
;
|
||||
|
||||
create view reply_view as
|
||||
with closereply as (
|
||||
select
|
||||
c2.id,
|
||||
c2.creator_id as sender_id,
|
||||
c.creator_id as recipient_id
|
||||
from comment c
|
||||
inner join comment c2 on c.id = c2.parent_id
|
||||
where c2.creator_id != c.creator_id
|
||||
-- Do union where post is null
|
||||
union
|
||||
select
|
||||
c.id,
|
||||
c.creator_id as sender_id,
|
||||
p.creator_id as recipient_id
|
||||
from comment c, post p
|
||||
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||
)
|
||||
select cv.*,
|
||||
closereply.recipient_id
|
||||
from comment_view cv, closereply
|
||||
where closereply.id = cv.id
|
||||
;
|
||||
|
||||
-- user mention
|
||||
create view user_mention_view as
|
||||
select
|
||||
c.id,
|
||||
um.id as user_mention_id,
|
||||
c.creator_id,
|
||||
c.post_id,
|
||||
c.parent_id,
|
||||
c.content,
|
||||
c.removed,
|
||||
um.read,
|
||||
c.published,
|
||||
c.updated,
|
||||
c.deleted,
|
||||
c.community_id,
|
||||
c.banned,
|
||||
c.banned_from_community,
|
||||
c.creator_name,
|
||||
c.score,
|
||||
c.upvotes,
|
||||
c.downvotes,
|
||||
c.user_id,
|
||||
c.my_vote,
|
||||
c.saved,
|
||||
um.recipient_id
|
||||
from user_mention um, comment_view c
|
||||
where um.comment_id = c.id;
|
||||
|
||||
-- community tables
|
||||
drop view community_moderator_view;
|
||||
drop view community_follower_view;
|
||||
drop view community_user_ban_view;
|
||||
drop view site_view;
|
||||
|
||||
create view community_moderator_view as
|
||||
select *,
|
||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||
(select name from community c where cm.community_id = c.id) as community_name
|
||||
from community_moderator cm;
|
||||
|
||||
create view community_follower_view as
|
||||
select *,
|
||||
(select name from user_ u where cf.user_id = u.id) as user_name,
|
||||
(select name from community c where cf.community_id = c.id) as community_name
|
||||
from community_follower cf;
|
||||
|
||||
create view community_user_ban_view as
|
||||
select *,
|
||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||
(select name from community c where cm.community_id = c.id) as community_name
|
||||
from community_user_ban cm;
|
||||
|
||||
create view site_view as
|
||||
select *,
|
||||
(select name from user_ u where s.creator_id = u.id) as creator_name,
|
||||
(select count(*) from user_) as number_of_users,
|
||||
(select count(*) from post) as number_of_posts,
|
||||
(select count(*) from comment) as number_of_comments,
|
||||
(select count(*) from community) as number_of_communities
|
||||
from site s;
|
||||
|
||||
alter table user_ rename column avatar to icon;
|
||||
alter table user_ alter column icon type bytea using icon::bytea;
|
234
server/migrations/2019-12-29-164820_add_avatar/up.sql
vendored
Normal file
234
server/migrations/2019-12-29-164820_add_avatar/up.sql
vendored
Normal file
|
@ -0,0 +1,234 @@
|
|||
-- Rename to avatar
|
||||
alter table user_ rename column icon to avatar;
|
||||
alter table user_ alter column avatar type text;
|
||||
|
||||
-- Rebuild nearly all the views, to include the creator avatars
|
||||
|
||||
-- user
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
-- post
|
||||
-- Recreate the view
|
||||
drop view post_view;
|
||||
create view post_view as
|
||||
with all_post as
|
||||
(
|
||||
select
|
||||
p.*,
|
||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
||||
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
||||
(select name from community where p.community_id = community.id) as community_name,
|
||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
||||
coalesce(sum(pl.score), 0) as score,
|
||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
||||
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
|
||||
from post p
|
||||
left join post_like pl on p.id = pl.post_id
|
||||
group by p.id
|
||||
)
|
||||
|
||||
select
|
||||
ap.*,
|
||||
u.id as user_id,
|
||||
coalesce(pl.score, 0) as my_vote,
|
||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
||||
from user_ u
|
||||
cross join all_post ap
|
||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ap.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as subscribed,
|
||||
null as read,
|
||||
null as saved
|
||||
from all_post ap
|
||||
;
|
||||
|
||||
|
||||
-- community
|
||||
drop view community_view;
|
||||
create view community_view as
|
||||
with all_community as
|
||||
(
|
||||
select *,
|
||||
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
||||
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
|
||||
(select name from category ct where c.category_id = ct.id) as category_name,
|
||||
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
||||
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
||||
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
||||
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
||||
from community c
|
||||
)
|
||||
|
||||
select
|
||||
ac.*,
|
||||
u.id as user_id,
|
||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
||||
from user_ u
|
||||
cross join all_community ac
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ac.*,
|
||||
null as user_id,
|
||||
null as subscribed
|
||||
from all_community ac
|
||||
;
|
||||
|
||||
-- reply and comment view
|
||||
drop view reply_view;
|
||||
drop view user_mention_view;
|
||||
drop view comment_view;
|
||||
create view comment_view as
|
||||
with all_comment as
|
||||
(
|
||||
select
|
||||
c.*,
|
||||
(select community_id from post p where p.id = c.post_id),
|
||||
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
||||
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
||||
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
||||
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
|
||||
coalesce(sum(cl.score), 0) as score,
|
||||
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
||||
count (case when cl.score = -1 then 1 else null end) as downvotes
|
||||
from comment c
|
||||
left join comment_like cl on c.id = cl.comment_id
|
||||
group by c.id
|
||||
)
|
||||
|
||||
select
|
||||
ac.*,
|
||||
u.id as user_id,
|
||||
coalesce(cl.score, 0) as my_vote,
|
||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
||||
from user_ u
|
||||
cross join all_comment ac
|
||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
||||
|
||||
union all
|
||||
|
||||
select
|
||||
ac.*,
|
||||
null as user_id,
|
||||
null as my_vote,
|
||||
null as saved
|
||||
from all_comment ac
|
||||
;
|
||||
|
||||
create view reply_view as
|
||||
with closereply as (
|
||||
select
|
||||
c2.id,
|
||||
c2.creator_id as sender_id,
|
||||
c.creator_id as recipient_id
|
||||
from comment c
|
||||
inner join comment c2 on c.id = c2.parent_id
|
||||
where c2.creator_id != c.creator_id
|
||||
-- Do union where post is null
|
||||
union
|
||||
select
|
||||
c.id,
|
||||
c.creator_id as sender_id,
|
||||
p.creator_id as recipient_id
|
||||
from comment c, post p
|
||||
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
||||
)
|
||||
select cv.*,
|
||||
closereply.recipient_id
|
||||
from comment_view cv, closereply
|
||||
where closereply.id = cv.id
|
||||
;
|
||||
|
||||
-- user mention
|
||||
create view user_mention_view as
|
||||
select
|
||||
c.id,
|
||||
um.id as user_mention_id,
|
||||
c.creator_id,
|
||||
c.post_id,
|
||||
c.parent_id,
|
||||
c.content,
|
||||
c.removed,
|
||||
um.read,
|
||||
c.published,
|
||||
c.updated,
|
||||
c.deleted,
|
||||
c.community_id,
|
||||
c.banned,
|
||||
c.banned_from_community,
|
||||
c.creator_name,
|
||||
c.creator_avatar,
|
||||
c.score,
|
||||
c.upvotes,
|
||||
c.downvotes,
|
||||
c.user_id,
|
||||
c.my_vote,
|
||||
c.saved,
|
||||
um.recipient_id
|
||||
from user_mention um, comment_view c
|
||||
where um.comment_id = c.id;
|
||||
|
||||
-- community views
|
||||
drop view community_moderator_view;
|
||||
drop view community_follower_view;
|
||||
drop view community_user_ban_view;
|
||||
drop view site_view;
|
||||
|
||||
create view community_moderator_view as
|
||||
select *,
|
||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||
(select avatar from user_ u where cm.user_id = u.id),
|
||||
(select name from community c where cm.community_id = c.id) as community_name
|
||||
from community_moderator cm;
|
||||
|
||||
create view community_follower_view as
|
||||
select *,
|
||||
(select name from user_ u where cf.user_id = u.id) as user_name,
|
||||
(select avatar from user_ u where cf.user_id = u.id),
|
||||
(select name from community c where cf.community_id = c.id) as community_name
|
||||
from community_follower cf;
|
||||
|
||||
create view community_user_ban_view as
|
||||
select *,
|
||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
||||
(select avatar from user_ u where cm.user_id = u.id),
|
||||
(select name from community c where cm.community_id = c.id) as community_name
|
||||
from community_user_ban cm;
|
||||
|
||||
create view site_view as
|
||||
select *,
|
||||
(select name from user_ u where s.creator_id = u.id) as creator_name,
|
||||
(select avatar from user_ u where s.creator_id = u.id) as creator_avatar,
|
||||
(select count(*) from user_) as number_of_users,
|
||||
(select count(*) from post) as number_of_posts,
|
||||
(select count(*) from comment) as number_of_comments,
|
||||
(select count(*) from community) as number_of_communities
|
||||
from site s;
|
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
15
server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
-- user
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
16
server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
-- user
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
email,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
20
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/down.sql
vendored
Normal file
20
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/down.sql
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
-- Drop the columns
|
||||
drop view user_view;
|
||||
alter table user_ drop column show_avatars;
|
||||
alter table user_ drop column send_notifications_to_email;
|
||||
|
||||
-- Rebuild the view
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
email,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
22
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/up.sql
vendored
Normal file
22
server/migrations/2020-01-02-172755_add_show_avatar_and_email_notifications_to_user/up.sql
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
-- Add columns
|
||||
alter table user_ add column show_avatars boolean default true not null;
|
||||
alter table user_ add column send_notifications_to_email boolean default false not null;
|
||||
|
||||
-- Rebuild the user_view
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select id,
|
||||
name,
|
||||
avatar,
|
||||
email,
|
||||
fedi_name,
|
||||
admin,
|
||||
banned,
|
||||
show_avatars,
|
||||
send_notifications_to_email,
|
||||
published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
|
@ -1,4 +1,6 @@
|
|||
use super::*;
|
||||
use crate::send_email;
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateComment {
|
||||
|
@ -51,20 +53,22 @@ 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;
|
||||
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
||||
// 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,24 +86,20 @@ 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());
|
||||
|
||||
if mention_user.is_ok() {
|
||||
let mention_user_id = mention_user?.id;
|
||||
|
||||
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||
// You can't mention yourself
|
||||
// At some point, make it so you can't tag the parent creator either
|
||||
// This can cause two notifications, one for reply and the other for mention
|
||||
if mention_user_id != user_id {
|
||||
if mention_user.id != user_id {
|
||||
let user_mention_form = UserMentionForm {
|
||||
recipient_id: mention_user_id,
|
||||
recipient_id: mention_user.id,
|
||||
comment_id: inserted_comment.id,
|
||||
read: None,
|
||||
};
|
||||
|
@ -109,10 +109,79 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
match UserMention::create(&conn, &user_mention_form) {
|
||||
Ok(_mention) => (),
|
||||
Err(_e) => eprintln!("{}", &_e),
|
||||
};
|
||||
|
||||
// Send an email to those users that have notifications on
|
||||
if mention_user.send_notifications_to_email {
|
||||
if let Some(mention_email) = mention_user.email {
|
||||
let subject = &format!(
|
||||
"{} - Mentioned by {}",
|
||||
Settings::get().hostname,
|
||||
claims.username
|
||||
);
|
||||
let html = &format!(
|
||||
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||
claims.username, comment_form.content, hostname
|
||||
);
|
||||
match send_email(subject, &mention_email, &mention_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send notifs to the parent commenter / poster
|
||||
match data.parent_id {
|
||||
Some(parent_id) => {
|
||||
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||
if parent_comment.creator_id != user_id {
|
||||
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||
if parent_user.send_notifications_to_email {
|
||||
if let Some(comment_reply_email) = parent_user.email {
|
||||
let subject = &format!(
|
||||
"{} - Reply from {}",
|
||||
Settings::get().hostname,
|
||||
claims.username
|
||||
);
|
||||
let html = &format!(
|
||||
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||
claims.username, comment_form.content, hostname
|
||||
);
|
||||
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Its a post
|
||||
None => {
|
||||
if post.creator_id != user_id {
|
||||
let parent_user = User_::read(&conn, post.creator_id)?;
|
||||
if parent_user.send_notifications_to_email {
|
||||
if let Some(post_reply_email) = parent_user.email {
|
||||
let subject = &format!(
|
||||
"{} - Reply from {}",
|
||||
Settings::get().hostname,
|
||||
claims.username
|
||||
);
|
||||
let html = &format!(
|
||||
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||
claims.username, comment_form.content, hostname
|
||||
);
|
||||
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
||||
Ok(_o) => _o,
|
||||
Err(e) => eprintln!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// You like your own comment by default
|
||||
let like_form = CommentLikeForm {
|
||||
|
@ -124,7 +193,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 +212,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 +232,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 +265,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 +324,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 +337,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 +362,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 +370,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 {
|
||||
|
@ -328,11 +397,11 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
CommentLike::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 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
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::db::user_mention::*;
|
|||
use crate::db::user_mention_view::*;
|
||||
use crate::db::user_view::*;
|
||||
use crate::db::*;
|
||||
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs, Settings};
|
||||
use crate::{extract_usernames, has_slurs, naive_from_unix, naive_now, remove_slurs};
|
||||
use failure::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::{generate_random_string, send_email};
|
||||
use bcrypt::verify;
|
||||
use std::str::FromStr;
|
||||
|
@ -26,6 +27,13 @@ pub struct SaveUserSettings {
|
|||
default_sort_type: i16,
|
||||
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>,
|
||||
show_avatars: bool,
|
||||
send_notifications_to_email: bool,
|
||||
auth: String,
|
||||
}
|
||||
|
||||
|
@ -166,18 +174,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
|
||||
|
@ -196,29 +199,30 @@ 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
|
||||
let user_form = UserForm {
|
||||
name: data.username.to_owned(),
|
||||
fedi_name: Settings::get().hostname.into(),
|
||||
fedi_name: Settings::get().hostname.to_owned(),
|
||||
email: data.email.to_owned(),
|
||||
avatar: None,
|
||||
password_encrypted: data.password.to_owned(),
|
||||
preferred_username: None,
|
||||
updated: None,
|
||||
|
@ -229,12 +233,14 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
// 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
|
||||
|
@ -265,7 +271,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
|
||||
|
@ -279,10 +285,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())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -302,18 +305,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,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
email,
|
||||
avatar: data.avatar.to_owned(),
|
||||
password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
admin: read_user.admin,
|
||||
|
@ -323,11 +360,13 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
default_sort_type: data.default_sort_type,
|
||||
default_listing_type: data.default_listing_type,
|
||||
lang: data.lang.to_owned(),
|
||||
show_avatars: data.show_avatars,
|
||||
send_notifications_to_email: data.send_notifications_to_email,
|
||||
};
|
||||
|
||||
let updated_user = match User_::update(&conn, user_id, &user_form) {
|
||||
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
|
||||
|
@ -366,11 +405,18 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
let user_details_id = match data.user_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
User_::read_from_name(
|
||||
match User_::read_from_name(
|
||||
&conn,
|
||||
data.username.to_owned().unwrap_or("admin".to_string()),
|
||||
)?
|
||||
.id
|
||||
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").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -430,14 +476,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)?;
|
||||
|
@ -446,6 +492,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
|
@ -456,11 +503,13 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
default_sort_type: read_user.default_sort_type,
|
||||
default_listing_type: read_user.default_listing_type,
|
||||
lang: read_user.lang,
|
||||
show_avatars: read_user.show_avatars,
|
||||
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||
};
|
||||
|
||||
match User_::update(&conn, data.user_id, &user_form) {
|
||||
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
|
||||
|
@ -492,14 +541,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)?;
|
||||
|
@ -508,6 +557,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
name: read_user.name,
|
||||
fedi_name: read_user.fedi_name,
|
||||
email: read_user.email,
|
||||
avatar: read_user.avatar,
|
||||
password_encrypted: read_user.password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
|
@ -518,11 +568,13 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
default_sort_type: read_user.default_sort_type,
|
||||
default_listing_type: read_user.default_listing_type,
|
||||
lang: read_user.lang,
|
||||
show_avatars: read_user.show_avatars,
|
||||
send_notifications_to_email: read_user.send_notifications_to_email,
|
||||
};
|
||||
|
||||
match User_::update(&conn, data.user_id, &user_form) {
|
||||
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
|
||||
|
@ -558,7 +610,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;
|
||||
|
@ -586,7 +638,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;
|
||||
|
@ -614,7 +666,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;
|
||||
|
@ -630,7 +682,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)?;
|
||||
|
@ -649,7 +701,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;
|
||||
|
@ -674,7 +726,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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -695,7 +747,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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -713,7 +765,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;
|
||||
|
@ -723,7 +775,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
|
||||
|
@ -746,7 +798,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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -774,7 +826,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()),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -793,12 +845,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
|
||||
|
@ -815,7 +862,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 {
|
||||
|
@ -833,33 +880,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,
|
||||
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
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
extern crate activitypub;
|
||||
use self::activitypub::{actor::Person, context};
|
||||
use crate::db::user::User_;
|
||||
|
||||
impl User_ {
|
||||
pub fn person(&self) -> Person {
|
||||
use crate::{to_datetime_utc, Settings};
|
||||
let base_url = &format!("{}/user/{}", Settings::get().api_endpoint(), self.name);
|
||||
let mut person = Person::default();
|
||||
person.object_props.set_context_object(context()).ok();
|
||||
person.object_props.set_id_string(base_url.to_string()).ok();
|
||||
person
|
||||
.object_props
|
||||
.set_name_string(self.name.to_owned())
|
||||
.ok();
|
||||
person
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(i) = self.updated {
|
||||
person
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(i))
|
||||
.ok();
|
||||
}
|
||||
// person.object_props.summary = self.summary;
|
||||
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_following_string(format!("{}/following", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_liked_string(format!("{}/liked", &base_url))
|
||||
.ok();
|
||||
if let Some(i) = &self.preferred_username {
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_preferred_username_string(i.to_string())
|
||||
.ok();
|
||||
}
|
||||
|
||||
person
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::User_;
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::naive_now;
|
||||
|
||||
#[test]
|
||||
fn test_person() {
|
||||
let expected_user = User_ {
|
||||
id: 52,
|
||||
name: "thom".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "here".into(),
|
||||
email: None,
|
||||
icon: None,
|
||||
published: naive_now(),
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
};
|
||||
|
||||
let person = expected_user.person();
|
||||
assert_eq!(
|
||||
"rrr/api/v1/user/thom",
|
||||
person.object_props.id_string().unwrap()
|
||||
);
|
||||
let json = serde_json::to_string_pretty(&person).unwrap();
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
117
server/src/apub/community.rs
Normal file
117
server/src/apub/community.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::constants::CACHE_INTERVAL_FEDERATION;
|
||||
use crate::db::community::Community;
|
||||
use crate::db::community_view::CommunityFollowerView;
|
||||
use crate::db::establish_connection;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Group, collection::UnorderedCollection, context};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||
use actix_web::web::Path;
|
||||
use actix_web::HttpResponse;
|
||||
use serde::Deserialize;
|
||||
|
||||
impl Community {
|
||||
pub fn as_group(&self) -> Group {
|
||||
let base_url = make_apub_endpoint("c", &self.name);
|
||||
|
||||
let mut group = Group::default();
|
||||
|
||||
group.object_props.set_context_object(context()).ok();
|
||||
group.object_props.set_id_string(base_url.to_string()).ok();
|
||||
group
|
||||
.object_props
|
||||
.set_name_string(self.name.to_owned())
|
||||
.ok();
|
||||
group
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
group
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Some(description) = &self.description {
|
||||
group
|
||||
.object_props
|
||||
.set_summary_string(description.to_string())
|
||||
.ok();
|
||||
}
|
||||
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||
.ok();
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||
.ok();
|
||||
group
|
||||
.ap_actor_props
|
||||
.set_followers_string(format!("{}/followers", &base_url))
|
||||
.ok();
|
||||
|
||||
group
|
||||
}
|
||||
|
||||
pub fn followers_as_collection(&self) -> UnorderedCollection {
|
||||
let base_url = make_apub_endpoint("c", &self.name);
|
||||
|
||||
let mut collection = UnorderedCollection::default();
|
||||
collection.object_props.set_context_object(context()).ok();
|
||||
collection.object_props.set_id_string(base_url).ok();
|
||||
|
||||
let connection = establish_connection();
|
||||
//As we are an object, we validated that the community id was valid
|
||||
let community_followers = CommunityFollowerView::for_community(&connection, self.id).unwrap();
|
||||
|
||||
let ap_followers = community_followers
|
||||
.iter()
|
||||
.map(|follower| make_apub_endpoint("u", &follower.user_name))
|
||||
.collect();
|
||||
|
||||
collection
|
||||
.collection_props
|
||||
.set_items_string_vec(ap_followers)
|
||||
.unwrap();
|
||||
collection
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommunityQuery {
|
||||
community_name: String,
|
||||
}
|
||||
|
||||
pub async fn get_apub_community(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_connection();
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||
)]))
|
||||
.body(serde_json::to_string(&community.as_group()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_connection();
|
||||
|
||||
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||
)]))
|
||||
.body(serde_json::to_string(&community.followers_as_collection()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
102
server/src/apub/mod.rs
Normal file
102
server/src/apub/mod.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
pub mod community;
|
||||
pub mod post;
|
||||
pub mod user;
|
||||
use crate::Settings;
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::community::Community;
|
||||
use crate::db::post::Post;
|
||||
use crate::db::user::User_;
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::{naive_now, Settings};
|
||||
|
||||
#[test]
|
||||
fn test_person() {
|
||||
let user = User_ {
|
||||
id: 52,
|
||||
name: "thom".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "here".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
published: naive_now(),
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let person = user.as_person();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/u/thom", Settings::get().hostname),
|
||||
person.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community() {
|
||||
let community = Community {
|
||||
id: 42,
|
||||
name: "Test".into(),
|
||||
title: "Test Title".into(),
|
||||
description: Some("Test community".into()),
|
||||
category_id: 32,
|
||||
creator_id: 52,
|
||||
removed: false,
|
||||
published: naive_now(),
|
||||
updated: Some(naive_now()),
|
||||
deleted: false,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
let group = community.as_group();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/c/Test", Settings::get().hostname),
|
||||
group.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post() {
|
||||
let post = Post {
|
||||
id: 62,
|
||||
name: "A test post".into(),
|
||||
url: None,
|
||||
body: None,
|
||||
creator_id: 52,
|
||||
community_id: 42,
|
||||
published: naive_now(),
|
||||
removed: false,
|
||||
locked: false,
|
||||
stickied: false,
|
||||
nsfw: false,
|
||||
deleted: false,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let page = post.as_page();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/post/62", Settings::get().hostname),
|
||||
page.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_apub_endpoint<S: Display, T: Display>(point: S, value: T) -> String {
|
||||
format!(
|
||||
"https://{}/federation/{}/{}",
|
||||
Settings::get().hostname,
|
||||
point,
|
||||
value
|
||||
)
|
||||
}
|
38
server/src/apub/post.rs
Normal file
38
server/src/apub/post.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::post::Post;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{context, object::Page};
|
||||
|
||||
impl Post {
|
||||
pub fn as_page(&self) -> Page {
|
||||
let base_url = make_apub_endpoint("post", self.id);
|
||||
let mut page = Page::default();
|
||||
|
||||
page.object_props.set_context_object(context()).ok();
|
||||
page.object_props.set_id_string(base_url).ok();
|
||||
page.object_props.set_name_string(self.name.to_owned()).ok();
|
||||
|
||||
if let Some(body) = &self.body {
|
||||
page.object_props.set_content_string(body.to_owned()).ok();
|
||||
}
|
||||
|
||||
if let Some(url) = &self.url {
|
||||
page.object_props.set_url_string(url.to_owned()).ok();
|
||||
}
|
||||
|
||||
//page.object_props.set_attributed_to_string
|
||||
|
||||
page
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
page
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
}
|
||||
|
||||
page
|
||||
}
|
||||
}
|
79
server/src/apub/user.rs
Normal file
79
server/src/apub/user.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::constants::CACHE_INTERVAL_FEDERATION;
|
||||
use crate::db::establish_connection;
|
||||
use crate::db::user::User_;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{actor::Person, context};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||
use actix_web::web::Path;
|
||||
use actix_web::HttpResponse;
|
||||
use serde::Deserialize;
|
||||
|
||||
impl User_ {
|
||||
pub fn as_person(&self) -> Person {
|
||||
let base_url = make_apub_endpoint("u", &self.name);
|
||||
let mut person = Person::default();
|
||||
person.object_props.set_context_object(context()).ok();
|
||||
person.object_props.set_id_string(base_url.to_string()).ok();
|
||||
person
|
||||
.object_props
|
||||
.set_name_string(self.name.to_owned())
|
||||
.ok();
|
||||
person
|
||||
.object_props
|
||||
.set_published_utctime(to_datetime_utc(self.published))
|
||||
.ok();
|
||||
if let Some(updated) = self.updated {
|
||||
person
|
||||
.object_props
|
||||
.set_updated_utctime(to_datetime_utc(updated))
|
||||
.ok();
|
||||
}
|
||||
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_inbox_string(format!("{}/inbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_outbox_string(format!("{}/outbox", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_following_string(format!("{}/following", &base_url))
|
||||
.ok();
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_liked_string(format!("{}/liked", &base_url))
|
||||
.ok();
|
||||
if let Some(i) = &self.preferred_username {
|
||||
person
|
||||
.ap_actor_props
|
||||
.set_preferred_username_string(i.to_string())
|
||||
.ok();
|
||||
}
|
||||
|
||||
person
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserQuery {
|
||||
user_name: String,
|
||||
}
|
||||
|
||||
pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
|
||||
let connection = establish_connection();
|
||||
|
||||
if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||
CACHE_INTERVAL_FEDERATION.num_seconds() as u32,
|
||||
)]))
|
||||
.body(serde_json::to_string(&user.as_person()).unwrap())
|
||||
} else {
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
6
server/src/constants.rs
Normal file
6
server/src/constants.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use chrono::Duration;
|
||||
|
||||
// TODO: should all be 0 during debug
|
||||
pub static CACHE_INTERVAL_FEEDS: Duration = Duration::minutes(15);
|
||||
pub static CACHE_INTERVAL_FEDERATION: Duration = Duration::minutes(1);
|
||||
pub static CACHE_INTERVAL_FRONTEND: Duration = Duration::hours(2);
|
|
@ -174,6 +174,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -182,6 +183,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -18,6 +18,7 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -46,6 +47,7 @@ pub struct CommentView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -226,6 +228,7 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -255,6 +258,7 @@ pub struct ReplyView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -368,6 +372,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -376,6 +381,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -447,6 +454,7 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
upvotes: 1,
|
||||
|
@ -470,6 +478,7 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
upvotes: 1,
|
||||
|
|
|
@ -68,6 +68,10 @@ impl Community {
|
|||
.filter(name.eq(community_name))
|
||||
.first::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> String {
|
||||
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
|
||||
|
@ -216,6 +220,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -224,6 +229,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
|||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
|
@ -33,6 +34,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +46,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +58,7 @@ table! {
|
|||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
user_name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +80,7 @@ pub struct CommunityView {
|
|||
pub deleted: bool,
|
||||
pub nsfw: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub category_name: String,
|
||||
pub number_of_subscribers: i64,
|
||||
pub number_of_posts: i64,
|
||||
|
@ -119,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
|
||||
}
|
||||
|
@ -224,6 +229,7 @@ pub struct CommunityModeratorView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
@ -253,6 +259,7 @@ pub struct CommunityFollowerView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
@ -282,6 +289,7 @@ pub struct CommunityUserBanView {
|
|||
pub user_id: i32,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub user_name: String,
|
||||
pub avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::Settings;
|
||||
extern crate lazy_static;
|
||||
use crate::settings::Settings;
|
||||
use diesel::dsl::*;
|
||||
use diesel::r2d2::*;
|
||||
use diesel::result::Error;
|
||||
use diesel::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -99,19 +101,29 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn establish_connection() -> PgConnection {
|
||||
let db_url = Settings::get().db_url;
|
||||
PgConnection::establish(&db_url).expect(&format!("Error connecting to {}", db_url))
|
||||
lazy_static! {
|
||||
static ref PG_POOL: Pool<ConnectionManager<PgConnection>> = {
|
||||
let db_url = Settings::get().get_database_url();
|
||||
let manager = ConnectionManager::<PgConnection>::new(&db_url);
|
||||
Pool::builder()
|
||||
.max_size(Settings::get().database.pool_size)
|
||||
.build(manager)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", db_url))
|
||||
};
|
||||
}
|
||||
|
||||
pub fn establish_connection() -> PooledConnection<ConnectionManager<PgConnection>> {
|
||||
PG_POOL.get().unwrap()
|
||||
}
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -442,6 +442,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -450,6 +451,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
|
||||
|
@ -460,6 +463,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -468,6 +472,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use super::*;
|
||||
use crate::schema::password_reset_request;
|
||||
use crate::schema::password_reset_request::dsl::*;
|
||||
use crypto::digest::Digest;
|
||||
use crypto::sha2::Sha256;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug)]
|
||||
#[table_name = "password_reset_request"]
|
||||
|
@ -49,8 +48,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
|
|||
impl PasswordResetRequest {
|
||||
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.input_str(token);
|
||||
let token_hash = hasher.result_str();
|
||||
hasher.input(token);
|
||||
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||
|
||||
let form = PasswordResetRequestForm {
|
||||
user_id: from_user_id,
|
||||
|
@ -61,13 +60,21 @@ impl PasswordResetRequest {
|
|||
}
|
||||
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.input_str(token);
|
||||
let token_hash = hasher.result_str();
|
||||
hasher.input(token);
|
||||
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec());
|
||||
password_reset_request
|
||||
.filter(token_encrypted.eq(token_hash))
|
||||
.filter(published.gt(now - 1.days()))
|
||||
.first::<Self>(conn)
|
||||
}
|
||||
|
||||
fn bytes_to_hex(bytes: Vec<u8>) -> String {
|
||||
let mut str = String::new();
|
||||
for byte in bytes {
|
||||
str = format!("{}{:02x}", str, byte);
|
||||
}
|
||||
str
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -85,6 +92,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -93,27 +101,26 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
||||
let new_password_reset_request = PasswordResetRequestForm {
|
||||
user_id: inserted_user.id,
|
||||
token_encrypted: "no".into(),
|
||||
};
|
||||
let token = "nope";
|
||||
let token_encrypted_ = "ca3704aa0b06f5954c79ee837faa152d84d6b2d42838f0637a15eda8337dbdce";
|
||||
|
||||
let inserted_password_reset_request =
|
||||
PasswordResetRequest::create(&conn, &new_password_reset_request).unwrap();
|
||||
PasswordResetRequest::create_token(&conn, inserted_user.id, token).unwrap();
|
||||
|
||||
let expected_password_reset_request = PasswordResetRequest {
|
||||
id: inserted_password_reset_request.id,
|
||||
user_id: inserted_user.id,
|
||||
token_encrypted: "no".into(),
|
||||
token_encrypted: token_encrypted_.to_string(),
|
||||
published: inserted_password_reset_request.published,
|
||||
};
|
||||
|
||||
let read_password_reset_request =
|
||||
PasswordResetRequest::read(&conn, inserted_password_reset_request.id).unwrap();
|
||||
let read_password_reset_request = PasswordResetRequest::read_from_token(&conn, token).unwrap();
|
||||
let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
|
||||
|
||||
assert_eq!(expected_password_reset_request, read_password_reset_request);
|
||||
|
|
|
@ -187,6 +187,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -195,6 +196,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
|
|
@ -21,6 +21,7 @@ table! {
|
|||
banned_from_community -> Bool,
|
||||
stickied -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
community_name -> Varchar,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
|
@ -59,6 +60,7 @@ pub struct PostView {
|
|||
pub banned_from_community: bool,
|
||||
pub stickied: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub community_name: String,
|
||||
pub community_removed: bool,
|
||||
pub community_deleted: bool,
|
||||
|
@ -187,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
|
||||
|
@ -303,6 +302,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
updated: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -311,6 +311,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -377,6 +379,7 @@ mod tests {
|
|||
body: None,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
community_id: inserted_community.id,
|
||||
|
@ -405,7 +408,7 @@ mod tests {
|
|||
user_id: Some(inserted_user.id),
|
||||
my_vote: Some(1),
|
||||
id: inserted_post.id,
|
||||
name: post_name.to_owned(),
|
||||
name: post_name,
|
||||
url: None,
|
||||
body: None,
|
||||
removed: false,
|
||||
|
@ -413,11 +416,12 @@ mod tests {
|
|||
locked: false,
|
||||
stickied: false,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_name: user_name,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
community_id: inserted_community.id,
|
||||
community_name: community_name.to_owned(),
|
||||
community_name,
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
community_nsfw: false,
|
||||
|
|
|
@ -12,6 +12,7 @@ table! {
|
|||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
number_of_users -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
number_of_comments -> BigInt,
|
||||
|
@ -34,6 +35,7 @@ pub struct SiteView {
|
|||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub number_of_users: i64,
|
||||
pub number_of_posts: i64,
|
||||
pub number_of_comments: i64,
|
||||
|
|
|
@ -14,7 +14,7 @@ pub struct User_ {
|
|||
pub preferred_username: Option<String>,
|
||||
pub password_encrypted: String,
|
||||
pub email: Option<String>,
|
||||
pub icon: Option<Vec<u8>>,
|
||||
pub avatar: Option<String>,
|
||||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
|
@ -24,6 +24,8 @@ pub struct User_ {
|
|||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
|
@ -36,12 +38,15 @@ pub struct UserForm {
|
|||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub email: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
}
|
||||
|
||||
impl Crud<UserForm> for User_ {
|
||||
|
@ -74,14 +79,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> {
|
||||
|
@ -99,6 +103,8 @@ pub struct Claims {
|
|||
pub default_sort_type: i16,
|
||||
pub default_listing_type: i16,
|
||||
pub lang: String,
|
||||
pub avatar: Option<String>,
|
||||
pub show_avatars: bool,
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
|
@ -123,6 +129,8 @@ impl User_ {
|
|||
default_sort_type: self.default_sort_type,
|
||||
default_listing_type: self.default_listing_type,
|
||||
lang: self.lang.to_owned(),
|
||||
avatar: self.avatar.to_owned(),
|
||||
show_avatars: self.show_avatars.to_owned(),
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
|
@ -132,23 +140,27 @@ impl User_ {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<Self, Error> {
|
||||
user_.filter(name.eq(username)).first::<User_>(conn)
|
||||
}
|
||||
|
||||
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
||||
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
||||
}
|
||||
|
||||
pub fn find_by_email_or_username(
|
||||
conn: &PgConnection,
|
||||
username_or_email: &str,
|
||||
) -> Result<Self, Error> {
|
||||
if is_email_regex(username_or_email) {
|
||||
user_
|
||||
.filter(email.eq(username_or_email))
|
||||
.first::<User_>(conn)
|
||||
User_::find_by_email(conn, username_or_email)
|
||||
} else {
|
||||
user_
|
||||
.filter(name.eq(username_or_email))
|
||||
.first::<User_>(conn)
|
||||
User_::find_by_username(conn, username_or_email)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
|
||||
user_.filter(email.eq(from_email)).first::<User_>(conn)
|
||||
pub fn get_profile_url(&self) -> String {
|
||||
format!("https://{}/u/{}", Settings::get().hostname, self.name)
|
||||
}
|
||||
|
||||
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
|
||||
|
@ -172,6 +184,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -180,6 +193,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -191,7 +206,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
icon: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
published: inserted_user.published,
|
||||
|
@ -201,6 +216,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||
|
|
|
@ -68,6 +68,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -76,6 +77,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_user = User_::create(&conn, &new_user).unwrap();
|
||||
|
@ -86,6 +89,7 @@ mod tests {
|
|||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -94,6 +98,8 @@ mod tests {
|
|||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||
|
|
|
@ -20,6 +20,7 @@ table! {
|
|||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
|
@ -50,6 +51,7 @@ pub struct UserMentionView {
|
|||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
|
@ -78,7 +80,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
|||
UserMentionQueryBuilder {
|
||||
conn,
|
||||
query,
|
||||
for_user_id: for_user_id,
|
||||
for_user_id,
|
||||
sort: &SortType::New,
|
||||
unread_only: false,
|
||||
page: None,
|
||||
|
|
|
@ -6,9 +6,13 @@ table! {
|
|||
user_view (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
avatar -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
fedi_name -> Varchar,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
published -> Timestamp,
|
||||
number_of_posts -> BigInt,
|
||||
post_score -> BigInt,
|
||||
|
@ -24,9 +28,13 @@ table! {
|
|||
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,
|
||||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub number_of_posts: i64,
|
||||
pub post_score: i64,
|
||||
|
|
|
@ -11,7 +11,6 @@ pub extern crate actix;
|
|||
pub extern crate actix_web;
|
||||
pub extern crate bcrypt;
|
||||
pub extern crate chrono;
|
||||
pub extern crate crypto;
|
||||
pub extern crate dotenv;
|
||||
pub extern crate jsonwebtoken;
|
||||
pub extern crate lettre;
|
||||
|
@ -20,19 +19,21 @@ pub extern crate rand;
|
|||
pub extern crate regex;
|
||||
pub extern crate serde;
|
||||
pub extern crate serde_json;
|
||||
pub extern crate sha2;
|
||||
pub extern crate strum;
|
||||
|
||||
pub mod api;
|
||||
pub mod apub;
|
||||
pub mod constants;
|
||||
pub mod db;
|
||||
pub mod feeds;
|
||||
pub mod nodeinfo;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
pub mod settings;
|
||||
pub mod version;
|
||||
pub mod websocket;
|
||||
|
||||
use crate::settings::Settings;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use dotenv::dotenv;
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
|
@ -40,91 +41,7 @@ use lettre::{SmtpClient, Transport};
|
|||
use lettre_email::Email;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub struct Settings {
|
||||
pub db_url: String,
|
||||
pub hostname: String,
|
||||
pub bind: IpAddr,
|
||||
pub port: u16,
|
||||
pub jwt_secret: String,
|
||||
pub rate_limit_message: i32,
|
||||
pub rate_limit_message_per_second: i32,
|
||||
pub rate_limit_post: i32,
|
||||
pub rate_limit_post_per_second: i32,
|
||||
pub rate_limit_register: i32,
|
||||
pub rate_limit_register_per_second: i32,
|
||||
pub email_config: Option<EmailConfig>,
|
||||
}
|
||||
|
||||
pub struct EmailConfig {
|
||||
smtp_server: String,
|
||||
smtp_login: String,
|
||||
smtp_password: String,
|
||||
smtp_from_address: String,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn get() -> Self {
|
||||
dotenv().ok();
|
||||
|
||||
let email_config =
|
||||
if env::var("SMTP_SERVER").is_ok() && !env::var("SMTP_SERVER").unwrap().eq("") {
|
||||
Some(EmailConfig {
|
||||
smtp_server: env::var("SMTP_SERVER").expect("SMTP_SERVER must be set"),
|
||||
smtp_login: env::var("SMTP_LOGIN").expect("SMTP_LOGIN must be set"),
|
||||
smtp_password: env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD must be set"),
|
||||
smtp_from_address: env::var("SMTP_FROM_ADDRESS").expect("SMTP_FROM_ADDRESS must be set"),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Settings {
|
||||
db_url: env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
|
||||
hostname: env::var("HOSTNAME").unwrap_or("rrr".to_string()),
|
||||
bind: env::var("BIND")
|
||||
.unwrap_or("0.0.0.0".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
port: env::var("PORT")
|
||||
.unwrap_or("8536".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
jwt_secret: env::var("JWT_SECRET").unwrap_or("changeme".to_string()),
|
||||
rate_limit_message: env::var("RATE_LIMIT_MESSAGE")
|
||||
.unwrap_or("30".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
rate_limit_message_per_second: env::var("RATE_LIMIT_MESSAGE_PER_SECOND")
|
||||
.unwrap_or("60".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
rate_limit_post: env::var("RATE_LIMIT_POST")
|
||||
.unwrap_or("3".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
rate_limit_post_per_second: env::var("RATE_LIMIT_POST_PER_SECOND")
|
||||
.unwrap_or("600".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
rate_limit_register: env::var("RATE_LIMIT_REGISTER")
|
||||
.unwrap_or("1".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
rate_limit_register_per_second: env::var("RATE_LIMIT_REGISTER_PER_SECOND")
|
||||
.unwrap_or("3600".to_string())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
email_config,
|
||||
}
|
||||
}
|
||||
fn api_endpoint(&self) -> String {
|
||||
format!("{}/api/v1", self.hostname)
|
||||
}
|
||||
}
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||
|
@ -174,13 +91,13 @@ pub fn send_email(
|
|||
to_username: &str,
|
||||
html: &str,
|
||||
) -> Result<(), String> {
|
||||
let email_config = Settings::get().email_config.ok_or("no_email_setup")?;
|
||||
let email_config = Settings::get().email.as_ref().ok_or("no_email_setup")?;
|
||||
|
||||
let email = Email::builder()
|
||||
.to((to_email, to_username))
|
||||
.from((
|
||||
email_config.smtp_login.to_owned(),
|
||||
email_config.smtp_from_address,
|
||||
email_config.smtp_from_address.to_owned(),
|
||||
))
|
||||
.subject(subject)
|
||||
.html(html)
|
||||
|
@ -209,11 +126,7 @@ pub fn send_email(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs, Settings};
|
||||
#[test]
|
||||
fn test_api() {
|
||||
assert_eq!(Settings::get().api_endpoint(), "rrr/api/v1");
|
||||
}
|
||||
use crate::{extract_usernames, has_slurs, is_email_regex, remove_slurs};
|
||||
|
||||
#[test]
|
||||
fn test_email() {
|
||||
|
@ -224,11 +137,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_slur_filter() {
|
||||
let test =
|
||||
"coons test dindu ladyboy tranny retardeds. This is a bunch of other safe text.".to_string();
|
||||
"coons test dindu ladyboy tranny retardeds. Capitalized Nigger. This is a bunch of other safe text.".to_string();
|
||||
let slur_free = "No slurs here";
|
||||
assert_eq!(
|
||||
remove_slurs(&test),
|
||||
"*removed* test *removed* *removed* *removed* *removed*. This is a bunch of other safe text."
|
||||
"*removed* test *removed* *removed* *removed* *removed*. Capitalized *removed*. This is a bunch of other safe text."
|
||||
.to_string()
|
||||
);
|
||||
assert!(has_slurs(&test));
|
||||
|
@ -251,6 +164,6 @@ mod tests {
|
|||
|
||||
lazy_static! {
|
||||
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||
static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").unwrap();
|
||||
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
|
||||
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
|
||||
}
|
||||
|
|
|
@ -2,260 +2,48 @@ extern crate lemmy_server;
|
|||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::*;
|
||||
use actix_web_actors::ws;
|
||||
use lemmy_server::db::establish_connection;
|
||||
use lemmy_server::feeds;
|
||||
use lemmy_server::nodeinfo;
|
||||
use lemmy_server::websocket::server::*;
|
||||
use lemmy_server::Settings;
|
||||
use std::env;
|
||||
use std::time::{Duration, Instant};
|
||||
use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
|
||||
use lemmy_server::settings::Settings;
|
||||
use std::io;
|
||||
|
||||
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();
|
||||
let sys = actix::System::new("lemmy");
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
// 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();
|
||||
|
||||
println!(
|
||||
"Starting http server at {}:{}",
|
||||
settings.bind, settings.port
|
||||
);
|
||||
|
||||
// Create Http server with websocket support
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(server.clone())
|
||||
// Front end routes
|
||||
.service(actix_files::Files::new("/static", front_end_dir()))
|
||||
.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))
|
||||
.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(),
|
||||
))
|
||||
.service(actix_files::Files::new(
|
||||
"/docs",
|
||||
settings.front_end_dir.to_owned() + "/documentation",
|
||||
))
|
||||
})
|
||||
.bind((settings.bind, settings.port))
|
||||
.unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server at {}:{}", settings.bind, settings.port);
|
||||
let _ = sys.run();
|
||||
}
|
||||
|
||||
fn index() -> Result<NamedFile, actix_web::error::Error> {
|
||||
Ok(NamedFile::open(front_end_dir() + "/index.html")?)
|
||||
}
|
||||
|
||||
fn front_end_dir() -> String {
|
||||
env::var("LEMMY_FRONT_END_DIR").unwrap_or("../ui/dist".to_string())
|
||||
.bind((settings.bind, settings.port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
19
server/src/routes/federation.rs
Normal file
19
server/src/routes/federation.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use crate::apub;
|
||||
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||
use actix_web::web;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
.route(
|
||||
"/federation/c/{community_name}",
|
||||
web::get().to(apub::community::get_apub_community),
|
||||
)
|
||||
.route(
|
||||
"/federation/c/{community_name}/followers",
|
||||
web::get().to(apub::community::get_apub_community_followers),
|
||||
)
|
||||
.route(
|
||||
"/federation/u/{user_name}",
|
||||
web::get().to(apub::user::get_apub_user),
|
||||
);
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
extern crate rss;
|
||||
|
||||
use super::*;
|
||||
use crate::constants::CACHE_INTERVAL_FEEDS;
|
||||
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::http::header::{CacheControl, CacheDirective};
|
||||
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 +32,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));
|
||||
}
|
||||
|
||||
async 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 +55,10 @@ 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> {
|
||||
async 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(),
|
||||
|
@ -70,6 +83,9 @@ pub fn get_feed(path: web::Path<(String, String)>, info: web::Query<Params>) ->
|
|||
|
||||
match feed_result {
|
||||
Ok(rss) => HttpResponse::Ok()
|
||||
.set(CacheControl(vec![CacheDirective::MaxAge(
|
||||
CACHE_INTERVAL_FEEDS.num_seconds() as u32,
|
||||
)]))
|
||||
.content_type("application/rss+xml")
|
||||
.body(rss),
|
||||
Err(_) => HttpResponse::NotFound().finish(),
|
||||
|
@ -77,7 +93,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)
|
||||
}
|
||||
|
||||
|
@ -110,8 +129,8 @@ fn get_feed_user(sort_type: &SortType, user_name: String) -> Result<String, Erro
|
|||
let conn = establish_connection();
|
||||
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
let user = User_::find_by_email_or_username(&conn, &user_name)?;
|
||||
let user_url = format!("https://{}/u/{}", Settings::get().hostname, user.name);
|
||||
let user = User_::find_by_username(&conn, &user_name)?;
|
||||
let user_url = user.get_profile_url();
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
.listing_type(ListingType::All)
|
||||
|
@ -135,7 +154,7 @@ fn get_feed_community(sort_type: &SortType, community_name: String) -> Result<St
|
|||
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
let community = Community::read_from_name(&conn, community_name)?;
|
||||
let community_url = format!("https://{}/c/{}", Settings::get().hostname, community.name);
|
||||
let community_url = community.get_url();
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
.listing_type(ListingType::All)
|
||||
|
@ -162,7 +181,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 +208,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;
|
||||
|
||||
|
@ -331,7 +350,7 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
|
|||
"/c/{} <a href=\"{}\">(link)</a>",
|
||||
p.community_name, community_url
|
||||
))
|
||||
.domain(Settings::get().hostname)
|
||||
.domain(Settings::get().hostname.to_owned())
|
||||
.build();
|
||||
i.categories(vec![category.unwrap()]);
|
||||
|
51
server/src/routes/index.rs
Normal file
51
server/src/routes/index.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::constants::CACHE_INTERVAL_FRONTEND;
|
||||
use crate::settings::Settings;
|
||||
use actix::Response;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||
use actix_web::middleware;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
async fn index() -> Result<NamedFile, actix_web::error::Error> {
|
||||
// TODO: figure out how to set cache-control header here
|
||||
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,10 +3,17 @@ 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 node_info_well_known() -> HttpResponse<Body> {
|
||||
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));
|
||||
}
|
||||
|
||||
async fn node_info_well_known() -> HttpResponse<Body> {
|
||||
let json = json!({
|
||||
"links": {
|
||||
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
|
@ -14,34 +21,39 @@ 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> {
|
||||
async fn node_info() -> HttpResponse<Body> {
|
||||
let conn = establish_connection();
|
||||
let site_view = match SiteView::read(&conn) {
|
||||
Ok(site_view) => site_view,
|
||||
Err(_e) => return HttpResponse::InternalServerError().finish(),
|
||||
};
|
||||
let protocols = if Settings::get().federation_enabled {
|
||||
vec!["activitypub"]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let json = json!({
|
||||
"version": "2.0",
|
||||
"software": {
|
||||
"name": "lemmy",
|
||||
"version": version::VERSION,
|
||||
},
|
||||
"protocols": [],
|
||||
"protocols": protocols,
|
||||
"usage": {
|
||||
"users": {
|
||||
"total": site_view.number_of_users
|
||||
},
|
||||
"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())
|
||||
}
|
90
server/src/routes/webfinger.rs
Normal file
90
server/src/routes/webfinger.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
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;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
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}})@{}$",
|
||||
Settings::get().hostname
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Responds to webfinger requests of the following format. There isn't any real documentation for
|
||||
/// this, but it described in this blog post:
|
||||
/// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social
|
||||
///
|
||||
/// You can also view the webfinger response that Mastodon sends:
|
||||
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
|
||||
async fn get_webfinger_response(info: Query<Params>) -> HttpResponse<Body> {
|
||||
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
|
||||
.captures(&info.resource)
|
||||
.map(|c| c.get(1));
|
||||
// TODO: replace this with .flatten() once we are running rust 1.40
|
||||
let regex_parsed_flattened = match regex_parsed {
|
||||
Some(s) => s,
|
||||
None => None,
|
||||
};
|
||||
let community_name = match regex_parsed_flattened {
|
||||
Some(c) => c.as_str(),
|
||||
None => return HttpResponse::NotFound().finish(),
|
||||
};
|
||||
|
||||
// Make sure the requested community exists.
|
||||
let conn = establish_connection();
|
||||
let community = match Community::read_from_name(&conn, community_name.to_string()) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return HttpResponse::NotFound().finish(),
|
||||
};
|
||||
|
||||
let community_url = community.get_url();
|
||||
|
||||
let json = json!({
|
||||
"subject": info.resource,
|
||||
"aliases": [
|
||||
community_url,
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"type": "text/html",
|
||||
"href": community_url
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
// Yes this is correct, this link doesn't include the `.json` extension
|
||||
"href": community_url
|
||||
}
|
||||
// TODO: this also needs to return the subscribe link once that's implemented
|
||||
//{
|
||||
// "rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
|
||||
//}
|
||||
]
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/activity+json")
|
||||
.body(json.to_string())
|
||||
}
|
186
server/src/routes/websocket.rs
Normal file
186
server/src/routes/websocket.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
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
|
||||
async 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(),
|
||||
}
|
||||
actix::fut::ready(())
|
||||
})
|
||||
.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<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
||||
fn handle(&mut self, result: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
// println!("WEBSOCKET MESSAGE: {:?} from id: {}", msg, self.id);
|
||||
let message = match result {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match message {
|
||||
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);
|
||||
}
|
||||
}
|
||||
actix::fut::ready(())
|
||||
})
|
||||
.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(b"");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -260,7 +260,7 @@ table! {
|
|||
preferred_username -> Nullable<Varchar>,
|
||||
password_encrypted -> Text,
|
||||
email -> Nullable<Text>,
|
||||
icon -> Nullable<Bytea>,
|
||||
avatar -> Nullable<Text>,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
published -> Timestamp,
|
||||
|
@ -270,6 +270,8 @@ table! {
|
|||
default_sort_type -> Int2,
|
||||
default_listing_type -> Int2,
|
||||
lang -> Varchar,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
106
server/src/settings.rs
Normal file
106
server/src/settings.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
extern crate lazy_static;
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
|
||||
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
|
||||
static CONFIG_FILE: &str = "config/config.hjson";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
pub hostname: String,
|
||||
pub bind: IpAddr,
|
||||
pub port: u16,
|
||||
pub jwt_secret: String,
|
||||
pub front_end_dir: String,
|
||||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RateLimitConfig {
|
||||
pub message: i32,
|
||||
pub message_per_second: i32,
|
||||
pub post: i32,
|
||||
pub post_per_second: i32,
|
||||
pub register: i32,
|
||||
pub register_per_second: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EmailConfig {
|
||||
pub smtp_server: String,
|
||||
pub smtp_login: String,
|
||||
pub smtp_password: String,
|
||||
pub smtp_from_address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Database {
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub host: String,
|
||||
pub port: i32,
|
||||
pub database: String,
|
||||
pub pool_size: u32,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SETTINGS: Settings = {
|
||||
match Settings::init() {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("{}", e),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Reads config from the files and environment.
|
||||
/// First, defaults are loaded from CONFIG_FILE_DEFAULTS, then these values can be overwritten
|
||||
/// from CONFIG_FILE (optional). Finally, values from the environment (with prefix LEMMY) are
|
||||
/// added to the config.
|
||||
fn init() -> Result<Self, ConfigError> {
|
||||
let mut s = Config::new();
|
||||
|
||||
s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
|
||||
|
||||
s.merge(File::with_name(CONFIG_FILE).required(false))?;
|
||||
|
||||
// Add in settings from the environment (with a prefix of LEMMY)
|
||||
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
|
||||
// Note: we need to use double underscore here, because otherwise variables containing
|
||||
// underscore cant be set from environmnet.
|
||||
// https://github.com/mehcode/config-rs/issues/73
|
||||
s.merge(Environment::with_prefix("LEMMY").separator("__"))?;
|
||||
|
||||
s.try_into()
|
||||
}
|
||||
|
||||
/// Returns the config as a struct.
|
||||
pub fn get() -> &'static Self {
|
||||
&SETTINGS
|
||||
}
|
||||
|
||||
/// Returns the postgres connection url. If LEMMY_DATABASE_URL is set, that is used,
|
||||
/// otherwise the connection url is generated from the config.
|
||||
pub fn get_database_url(&self) -> String {
|
||||
match env::var("LEMMY_DATABASE_URL") {
|
||||
Ok(url) => url,
|
||||
Err(_) => format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.database.user,
|
||||
self.database.password,
|
||||
self.database.host,
|
||||
self.database.port,
|
||||
self.database.database
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn api_endpoint(&self) -> String {
|
||||
format!("{}/api/v1", self.hostname)
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &'static str = "v0.5.0.3";
|
||||
pub const VERSION: &str = "v0.5.17";
|
||||
|
|
|
@ -21,6 +21,7 @@ use crate::Settings;
|
|||
|
||||
/// Chat server sends this messages to session
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct WSMessage(pub String);
|
||||
|
||||
/// Message for chat server communications
|
||||
|
@ -35,6 +36,7 @@ pub struct Connect {
|
|||
|
||||
/// Session is disconnected
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct Disconnect {
|
||||
pub id: usize,
|
||||
pub ip: String,
|
||||
|
@ -42,6 +44,7 @@ pub struct Disconnect {
|
|||
|
||||
/// Send message to specific room
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ClientMessage {
|
||||
/// Id of the client session
|
||||
pub id: usize,
|
||||
|
@ -51,7 +54,8 @@ pub struct ClientMessage {
|
|||
pub room: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Message)]
|
||||
#[rtype(String)]
|
||||
pub struct StandardMessage {
|
||||
/// Id of the client session
|
||||
pub id: usize,
|
||||
|
@ -59,10 +63,6 @@ pub struct StandardMessage {
|
|||
pub msg: String,
|
||||
}
|
||||
|
||||
impl actix::Message for StandardMessage {
|
||||
type Result = String;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimitBucket {
|
||||
last_checked: SystemTime,
|
||||
|
@ -91,7 +91,7 @@ impl Default for ChatServer {
|
|||
ChatServer {
|
||||
sessions: HashMap::new(),
|
||||
rate_limits: HashMap::new(),
|
||||
rooms: rooms,
|
||||
rooms,
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
|
@ -99,8 +99,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) {
|
||||
|
@ -113,7 +113,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);
|
||||
}
|
||||
|
||||
|
@ -122,12 +122,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> {
|
||||
|
@ -138,12 +138,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(())
|
||||
|
@ -152,27 +152,28 @@ impl ChatServer {
|
|||
fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> {
|
||||
self.check_rate_limit_full(
|
||||
id,
|
||||
Settings::get().rate_limit_register,
|
||||
Settings::get().rate_limit_register_per_second,
|
||||
Settings::get().rate_limit.register,
|
||||
Settings::get().rate_limit.register_per_second,
|
||||
)
|
||||
}
|
||||
|
||||
fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> {
|
||||
self.check_rate_limit_full(
|
||||
id,
|
||||
Settings::get().rate_limit_post,
|
||||
Settings::get().rate_limit_post_per_second,
|
||||
Settings::get().rate_limit.post,
|
||||
Settings::get().rate_limit.post_per_second,
|
||||
)
|
||||
}
|
||||
|
||||
fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> {
|
||||
self.check_rate_limit_full(
|
||||
id,
|
||||
Settings::get().rate_limit_message,
|
||||
Settings::get().rate_limit_message_per_second,
|
||||
Settings::get().rate_limit.message,
|
||||
Settings::get().rate_limit.message_per_second,
|
||||
)
|
||||
}
|
||||
|
||||
#[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) {
|
||||
|
@ -194,10 +195,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(())
|
||||
|
@ -264,7 +268,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);
|
||||
}
|
||||
|
@ -292,7 +296,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)?;
|
||||
|
@ -374,7 +378,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 => {
|
||||
|
@ -392,7 +396,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 => {
|
||||
|
@ -400,7 +404,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 => {
|
||||
|
@ -437,7 +441,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 => {
|
||||
|
@ -454,7 +458,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 => {
|
||||
|
@ -465,7 +469,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 => {
|
||||
|
@ -482,7 +486,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 => {
|
||||
|
|
2
ui/.eslintignore
vendored
Normal file
2
ui/.eslintignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
fuse.js
|
||||
translation_report.ts
|
10
ui/fuse.js
vendored
10
ui/fuse.js
vendored
|
@ -4,7 +4,7 @@ const {
|
|||
EnvPlugin,
|
||||
CSSPlugin,
|
||||
WebIndexPlugin,
|
||||
QuantumPlugin
|
||||
QuantumPlugin,
|
||||
} = require('fuse-box');
|
||||
// const transformInferno = require('../../dist').default
|
||||
const transformInferno = require('ts-transform-inferno').default;
|
||||
|
@ -25,7 +25,7 @@ Sparky.task('config', _ => {
|
|||
before: [transformClasscat(), transformInferno()],
|
||||
},
|
||||
alias: {
|
||||
'locale': 'moment/locale'
|
||||
locale: 'moment/locale',
|
||||
},
|
||||
plugins: [
|
||||
EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
|
||||
|
@ -33,7 +33,7 @@ Sparky.task('config', _ => {
|
|||
WebIndexPlugin({
|
||||
title: 'Inferno Typescript FuseBox Example',
|
||||
template: 'src/index.html',
|
||||
path: isProduction ? "/static" : "/"
|
||||
path: isProduction ? '/static' : '/',
|
||||
}),
|
||||
isProduction &&
|
||||
QuantumPlugin({
|
||||
|
@ -48,7 +48,9 @@ Sparky.task('config', _ => {
|
|||
// Sparky.task('version', _ => setVersion());
|
||||
Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
|
||||
Sparky.task('env', _ => (isProduction = true));
|
||||
Sparky.task('copy-assets', () => Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static'));
|
||||
Sparky.task('copy-assets', () =>
|
||||
Sparky.src('assets/**/**.*').dest(isProduction ? 'dist/' : 'dist/static')
|
||||
);
|
||||
Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
|
||||
fuse.dev();
|
||||
app.hmr().watch();
|
||||
|
|
18
ui/package.json
vendored
18
ui/package.json
vendored
|
@ -15,33 +15,32 @@
|
|||
"@types/autosize": "^3.0.6",
|
||||
"@types/js-cookie": "^2.2.1",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/markdown-it": "^0.0.7",
|
||||
"@types/markdown-it": "^0.0.9",
|
||||
"@types/markdown-it-container": "^2.0.2",
|
||||
"autosize": "^4.0.2",
|
||||
"bootswatch": "^4.3.1",
|
||||
"classcat": "^1.1.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"emoji-short-name": "^0.1.0",
|
||||
"husky": "^3.0.9",
|
||||
"i18next": "^17.0.9",
|
||||
"i18next": "^19.0.3",
|
||||
"inferno": "^7.0.1",
|
||||
"inferno-i18next": "nimbusec-oss/inferno-i18next",
|
||||
"inferno-router": "^7.0.1",
|
||||
"js-cookie": "^2.2.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it": "^10.0.0",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"moment": "^2.24.0",
|
||||
"prettier": "^1.18.2",
|
||||
"rxjs": "^6.4.0",
|
||||
"terser": "^3.17.0",
|
||||
"tributejs": "3.7.2",
|
||||
"terser": "^4.6.0",
|
||||
"tributejs": "^4.1.1",
|
||||
"twemoji": "^12.1.2",
|
||||
"ws": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/i18next": "^12.1.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-plugin-inferno": "^7.14.3",
|
||||
"eslint-plugin-jane": "^7.0.0",
|
||||
|
@ -58,7 +57,7 @@
|
|||
"engineStrict": true,
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
"pre-commit": "cargo fmt --manifest-path ../server/Cargo.toml && cargo clippy --manifest-path ../server/Cargo.toml --all-targets --all-features -- -D warnings && lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
|
@ -67,6 +66,9 @@
|
|||
"eslint --fix",
|
||||
"git add"
|
||||
],
|
||||
"../server/src/**/*.rs": [
|
||||
"git add"
|
||||
],
|
||||
"package.json": [
|
||||
"sortpack",
|
||||
"git add"
|
||||
|
|
4
ui/src/components/comment-form.tsx
vendored
4
ui/src/components/comment-form.tsx
vendored
|
@ -18,11 +18,11 @@ import {
|
|||
markdownHelpUrl,
|
||||
} from '../utils';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import * as autosize from 'autosize';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import * as emojiShortName from 'emoji-short-name';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
|
||||
interface CommentFormProps {
|
||||
postId?: number;
|
||||
|
|
172
ui/src/components/comment-node.tsx
vendored
172
ui/src/components/comment-node.tsx
vendored
|
@ -17,8 +17,15 @@ import {
|
|||
BanType,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, getUnixTime, canMod, isMod } from '../utils';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
mdToHtml,
|
||||
getUnixTime,
|
||||
canMod,
|
||||
isMod,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import moment from 'moment';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
|
@ -36,6 +43,8 @@ interface CommentNodeState {
|
|||
banType: BanType;
|
||||
showConfirmTransferSite: boolean;
|
||||
showConfirmTransferCommunity: boolean;
|
||||
showConfirmAppointAsMod: boolean;
|
||||
showConfirmAppointAsAdmin: boolean;
|
||||
collapsed: boolean;
|
||||
viewSource: boolean;
|
||||
}
|
||||
|
@ -65,6 +74,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
viewSource: false,
|
||||
showConfirmTransferSite: false,
|
||||
showConfirmTransferCommunity: false,
|
||||
showConfirmAppointAsMod: false,
|
||||
showConfirmAppointAsAdmin: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -128,7 +139,15 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
className="text-info"
|
||||
to={`/u/${node.comment.creator_name}`}
|
||||
>
|
||||
{node.comment.creator_name}
|
||||
{node.comment.creator_avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(node.comment.creator_avatar)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{node.comment.creator_name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
{this.isMod && (
|
||||
|
@ -192,6 +211,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">
|
||||
|
@ -232,13 +263,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>
|
||||
|
@ -254,6 +307,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
</span>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{/* Mods can ban from community, and appoint as mods to community */}
|
||||
{this.canMod && (
|
||||
|
@ -285,17 +339,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>
|
||||
)}
|
||||
</>
|
||||
|
@ -367,14 +447,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>
|
||||
)}
|
||||
</>
|
||||
|
@ -418,34 +524,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>
|
||||
)}
|
||||
|
@ -711,13 +789,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);
|
||||
}
|
||||
|
@ -770,6 +848,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,
|
||||
|
@ -777,6 +865,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);
|
||||
}
|
||||
|
||||
|
@ -786,6 +885,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
added: !i.isAdmin,
|
||||
};
|
||||
WebSocketService.Instance.addAdmin(form);
|
||||
i.state.showConfirmAppointAsAdmin = false;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
|
|
10
ui/src/components/community-form.tsx
vendored
10
ui/src/components/community-form.tsx
vendored
|
@ -7,6 +7,7 @@ import {
|
|||
Category,
|
||||
ListCategoriesResponse,
|
||||
CommunityResponse,
|
||||
GetSiteResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { msgOp, capitalizeFirstLetter } from '../utils';
|
||||
|
@ -27,6 +28,7 @@ interface CommunityFormState {
|
|||
communityForm: CommunityFormI;
|
||||
categories: Array<Category>;
|
||||
loading: boolean;
|
||||
enable_nsfw: boolean;
|
||||
}
|
||||
|
||||
export class CommunityForm extends Component<
|
||||
|
@ -44,6 +46,7 @@ export class CommunityForm extends Component<
|
|||
},
|
||||
categories: [],
|
||||
loading: false,
|
||||
enable_nsfw: null,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -79,6 +82,7 @@ export class CommunityForm extends Component<
|
|||
);
|
||||
|
||||
WebSocketService.Instance.listCategories();
|
||||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -157,7 +161,7 @@ export class CommunityForm extends Component<
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{WebSocketService.Instance.site.enable_nsfw && (
|
||||
{this.state.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
|
@ -267,6 +271,10 @@ export class CommunityForm extends Component<
|
|||
let res: CommunityResponse = msg;
|
||||
this.state.loading = false;
|
||||
this.props.onEdit(res.community);
|
||||
} else if (op == UserOperation.GetSite) {
|
||||
let res: GetSiteResponse = msg;
|
||||
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||
this.setState(this.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">
|
||||
|
|
23
ui/src/components/login.tsx
vendored
23
ui/src/components/login.tsx
vendored
|
@ -7,6 +7,7 @@ import {
|
|||
LoginResponse,
|
||||
UserOperation,
|
||||
PasswordResetForm,
|
||||
GetSiteResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { msgOp, validEmail } from '../utils';
|
||||
|
@ -18,6 +19,7 @@ interface State {
|
|||
registerForm: RegisterForm;
|
||||
loginLoading: boolean;
|
||||
registerLoading: boolean;
|
||||
enable_nsfw: boolean;
|
||||
}
|
||||
|
||||
export class Login extends Component<any, State> {
|
||||
|
@ -37,6 +39,7 @@ export class Login extends Component<any, State> {
|
|||
},
|
||||
loginLoading: false,
|
||||
registerLoading: false,
|
||||
enable_nsfw: undefined,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -58,18 +61,14 @@ export class Login extends Component<any, State> {
|
|||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
|
||||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('login')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
|
@ -205,7 +204,7 @@ export class Login extends Component<any, State> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{WebSocketService.Instance.site.enable_nsfw && (
|
||||
{this.state.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
|
@ -271,6 +270,9 @@ export class Login extends Component<any, State> {
|
|||
|
||||
handleRegisterEmailChange(i: Login, event: any) {
|
||||
i.state.registerForm.email = event.target.value;
|
||||
if (i.state.registerForm.email == '') {
|
||||
i.state.registerForm.email = undefined;
|
||||
}
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
|
@ -319,6 +321,13 @@ export class Login extends Component<any, State> {
|
|||
this.props.history.push('/communities');
|
||||
} else if (op == UserOperation.PasswordReset) {
|
||||
alert(i18n.t('reset_password_mail_sent'));
|
||||
} else if (op == UserOperation.GetSite) {
|
||||
let res: GetSiteResponse = msg;
|
||||
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||
this.setState(this.state);
|
||||
document.title = `${i18n.t('login')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
ui/src/components/main.tsx
vendored
15
ui/src/components/main.tsx
vendored
|
@ -31,6 +31,8 @@ import {
|
|||
routeSortTypeToEnum,
|
||||
routeListingTypeToEnum,
|
||||
postRefetchSeconds,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -65,6 +67,9 @@ export class Main extends Component<any, MainState> {
|
|||
number_of_posts: null,
|
||||
number_of_comments: null,
|
||||
number_of_communities: null,
|
||||
enable_downvotes: null,
|
||||
open_registration: null,
|
||||
enable_nsfw: null,
|
||||
},
|
||||
admins: [],
|
||||
banned: [],
|
||||
|
@ -341,7 +346,15 @@ export class Main extends Component<any, MainState> {
|
|||
{this.state.site.admins.map(admin => (
|
||||
<li class="list-inline-item">
|
||||
<Link class="text-info" to={`/u/${admin.name}`}>
|
||||
{admin.name}
|
||||
{admin.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(admin.avatar)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{admin.name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
|
14
ui/src/components/navbar.tsx
vendored
14
ui/src/components/navbar.tsx
vendored
|
@ -13,7 +13,7 @@ import {
|
|||
GetSiteResponse,
|
||||
Comment,
|
||||
} from '../interfaces';
|
||||
import { msgOp } from '../utils';
|
||||
import { msgOp, pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||
import { version } from '../version';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -151,7 +151,19 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
class="nav-link"
|
||||
to={`/u/${UserService.Instance.user.username}`}
|
||||
>
|
||||
<span>
|
||||
{UserService.Instance.user.avatar && showAvatars() && (
|
||||
<img
|
||||
src={pictshareAvatarThumbnail(
|
||||
UserService.Instance.user.avatar
|
||||
)}
|
||||
height="32"
|
||||
width="32"
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
{UserService.Instance.user.username}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
|
|
79
ui/src/components/post-form.tsx
vendored
79
ui/src/components/post-form.tsx
vendored
|
@ -15,6 +15,7 @@ import {
|
|||
SearchForm,
|
||||
SearchType,
|
||||
SearchResponse,
|
||||
GetSiteResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
|
@ -26,8 +27,9 @@ import {
|
|||
archiveUrl,
|
||||
mdToHtml,
|
||||
debounce,
|
||||
isImage,
|
||||
} from '../utils';
|
||||
import * as autosize from 'autosize';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -48,6 +50,7 @@ interface PostFormState {
|
|||
suggestedTitle: string;
|
||||
suggestedPosts: Array<Post>;
|
||||
crossPosts: Array<Post>;
|
||||
enable_nsfw: boolean;
|
||||
}
|
||||
|
||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
|
@ -69,10 +72,13 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
suggestedTitle: undefined,
|
||||
suggestedPosts: [],
|
||||
crossPosts: [],
|
||||
enable_nsfw: undefined,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
|
@ -80,7 +86,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.state.postForm = {
|
||||
body: this.props.post.body,
|
||||
// NOTE: debouncing breaks both these for some reason, unless you use defaultValue
|
||||
name: undefined,
|
||||
name: this.props.post.name,
|
||||
community_id: this.props.post.community_id,
|
||||
edit_id: this.props.post.id,
|
||||
creator_id: this.props.post.creator_id,
|
||||
|
@ -101,14 +107,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
|
@ -121,6 +120,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
};
|
||||
|
||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||
WebSocketService.Instance.getSite();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -143,7 +143,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
defaultValue={this.state.postForm.url}
|
||||
value={this.state.postForm.url}
|
||||
onInput={linkEvent(this, this.handlePostUrlChange)}
|
||||
/>
|
||||
{this.state.suggestedTitle && (
|
||||
|
@ -193,6 +193,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
)}
|
||||
{isImage(this.state.postForm.url) && (
|
||||
<img src={this.state.postForm.url} class="img-fluid" />
|
||||
)}
|
||||
{this.state.crossPosts.length > 0 && (
|
||||
<>
|
||||
<div class="my-1 text-muted small font-weight-bold">
|
||||
|
@ -209,10 +212,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
defaultValue={
|
||||
this.props.post ? this.props.post.name : undefined
|
||||
}
|
||||
/* This needs to be undefined for some weird reason */
|
||||
value={this.state.postForm.name}
|
||||
onInput={linkEvent(this, this.handlePostNameChange)}
|
||||
class="form-control"
|
||||
|
@ -285,7 +284,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{WebSocketService.Instance.site.enable_nsfw && (
|
||||
{this.state.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
|
@ -348,11 +347,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handlePostUrlChange = debounce((i: PostForm, event: any) => {
|
||||
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,37 +366,40 @@ 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 = debounce((i: PostForm, event: any) => {
|
||||
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) {
|
||||
i.state.postForm.body = event.target.value;
|
||||
|
@ -488,6 +495,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.state.crossPosts = res.posts;
|
||||
}
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.GetSite) {
|
||||
let res: GetSiteResponse = msg;
|
||||
this.state.enable_nsfw = res.site.enable_nsfw;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
15
ui/src/components/post-listing.tsx
vendored
15
ui/src/components/post-listing.tsx
vendored
|
@ -25,6 +25,9 @@ import {
|
|||
isImage,
|
||||
isVideo,
|
||||
getUnixTime,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
imageThumbnailer,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -135,7 +138,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
>
|
||||
<img
|
||||
class="mx-2 mt-1 float-left img-fluid thumbnail rounded"
|
||||
src={post.url}
|
||||
src={imageThumbnailer(post.url)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
@ -248,7 +251,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
<li className="list-inline-item">
|
||||
<span>{i18n.t('by')} </span>
|
||||
<Link className="text-info" to={`/u/${post.creator_name}`}>
|
||||
{post.creator_name}
|
||||
{post.creator_avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(post.creator_avatar)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{post.creator_name}</span>
|
||||
</Link>
|
||||
{this.isMod && (
|
||||
<span className="mx-1 badge badge-light">
|
||||
|
|
12
ui/src/components/post.tsx
vendored
12
ui/src/components/post.tsx
vendored
|
@ -34,7 +34,7 @@ import { PostListings } from './post-listings';
|
|||
import { Sidebar } from './sidebar';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import * as autosize from 'autosize';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -76,14 +76,7 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(
|
||||
retryWhen(errors =>
|
||||
errors.pipe(
|
||||
delay(3000),
|
||||
take(10)
|
||||
)
|
||||
)
|
||||
)
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
|
@ -169,7 +162,6 @@ export class Post extends Component<any, PostState> {
|
|||
post={this.state.post}
|
||||
showBody
|
||||
showCommunity
|
||||
editable
|
||||
moderators={this.state.moderators}
|
||||
admins={this.state.admins}
|
||||
/>
|
||||
|
|
16
ui/src/components/search.tsx
vendored
16
ui/src/components/search.tsx
vendored
|
@ -19,6 +19,8 @@ import {
|
|||
fetchLimit,
|
||||
routeSearchTypeToEnum,
|
||||
routeSortTypeToEnum,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
|
@ -286,7 +288,19 @@ export class Search extends Component<any, SearchState> {
|
|||
<Link
|
||||
className="text-info"
|
||||
to={`/u/${(i.data as UserView).name}`}
|
||||
>{`/u/${(i.data as UserView).name}`}</Link>
|
||||
>
|
||||
{(i.data as UserView).avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
(i.data as UserView).avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{`/u/${(i.data as UserView).name}`}</span>
|
||||
</Link>
|
||||
</span>
|
||||
<span>{` - ${
|
||||
(i.data as UserView).comment_score
|
||||
|
|
17
ui/src/components/sidebar.tsx
vendored
17
ui/src/components/sidebar.tsx
vendored
|
@ -8,7 +8,12 @@ import {
|
|||
UserView,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, getUnixTime } from '../utils';
|
||||
import {
|
||||
mdToHtml,
|
||||
getUnixTime,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -194,7 +199,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
{this.props.moderators.map(mod => (
|
||||
<li class="list-inline-item">
|
||||
<Link class="text-info" to={`/u/${mod.user_name}`}>
|
||||
{mod.user_name}
|
||||
{mod.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(mod.avatar)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{mod.user_name}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
|
2
ui/src/components/site-form.tsx
vendored
2
ui/src/components/site-form.tsx
vendored
|
@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno';
|
|||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { capitalizeFirstLetter } from '../utils';
|
||||
import * as autosize from 'autosize';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue