diff --git a/.dockerignore b/.dockerignore index 03466f0a3..73c475542 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ ui/node_modules ui/dist server/target -docs .git diff --git a/.travis.yml b/.travis.yml index dfdcbf7e3..0f180dba5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,12 @@ before_cache: before_script: - psql -c "create user lemmy with password 'password' superuser;" -U postgres - psql -c 'create database lemmy with owner lemmy;' -U postgres + - rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu before_install: - cd server script: + # Default checks, but fail if anything is detected + - cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf - cargo build - diesel migration run - cargo test diff --git a/README.md b/README.md index 5407ac0bf..3778d3d6d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Github](https://img.shields.io/badge/-Github-blue)](https://github.com/dessalines/lemmy) [![Gitlab](https://img.shields.io/badge/-Gitlab-yellowgreen)](https://gitlab.com/dessalines/lemmy) -![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social) +[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev) ![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social) [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg) @@ -36,31 +36,17 @@ Front Page|Post ---|--- ![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png) -## 📝 Table of Contents +[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). - +For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere. -- [Features](#features) -- [About](#about) - * [Why's it called Lemmy?](#whys-it-called-lemmy) -- [Install](#install) - * [Docker](#docker) - + [Updating](#updating) - * [Ansible](#ansible) - * [Kubernetes](#kubernetes) -- [Develop](#develop) - * [Docker Development](#docker-development) - * [Local Development](#local-development) - + [Requirements](#requirements) - + [Set up Postgres DB](#set-up-postgres-db) - + [Running](#running) -- [Configuration](#configuration) -- [Documentation](#documentation) -- [Support](#support) -- [Translations](#translations) -- [Credits](#credits) +The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling. - +Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing. + +Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). + +[Documentation](https://dev.lemmy.ml/docs/index.html) ## Features @@ -91,25 +77,13 @@ Front Page|Post - Front end is `~80kB` gzipped. - Supports arm64 / Raspberry Pi. -## About - -[Lemmy](https://github.com/dessalines/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). - -For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere. - -The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling. - -Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing. - -### Why's it called Lemmy? +## Why's it called Lemmy? - Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U). - The old school [video game](). - The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). - The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). -Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://www.infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). - ## Install ### Docker @@ -121,7 +95,7 @@ mkdir lemmy/ cd lemmy/ wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson -# Edit the .env if you want custom passwords +# Edit lemmy.hjson to do more configuration docker-compose up -d ``` @@ -157,88 +131,6 @@ nano inventory # enter your server, domain, contact email ansible-playbook lemmy.yml --become ``` -### Kubernetes - -You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/). -Setting this up will vary depending on your provider. -To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/). - -Once you have a working cluster, edit the environment variables and volume sizes in `docker/k8s/*.yml`. -You may also want to change the service types to use `LoadBalancer`s depending on where you're running your cluster (add `type: LoadBalancer` to `ports)`, or `NodePort`s. -By default they will use `ClusterIP`s, which will allow access only within the cluster. See the [docs](https://kubernetes.io/docs/concepts/services-networking/service/) for more on networking in Kubernetes. - -**Important** Running a database in Kubernetes will work, but is generally not recommended. -If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.). - -Now you can deploy: - -```bash -# Add `-n foo` if you want to deploy into a specific namespace `foo`; -# otherwise your resources will be created in the `default` namespace. -kubectl apply -f docker/k8s/db.yml -kubectl apply -f docker/k8s/pictshare.yml -kubectl apply -f docker/k8s/lemmy.yml -``` - -If you used a `LoadBalancer`, you should see it in your cloud provider's console. - -## Develop - -### Docker Development - -Run: - -```bash -git clone https://github.com/dessalines/lemmy -cd lemmy/docker/dev -./docker_update.sh # This builds and runs it, updating for your changes -``` - -and go to http://localhost:8536. - -### Local Development - -#### Requirements - -- [Rust](https://www.rust-lang.org/) -- [Yarn](https://yarnpkg.com/en/) -- [Postgres](https://www.postgresql.org/) - -#### Set up Postgres DB - -```bash - psql -c "create user lemmy with password 'password' superuser;" -U postgres - psql -c 'create database lemmy with owner lemmy;' -U postgres - export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy -``` - -#### Running - -```bash -git clone https://github.com/dessalines/lemmy -cd lemmy -./install.sh -# For live coding, where both the front and back end, automagically reload on any save, do: -# cd ui && yarn start -# cd server && cargo watch -x run -``` - -## Configuration - -The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file. - -Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with -`LEMMY__DATABASE__POOL_SIZE=10`. - -An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once. - -## Documentation - -- [Websocket API for App developers](docs/api.md) -- [ActivityPub API.md](docs/apub_api_outline.md) -- [Goals](docs/goals.md) -- [Ranking Algorithm](docs/ranking.md) - ## Support Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. @@ -257,16 +149,15 @@ If you'd like to add translations, take a look a look at the [English translatio lang | done | missing --- | --- | --- -de | 97% | avatar,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw -eo | 84% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no -es | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw -fr | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw -it | 93% | avatar,archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw -nl | 86% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme -ru | 80% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no -sv | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw -zh | 78% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no - +de | 96% | avatar,docs,old_password,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw +eo | 83% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no +es | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw +fr | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw +it | 92% | avatar,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw +nl | 85% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme +ru | 79% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no +sv | 91% | avatar,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw +zh | 77% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no If you'd like to update this report, run: diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index d62e7b279..761d8cc3c 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -32,6 +32,14 @@ RUN cargo build --frozen --release # Get diesel-cli on there just in case # RUN cargo install diesel_cli --no-default-features --features postgres + +FROM ekidd/rust-musl-builder:1.38.0-openssl11 as docs +WORKDIR /app +COPY docs ./docs +RUN sudo chown -R rust:rust . +RUN mdbook build docs/ + + FROM alpine:3.10 # Install libpq for postgres @@ -40,6 +48,7 @@ RUN apk add libpq # Copy resources COPY server/config/defaults.hjson /config/defaults.hjson COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy +COPY --from=docs /app/docs/book/ /app/dist/documentation/ COPY --from=node /app/ui/dist /app/dist RUN addgroup -g 1000 lemmy diff --git a/docker/dev/deploy.sh b/docker/dev/deploy.sh index ba3675f33..8a709dcd9 100755 --- a/docker/dev/deploy.sh +++ b/docker/dev/deploy.sh @@ -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 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index d013b0d70..12d274d46 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -11,7 +11,7 @@ services: - lemmy_db:/var/lib/postgresql/data restart: always lemmy: - image: dessalines/lemmy:v0.5.9 + image: dessalines/lemmy:v0.5.14 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 000000000..55cce8c03 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Felix Ableitner"] +language = "en" +multilingual = false +src = "src" +title = "Lemmy Documentation" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 000000000..d89169746 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -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) diff --git a/docs/src/about.md b/docs/src/about.md new file mode 100644 index 000000000..71b397412 --- /dev/null +++ b/docs/src/about.md @@ -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](). +- 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/). diff --git a/docs/src/about_features.md b/docs/src/about_features.md new file mode 100644 index 000000000..5c70c978e --- /dev/null +++ b/docs/src/about_features.md @@ -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. diff --git a/docs/goals.md b/docs/src/about_goals.md similarity index 100% rename from docs/goals.md rename to docs/src/about_goals.md diff --git a/docs/ranking.md b/docs/src/about_ranking.md similarity index 100% rename from docs/ranking.md rename to docs/src/about_ranking.md diff --git a/docs/src/administration.md b/docs/src/administration.md new file mode 100644 index 000000000..c4c2b01f1 --- /dev/null +++ b/docs/src/administration.md @@ -0,0 +1 @@ +Information for Lemmy instance admins, and those who want to start an instance. \ No newline at end of file diff --git a/docs/src/administration_configuration.md b/docs/src/administration_configuration.md new file mode 100644 index 000000000..73ea35042 --- /dev/null +++ b/docs/src/administration_configuration.md @@ -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. diff --git a/docs/src/administration_install_ansible.md b/docs/src/administration_install_ansible.md new file mode 100644 index 000000000..03642b897 --- /dev/null +++ b/docs/src/administration_install_ansible.md @@ -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 +``` diff --git a/docs/src/administration_install_docker.md b/docs/src/administration_install_docker.md new file mode 100644 index 000000000..64abe737e --- /dev/null +++ b/docs/src/administration_install_docker.md @@ -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 +``` diff --git a/docs/src/administration_install_kubernetes.md b/docs/src/administration_install_kubernetes.md new file mode 100644 index 000000000..886558dce --- /dev/null +++ b/docs/src/administration_install_kubernetes.md @@ -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. diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 000000000..4f29af3da --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1 @@ +Information about contributing to Lemmy, whether it is translating, testing, designing or programming. \ No newline at end of file diff --git a/docs/apub_api_outline.md b/docs/src/contributing_apub_api_outline.md similarity index 100% rename from docs/apub_api_outline.md rename to docs/src/contributing_apub_api_outline.md diff --git a/docs/src/contributing_docker_development.md b/docs/src/contributing_docker_development.md new file mode 100644 index 000000000..0ed5bde5e --- /dev/null +++ b/docs/src/contributing_docker_development.md @@ -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). \ No newline at end of file diff --git a/docs/src/contributing_local_development.md b/docs/src/contributing_local_development.md new file mode 100644 index 000000000..a681eeb0d --- /dev/null +++ b/docs/src/contributing_local_development.md @@ -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 +``` diff --git a/docs/api.md b/docs/src/contributing_websocket_api.md similarity index 100% rename from docs/api.md rename to docs/src/contributing_websocket_api.md diff --git a/server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql b/server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql new file mode 100644 index 000000000..92f771f84 --- /dev/null +++ b/server/migrations/2020-01-01-200418_add_email_to_user_view/down.sql @@ -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; diff --git a/server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql b/server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql new file mode 100644 index 000000000..59972dfb8 --- /dev/null +++ b/server/migrations/2020-01-01-200418_add_email_to_user_view/up.sql @@ -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; diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 9a057f806..ed658985c 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -51,7 +51,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -59,12 +59,12 @@ impl Perform for Oper { // Check for a community ban let post = Post::read(&conn, data.post_id)?; if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban"))?; + return Err(APIError::err(&self.op, "community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban"))?; + return Err(APIError::err(&self.op, "site_ban").into()); } let content_slurs_removed = remove_slurs(&data.content.to_owned()); @@ -82,14 +82,14 @@ impl Perform for Oper { let inserted_comment = match Comment::create(&conn, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()), }; // Scan the comment for user mentions, add those rows let extracted_usernames = extract_usernames(&comment_form.content); for username_mention in &extracted_usernames { - let mention_user = User_::read_from_name(&conn, username_mention.to_string()); + let mention_user = User_::read_from_name(&conn, (*username_mention).to_string()); if mention_user.is_ok() { let mention_user_id = mention_user?.id; @@ -124,7 +124,7 @@ impl Perform for Oper { let _inserted_like = match CommentLike::like(&conn, &like_form) { Ok(like) => like, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()), }; let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; @@ -143,7 +143,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -163,17 +163,17 @@ impl Perform for Oper { editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect()); if !editors.contains(&user_id) { - return Err(APIError::err(&self.op, "no_comment_edit_allowed"))?; + return Err(APIError::err(&self.op, "no_comment_edit_allowed").into()); } // Check for a community ban if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban"))?; + return Err(APIError::err(&self.op, "community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban"))?; + return Err(APIError::err(&self.op, "site_ban").into()); } } @@ -196,14 +196,14 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), }; // Scan the comment for user mentions, add those rows let extracted_usernames = extract_usernames(&comment_form.content); for username_mention in &extracted_usernames { - let mention_user = User_::read_from_name(&conn, username_mention.to_string()); + let mention_user = User_::read_from_name(&conn, (*username_mention).to_string()); if mention_user.is_ok() { let mention_user_id = mention_user?.id; @@ -255,7 +255,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -268,12 +268,12 @@ impl Perform for Oper { if data.save { match CommentSaved::save(&conn, &comment_saved_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()), }; } else { match CommentSaved::unsave(&conn, &comment_saved_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()), }; } @@ -293,7 +293,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -301,20 +301,20 @@ impl Perform for Oper { // Don't do a downvote if site has downvotes disabled if data.score == -1 { let site = SiteView::read(&conn)?; - if site.enable_downvotes == false { - return Err(APIError::err(&self.op, "downvotes_disabled"))?; + if !site.enable_downvotes { + return Err(APIError::err(&self.op, "downvotes_disabled").into()); } } // Check for a community ban let post = Post::read(&conn, data.post_id)?; if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { - return Err(APIError::err(&self.op, "community_ban"))?; + return Err(APIError::err(&self.op, "community_ban").into()); } // Check for a site ban if UserView::read(&conn, user_id)?.banned { - return Err(APIError::err(&self.op, "site_ban"))?; + return Err(APIError::err(&self.op, "site_ban").into()); } let like_form = CommentLikeForm { @@ -332,7 +332,7 @@ impl Perform for Oper { 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()), }; } diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 014fa4c90..2edaa0b24 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -136,21 +136,24 @@ impl Perform for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { ); 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { let communities: Vec = 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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::>() - .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 for Oper { 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 for Oper { 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 for Oper { 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 diff --git a/server/src/api/post.rs b/server/src/api/post.rs index 4b2395a8a..5bc31defe 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -93,23 +93,23 @@ impl Perform for Oper { 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 for Oper { 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 for Oper { // 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 for Oper { 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 for Oper { .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 for Oper { 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 for Oper { // 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 for Oper { 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 for Oper { fn perform(&self) -> Result { 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 for Oper { ); 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 for Oper { 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 for Oper { 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 for Oper { 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()), }; } diff --git a/server/src/api/site.rs b/server/src/api/site.rs index ec89e46cd..58c34e8fa 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -160,16 +160,15 @@ impl Perform for Oper { )?; // 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { 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 for Oper { // 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 for Oper { 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 diff --git a/server/src/api/user.rs b/server/src/api/user.rs index e8ad20aa4..912587da3 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -28,6 +28,10 @@ pub struct SaveUserSettings { default_listing_type: i16, lang: String, avatar: Option, + email: Option, + new_password: Option, + new_password_verify: Option, + old_password: Option, auth: String, } @@ -168,18 +172,13 @@ impl Perform for Oper { // Fetch that username / email let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err( - &self.op, - "couldnt_find_that_username_or_email", - ))? - } + Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()), }; // Verify the password let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "password_incorrect"))?; + return Err(APIError::err(&self.op, "password_incorrect").into()); } // Return the jwt @@ -198,22 +197,22 @@ impl Perform for Oper { // Make sure site has open registration if let Ok(site) = SiteView::read(&conn) { if !site.open_registration { - return Err(APIError::err(&self.op, "registration_closed"))?; + return Err(APIError::err(&self.op, "registration_closed").into()); } } // Make sure passwords match - if &data.password != &data.password_verify { - return Err(APIError::err(&self.op, "passwords_dont_match"))?; + if data.password != data.password_verify { + return Err(APIError::err(&self.op, "passwords_dont_match").into()); } if has_slurs(&data.username) { - return Err(APIError::err(&self.op, "no_slurs"))?; + return Err(APIError::err(&self.op, "no_slurs").into()); } // Make sure there are no admins - if data.admin && UserView::admins(&conn)?.len() > 0 { - return Err(APIError::err(&self.op, "admin_already_created"))?; + if data.admin && !UserView::admins(&conn)?.is_empty() { + return Err(APIError::err(&self.op, "admin_already_created").into()); } // Register the new user @@ -237,7 +236,7 @@ impl Perform for Oper { // Create the user let inserted_user = match User_::register(&conn, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "user_already_exists"))?, + Err(_e) => return Err(APIError::err(&self.op, "user_already_exists").into()), }; // Create the main community if it doesn't exist @@ -268,7 +267,7 @@ impl Perform for Oper { let _inserted_community_follower = match CommunityFollower::follow(&conn, &community_follower_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists"))?, + Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()), }; // If its an admin, add them as a mod and follower to main @@ -282,10 +281,7 @@ impl Perform for Oper { match CommunityModerator::join(&conn, &community_moderator_form) { Ok(user) => user, Err(_e) => { - return Err(APIError::err( - &self.op, - "community_moderator_already_exists", - ))? + return Err(APIError::err(&self.op, "community_moderator_already_exists").into()) } }; } @@ -305,19 +301,52 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; let read_user = User_::read(&conn, user_id)?; + let email = match &data.email { + Some(email) => Some(email.to_owned()), + None => read_user.email, + }; + + let password_encrypted = match &data.new_password { + Some(new_password) => { + match &data.new_password_verify { + Some(new_password_verify) => { + // Make sure passwords match + if new_password != new_password_verify { + return Err(APIError::err(&self.op, "passwords_dont_match").into()); + } + + // Check the old password + match &data.old_password { + Some(old_password) => { + let valid: bool = + verify(old_password, &read_user.password_encrypted).unwrap_or(false); + if !valid { + return Err(APIError::err(&self.op, "password_incorrect").into()); + } + User_::update_password(&conn, user_id, &new_password)?.password_encrypted + } + None => return Err(APIError::err(&self.op, "password_incorrect").into()), + } + } + None => return Err(APIError::err(&self.op, "passwords_dont_match").into()), + } + } + None => read_user.password_encrypted, + }; + let user_form = UserForm { name: read_user.name, fedi_name: read_user.fedi_name, - email: read_user.email, + email, avatar: data.avatar.to_owned(), - password_encrypted: read_user.password_encrypted, + password_encrypted, preferred_username: read_user.preferred_username, updated: Some(naive_now()), admin: read_user.admin, @@ -331,7 +360,7 @@ impl Perform for Oper { let updated_user = match User_::update(&conn, user_id, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), }; // Return the jwt @@ -372,14 +401,14 @@ impl Perform for Oper { None => { match User_::read_from_name( &conn, - data.username.to_owned().unwrap_or("admin".to_string()), + data + .username + .to_owned() + .unwrap_or_else(|| "admin".to_string()), ) { Ok(user) => user.id, Err(_e) => { - return Err(APIError::err( - &self.op, - "couldnt_find_that_username_or_email", - ))? + return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()) } } } @@ -441,14 +470,14 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; // Make sure user is an admin - if UserView::read(&conn, user_id)?.admin == false { - return Err(APIError::err(&self.op, "not_an_admin"))?; + if !UserView::read(&conn, user_id)?.admin { + return Err(APIError::err(&self.op, "not_an_admin").into()); } let read_user = User_::read(&conn, data.user_id)?; @@ -472,7 +501,7 @@ impl Perform for Oper { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), }; // Mod tables @@ -504,14 +533,14 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; // Make sure user is an admin - if UserView::read(&conn, user_id)?.admin == false { - return Err(APIError::err(&self.op, "not_an_admin"))?; + if !UserView::read(&conn, user_id)?.admin { + return Err(APIError::err(&self.op, "not_an_admin").into()); } let read_user = User_::read(&conn, data.user_id)?; @@ -535,7 +564,7 @@ impl Perform for Oper { match User_::update(&conn, data.user_id, &user_form) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), }; // Mod tables @@ -571,7 +600,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -599,7 +628,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -627,7 +656,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -643,7 +672,7 @@ impl Perform for Oper { let _updated_user_mention = match UserMention::update(&conn, user_mention.id, &user_mention_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), }; let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?; @@ -662,7 +691,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -687,7 +716,7 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), }; } @@ -708,7 +737,7 @@ impl Perform for Oper { let _updated_mention = match UserMention::update(&conn, mention.user_mention_id, &mention_form) { Ok(mention) => mention, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), }; } @@ -726,7 +755,7 @@ impl Perform for Oper { let claims = match Claims::decode(&data.auth) { Ok(claims) => claims.claims, - Err(_e) => return Err(APIError::err(&self.op, "not_logged_in"))?, + Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()), }; let user_id = claims.id; @@ -736,7 +765,7 @@ impl Perform for Oper { // Verify the password let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); if !valid { - return Err(APIError::err(&self.op, "password_incorrect"))?; + return Err(APIError::err(&self.op, "password_incorrect").into()); } // Comments @@ -759,7 +788,7 @@ impl Perform for Oper { let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) { Ok(comment) => comment, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()), }; } @@ -787,7 +816,7 @@ impl Perform for Oper { let _updated_post = match Post::update(&conn, post.id, &post_form) { Ok(post) => post, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()), }; } @@ -806,12 +835,7 @@ impl Perform for Oper { // Fetch that email let user: User_ = match User_::find_by_email(&conn, &data.email) { Ok(user) => user, - Err(_e) => { - return Err(APIError::err( - &self.op, - "couldnt_find_that_username_or_email", - ))? - } + Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()), }; // Generate a random token @@ -828,7 +852,7 @@ impl Perform for Oper { let html = &format!("

Password Reset Request for {}


Click here to reset your password", user.name, hostname, &token); match send_email(subject, user_email, &user.name, html) { Ok(_o) => _o, - Err(_e) => return Err(APIError::err(&self.op, &_e.to_string()))?, + Err(_e) => return Err(APIError::err(&self.op, &_e).into()), }; Ok(PasswordResetResponse { @@ -846,34 +870,14 @@ impl Perform for Oper { let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id; // Make sure passwords match - if &data.password != &data.password_verify { - return Err(APIError::err(&self.op, "passwords_dont_match"))?; + if data.password != data.password_verify { + return Err(APIError::err(&self.op, "passwords_dont_match").into()); } - // Fetch the user - let read_user = User_::read(&conn, user_id)?; - // Update the user with the new password - let user_form = UserForm { - name: read_user.name, - fedi_name: read_user.fedi_name, - email: read_user.email, - avatar: read_user.avatar, - password_encrypted: data.password.to_owned(), - preferred_username: read_user.preferred_username, - updated: Some(naive_now()), - admin: read_user.admin, - banned: read_user.banned, - show_nsfw: read_user.show_nsfw, - theme: read_user.theme, - default_sort_type: read_user.default_sort_type, - default_listing_type: read_user.default_listing_type, - lang: read_user.lang, - }; - - let updated_user = match User_::update_password(&conn, user_id, &user_form) { + let updated_user = match User_::update_password(&conn, user_id, &data.password) { Ok(user) => user, - Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?, + Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()), }; // Return the jwt diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 3de68b967..504e17907 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -60,10 +60,7 @@ impl Community { let mut collection = UnorderedCollection::default(); collection.object_props.set_context_object(context()).ok(); - collection - .object_props - .set_id_string(base_url.to_string()) - .ok(); + collection.object_props.set_id_string(base_url).ok(); let connection = establish_connection(); //As we are an object, we validated that the community id was valid diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 19163657b..ebb171290 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -9,7 +9,7 @@ impl Post { let mut page = Page::default(); page.object_props.set_context_object(context()).ok(); - page.object_props.set_id_string(base_url.to_string()).ok(); + page.object_props.set_id_string(base_url).ok(); page.object_props.set_name_string(self.name.to_owned()).ok(); if let Some(body) = &self.body { diff --git a/server/src/apub/puller.rs b/server/src/apub/puller.rs index efca6c7b1..fe6f492ae 100644 --- a/server/src/apub/puller.rs +++ b/server/src/apub/puller.rs @@ -55,11 +55,12 @@ pub fn get_remote_community(identifier: String) -> Result CommunityQueryBuilder<'a> { self } - pub fn from_user_id>(mut self, from_user_id: T) -> Self { + pub fn for_user>(mut self, from_user_id: T) -> Self { self.from_user_id = from_user_id.get_optional(); self } diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index 7824580d5..fe6cb3ce4 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -101,13 +101,13 @@ pub trait MaybeOptional { impl MaybeOptional for T { fn get_optional(self) -> Option { - return Some(self); + Some(self) } } impl MaybeOptional for Option { fn get_optional(self) -> Option { - return self; + self } } @@ -118,12 +118,12 @@ lazy_static! { Pool::builder() .max_size(Settings::get().database.pool_size) .build(manager) - .expect(&format!("Error connecting to {}", db_url)) + .unwrap_or_else(|_| panic!("Error connecting to {}", db_url)) }; } pub fn establish_connection() -> PooledConnection> { - return PG_POOL.get().unwrap(); + PG_POOL.get().unwrap() } #[derive(EnumString, ToString, Debug, Serialize, Deserialize)] diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index d05eeccad..22e2eb14e 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -189,12 +189,9 @@ impl<'a> PostQueryBuilder<'a> { let mut query = self.query; - match self.listing_type { - ListingType::Subscribed => { - query = query.filter(subscribed.eq(true)); - } - _ => {} - }; + if let ListingType::Subscribed = self.listing_type { + query = query.filter(subscribed.eq(true)); + } query = match self.sort { SortType::Hot => query diff --git a/server/src/db/user.rs b/server/src/db/user.rs index db4aa453c..82736f8e2 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -75,14 +75,13 @@ impl User_ { pub fn update_password( conn: &PgConnection, user_id: i32, - form: &UserForm, + new_password: &str, ) -> Result { - 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::(conn) } pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result { diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs index 616159de5..2d50b40c4 100644 --- a/server/src/db/user_view.rs +++ b/server/src/db/user_view.rs @@ -7,6 +7,7 @@ table! { id -> Int4, name -> Varchar, avatar -> Nullable, + email -> Nullable, fedi_name -> Varchar, admin -> Bool, banned -> Bool, @@ -26,6 +27,7 @@ pub struct UserView { pub id: i32, pub name: String, pub avatar: Option, + pub email: Option, pub fedi_name: String, pub admin: bool, pub banned: bool, diff --git a/server/src/lib.rs b/server/src/lib.rs index cddd5b860..e23ec4ba1 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -25,12 +25,10 @@ pub extern crate strum; pub mod api; pub mod apub; pub mod db; -pub mod feeds; -pub mod nodeinfo; +pub mod routes; pub mod schema; pub mod settings; pub mod version; -pub mod webfinger; pub mod websocket; use crate::settings::Settings; diff --git a/server/src/main.rs b/server/src/main.rs index d7002359c..ab079703b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,294 +2,40 @@ extern crate lemmy_server; #[macro_use] extern crate diesel_migrations; -use actix::prelude::*; -use actix_files::NamedFile; -use actix_web::web::Query; use actix_web::*; -use actix_web_actors::ws; -use lemmy_server::api::community::ListCommunities; -use lemmy_server::api::Oper; -use lemmy_server::api::Perform; -use lemmy_server::api::UserOperation; -use lemmy_server::apub; use lemmy_server::db::establish_connection; -use lemmy_server::feeds; -use lemmy_server::nodeinfo; +use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket}; use lemmy_server::settings::Settings; -use lemmy_server::webfinger; -use lemmy_server::websocket::server::*; -use std::time::{Duration, Instant}; embed_migrations!(); -/// How often heartbeat pings are sent -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -/// How long before lack of client response causes a timeout -const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); - -/// Entry point for our route -fn chat_route( - req: HttpRequest, - stream: web::Payload, - chat_server: web::Data>, -) -> Result { - 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, - /// 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; - - /// 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 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 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) { - ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { - // check client heartbeats - if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { - // heartbeat timed out - println!("Websocket Client heartbeat failed, disconnecting!"); - - // notify chat server - act.cs_addr.do_send(Disconnect { - id: act.id, - ip: act.ip.to_owned(), - }); - - // stop actor - ctx.stop(); - - // don't try to send a ping - return; - } - - ctx.ping(""); - }); - } -} - fn main() { - let _ = env_logger::init(); + env_logger::init(); let sys = actix::System::new("lemmy"); // Run the migrations from code let conn = establish_connection(); embedded_migrations::run(&conn).unwrap(); - // Start chat server actor in separate thread - let server = ChatServer::default().start(); - let settings = Settings::get(); // Create Http server with websocket support HttpServer::new(move || { - let app = App::new() - .data(server.clone()) - // Front end routes + App::new() + .configure(federation::config) + .configure(feeds::config) + .configure(index::config) + .configure(nodeinfo::config) + .configure(webfinger::config) + .configure(websocket::config) .service(actix_files::Files::new( "/static", settings.front_end_dir.to_owned(), )) - .route("/", web::get().to(index)) - .route( - "/home/type/{type}/sort/{sort}/page/{page}", - web::get().to(index), - ) - .route("/login", web::get().to(index)) - .route("/create_post", web::get().to(index)) - .route("/create_community", web::get().to(index)) - .route("/communities/page/{page}", web::get().to(index)) - .route("/communities", web::get().to(index)) - .route("/post/{id}/comment/{id2}", web::get().to(index)) - .route("/post/{id}", web::get().to(index)) - .route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index)) - .route("/c/{name}", web::get().to(index)) - .route("/community/{id}", web::get().to(index)) - .route( - "/u/{username}/view/{view}/sort/{sort}/page/{page}", - web::get().to(index), - ) - .route("/u/{username}", web::get().to(index)) - .route("/user/{id}", web::get().to(index)) - .route("/inbox", web::get().to(index)) - .route("/modlog/community/{community_id}", web::get().to(index)) - .route("/modlog", web::get().to(index)) - .route("/setup", web::get().to(index)) - .route( - "/search/q/{q}/type/{type}/sort/{sort}/page/{page}", - web::get().to(index), - ) - .route("/search", web::get().to(index)) - .route("/sponsors", web::get().to(index)) - .route("/password_change/{token}", web::get().to(index)) - // Websocket - .service(web::resource("/api/v1/ws").to(chat_route)) - // NodeInfo - .route("/nodeinfo/2.0.json", web::get().to(nodeinfo::node_info)) - .route( - "/.well-known/nodeinfo", - web::get().to(nodeinfo::node_info_well_known), - ) - // RSS - .route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed)) - .route("/feeds/all.xml", web::get().to(feeds::get_all_feed)) - // Federation - .route( - "/federation/c/{community_name}", - web::get().to(apub::community::get_apub_community), - ) - .route( - "/federation/c/{community_name}/followers", - web::get().to(apub::community::get_apub_community_followers), - ) - .route( - "/federation/u/{user_name}", - web::get().to(apub::user::get_apub_user), - ) - .route("/feeds/all.xml", web::get().to(feeds::get_all_feed)); - - // Federation - if Settings::get().federation_enabled { - println!("federation enabled, host is {}", Settings::get().hostname); - app - .route( - ".well-known/webfinger", - web::get().to(webfinger::get_webfinger_response), - ) - // TODO: this is a very quick and dirty implementation for http api calls - .route( - "/api/v1/communities/list", - web::get().to(|query: Query| { - let res = Oper::new(UserOperation::ListCommunities, query.into_inner()) - .perform() - .unwrap(); - HttpResponse::Ok() - .content_type("application/json") - .body(serde_json::to_string(&res).unwrap()) - }), - ) - } else { - app - } + .service(actix_files::Files::new( + "/docs", + settings.front_end_dir.to_owned() + "/documentation", + )) }) .bind((settings.bind, settings.port)) .unwrap() @@ -299,9 +45,3 @@ fn main() { let _ = sys.run(); } - -fn index() -> Result { - Ok(NamedFile::open( - Settings::get().front_end_dir.to_owned() + "/index.html", - )?) -} diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs new file mode 100644 index 000000000..019a9a71b --- /dev/null +++ b/server/src/routes/federation.rs @@ -0,0 +1,38 @@ +use crate::api::community::ListCommunities; +use crate::api::Perform; +use crate::api::{Oper, UserOperation}; +use crate::apub; +use crate::settings::Settings; +use actix_web::web::Query; +use actix_web::{web, HttpResponse}; + +pub fn config(cfg: &mut web::ServiceConfig) { + if Settings::get().federation_enabled { + println!("federation enabled, host is {}", Settings::get().hostname); + cfg + .route( + "/federation/c/{community_name}", + web::get().to(apub::community::get_apub_community), + ) + .route( + "/federation/c/{community_name}/followers", + web::get().to(apub::community::get_apub_community_followers), + ) + .route( + "/federation/u/{user_name}", + web::get().to(apub::user::get_apub_user), + ) + // TODO: this is a very quick and dirty implementation for http api calls + .route( + "/api/v1/communities/list", + web::get().to(|query: Query| { + let res = Oper::new(UserOperation::ListCommunities, query.into_inner()) + .perform() + .unwrap(); + HttpResponse::Ok() + .content_type("application/json") + .body(serde_json::to_string(&res).unwrap()) + }), + ); + } +} diff --git a/server/src/feeds.rs b/server/src/routes/feeds.rs similarity index 93% rename from server/src/feeds.rs rename to server/src/routes/feeds.rs index c624bcc53..0b2ccac19 100644 --- a/server/src/feeds.rs +++ b/server/src/routes/feeds.rs @@ -5,12 +5,13 @@ use crate::db::comment_view::{ReplyQueryBuilder, ReplyView}; use crate::db::community::Community; use crate::db::post_view::{PostQueryBuilder, PostView}; use crate::db::site_view::SiteView; -use crate::db::user::User_; +use crate::db::user::{Claims, User_}; use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView}; use crate::db::{establish_connection, ListingType, SortType}; use crate::Settings; use actix_web::body::Body; use actix_web::{web, HttpResponse, Result}; +use chrono::{DateTime, Utc}; use failure::Error; use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder}; use serde::Deserialize; @@ -29,7 +30,14 @@ enum RequestType { Inbox, } -pub fn get_all_feed(info: web::Query) -> HttpResponse { +pub fn config(cfg: &mut web::ServiceConfig) { + cfg + .route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed)) + .route("/feeds/all.xml", web::get().to(feeds::get_all_feed)) + .route("/feeds/all.xml", web::get().to(feeds::get_all_feed)); +} + +fn get_all_feed(info: web::Query) -> HttpResponse { let sort_type = match get_sort_type(info) { Ok(sort_type) => sort_type, Err(_) => return HttpResponse::BadRequest().finish(), @@ -45,7 +53,7 @@ pub fn get_all_feed(info: web::Query) -> HttpResponse { } } -pub fn get_feed(path: web::Path<(String, String)>, info: web::Query) -> HttpResponse { +fn get_feed(path: web::Path<(String, String)>, info: web::Query) -> HttpResponse { let sort_type = match get_sort_type(info) { Ok(sort_type) => sort_type, Err(_) => return HttpResponse::BadRequest().finish(), @@ -77,7 +85,10 @@ pub fn get_feed(path: web::Path<(String, String)>, info: web::Query) -> } fn get_sort_type(info: web::Query) -> Result { - let sort_query = info.sort.to_owned().unwrap_or(SortType::Hot.to_string()); + let sort_query = info + .sort + .to_owned() + .unwrap_or_else(|| SortType::Hot.to_string()); SortType::from_str(&sort_query) } @@ -162,7 +173,7 @@ fn get_feed_front(sort_type: &SortType, jwt: String) -> Result { let conn = establish_connection(); let site_view = SiteView::read(&conn)?; - let user_id = db::user::Claims::decode(&jwt)?.claims.id; + let user_id = Claims::decode(&jwt)?.claims.id; let posts = PostQueryBuilder::create(&conn) .listing_type(ListingType::Subscribed) @@ -189,7 +200,7 @@ fn get_feed_inbox(jwt: String) -> Result { 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; diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs new file mode 100644 index 000000000..cd10a2b7a --- /dev/null +++ b/server/src/routes/index.rs @@ -0,0 +1,45 @@ +use crate::settings::Settings; +use actix_files::NamedFile; +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg + .route("/", web::get().to(index)) + .route( + "/home/type/{type}/sort/{sort}/page/{page}", + web::get().to(index), + ) + .route("/login", web::get().to(index)) + .route("/create_post", web::get().to(index)) + .route("/create_community", web::get().to(index)) + .route("/communities/page/{page}", web::get().to(index)) + .route("/communities", web::get().to(index)) + .route("/post/{id}/comment/{id2}", web::get().to(index)) + .route("/post/{id}", web::get().to(index)) + .route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index)) + .route("/c/{name}", web::get().to(index)) + .route("/community/{id}", web::get().to(index)) + .route( + "/u/{username}/view/{view}/sort/{sort}/page/{page}", + web::get().to(index), + ) + .route("/u/{username}", web::get().to(index)) + .route("/user/{id}", web::get().to(index)) + .route("/inbox", web::get().to(index)) + .route("/modlog/community/{community_id}", web::get().to(index)) + .route("/modlog", web::get().to(index)) + .route("/setup", web::get().to(index)) + .route( + "/search/q/{q}/type/{type}/sort/{sort}/page/{page}", + web::get().to(index), + ) + .route("/search", web::get().to(index)) + .route("/sponsors", web::get().to(index)) + .route("/password_change/{token}", web::get().to(index)); +} + +fn index() -> Result { + Ok(NamedFile::open( + Settings::get().front_end_dir.to_owned() + "/index.html", + )?) +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs new file mode 100644 index 000000000..6556c8d58 --- /dev/null +++ b/server/src/routes/mod.rs @@ -0,0 +1,6 @@ +pub mod federation; +pub mod feeds; +pub mod index; +pub mod nodeinfo; +pub mod webfinger; +pub mod websocket; diff --git a/server/src/nodeinfo.rs b/server/src/routes/nodeinfo.rs similarity index 74% rename from server/src/nodeinfo.rs rename to server/src/routes/nodeinfo.rs index 65bd93700..246596083 100644 --- a/server/src/nodeinfo.rs +++ b/server/src/routes/nodeinfo.rs @@ -3,9 +3,16 @@ use crate::db::site_view::SiteView; use crate::version; use crate::Settings; use actix_web::body::Body; +use actix_web::web; use actix_web::HttpResponse; use serde_json::json; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg + .route("/nodeinfo/2.0.json", web::get().to(node_info)) + .route("/.well-known/nodeinfo", web::get().to(node_info_well_known)); +} + pub fn node_info_well_known() -> HttpResponse { let json = json!({ "links": { @@ -14,12 +21,12 @@ pub fn node_info_well_known() -> HttpResponse { } }); - return HttpResponse::Ok() + HttpResponse::Ok() .content_type("application/json") - .body(json.to_string()); + .body(json.to_string()) } -pub fn node_info() -> HttpResponse { +fn node_info() -> HttpResponse { let conn = establish_connection(); let site_view = match SiteView::read(&conn) { Ok(site_view) => site_view, @@ -43,10 +50,10 @@ pub fn node_info() -> HttpResponse { }, "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()) } diff --git a/server/src/webfinger.rs b/server/src/routes/webfinger.rs similarity index 90% rename from server/src/webfinger.rs rename to server/src/routes/webfinger.rs index 03d2fafb3..014eb2bcc 100644 --- a/server/src/webfinger.rs +++ b/server/src/routes/webfinger.rs @@ -2,6 +2,7 @@ use crate::db::community::Community; use crate::db::establish_connection; use crate::Settings; use actix_web::body::Body; +use actix_web::web; use actix_web::web::Query; use actix_web::HttpResponse; use regex::Regex; @@ -13,6 +14,15 @@ pub struct Params { resource: String, } +pub fn config(cfg: &mut web::ServiceConfig) { + if Settings::get().federation_enabled { + cfg.route( + ".well-known/webfinger", + web::get().to(get_webfinger_response), + ); + } +} + lazy_static! { static ref WEBFINGER_COMMUNITY_REGEX: Regex = Regex::new(&format!( "^group:([a-z0-9_]{{3, 20}})@{}$", @@ -27,7 +37,7 @@ lazy_static! { /// /// You can also view the webfinger response that Mastodon sends: /// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town -pub fn get_webfinger_response(info: Query) -> HttpResponse { +fn get_webfinger_response(info: Query) -> HttpResponse { let regex_parsed = WEBFINGER_COMMUNITY_REGEX .captures(&info.resource) .map(|c| c.get(1)); diff --git a/server/src/routes/websocket.rs b/server/src/routes/websocket.rs new file mode 100644 index 000000000..8ae3552c6 --- /dev/null +++ b/server/src/routes/websocket.rs @@ -0,0 +1,179 @@ +use crate::websocket::server::*; +use actix::prelude::*; +use actix_web::web; +use actix_web::*; +use actix_web_actors::ws; +use std::time::{Duration, Instant}; + +pub fn config(cfg: &mut web::ServiceConfig) { + // Start chat server actor in separate thread + let server = ChatServer::default().start(); + cfg + .data(server) + .service(web::resource("/api/v1/ws").to(chat_route)); +} + +/// How often heartbeat pings are sent +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +/// How long before lack of client response causes a timeout +const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); + +/// Entry point for our route +fn chat_route( + req: HttpRequest, + stream: web::Payload, + chat_server: web::Data>, +) -> Result { + 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, + /// 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; + + /// 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 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 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) { + 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(""); + }); + } +} diff --git a/server/src/settings.rs b/server/src/settings.rs index 2f635b935..82d7e1428 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -51,10 +51,10 @@ pub struct Database { lazy_static! { static ref SETTINGS: Settings = { - return match Settings::init() { + match Settings::init() { Ok(c) => c, Err(e) => panic!("{}", e), - }; + } }; } @@ -77,7 +77,7 @@ impl Settings { // https://github.com/mehcode/config-rs/issues/73 s.merge(Environment::with_prefix("LEMMY").separator("__"))?; - return s.try_into(); + s.try_into() } /// Returns the config as a struct. diff --git a/server/src/version.rs b/server/src/version.rs index dc45321db..29f4246e6 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &'static str = "v0.5.9"; +pub const VERSION: &str = "v0.5.14"; diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 344324cbc..febd51fd4 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -92,7 +92,7 @@ impl Default for ChatServer { ChatServer { sessions: HashMap::new(), rate_limits: HashMap::new(), - rooms: rooms, + rooms, rng: rand::thread_rng(), } } @@ -100,8 +100,8 @@ impl Default for ChatServer { impl ChatServer { /// Send message to all users in the room - fn send_room_message(&self, room: &i32, message: &str, skip_id: usize) { - if let Some(sessions) = self.rooms.get(room) { + fn send_room_message(&self, room: i32, message: &str, skip_id: usize) { + if let Some(sessions) = self.rooms.get(&room) { for id in sessions { if *id != skip_id { if let Some(info) = self.sessions.get(id) { @@ -114,7 +114,7 @@ impl ChatServer { fn join_room(&mut self, room_id: i32, id: usize) { // remove session from all rooms - for (_n, sessions) in &mut self.rooms { + for sessions in self.rooms.values_mut() { sessions.remove(&id); } @@ -123,12 +123,12 @@ impl ChatServer { self.rooms.insert(room_id, HashSet::new()); } - &self.rooms.get_mut(&room_id).unwrap().insert(id); + self.rooms.get_mut(&room_id).unwrap().insert(id); } fn send_community_message( &self, - community_id: &i32, + community_id: i32, message: &str, skip_id: usize, ) -> Result<(), Error> { @@ -139,12 +139,12 @@ impl ChatServer { let posts = PostQueryBuilder::create(&conn) .listing_type(ListingType::Community) .sort(&SortType::New) - .for_community_id(*community_id) + .for_community_id(community_id) .limit(9999) .list()?; for post in posts { - self.send_room_message(&post.id, message, skip_id); + self.send_room_message(post.id, message, skip_id); } Ok(()) @@ -174,6 +174,7 @@ impl ChatServer { ) } + #[allow(clippy::float_cmp)] fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> { if let Some(info) = self.sessions.get(&id) { if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) { @@ -195,10 +196,13 @@ impl ChatServer { "Rate limited IP: {}, time_passed: {}, allowance: {}", &info.ip, time_passed, rate_limit.allowance ); - Err(APIError { - op: "Rate Limit".to_string(), - message: format!("Too many requests. {} per {} seconds", rate, per), - })? + Err( + APIError { + op: "Rate Limit".to_string(), + message: format!("Too many requests. {} per {} seconds", rate, per), + } + .into(), + ) } else { rate_limit.allowance -= 1.0; Ok(()) @@ -265,7 +269,7 @@ impl Handler for ChatServer { // remove address if self.sessions.remove(&msg.id).is_some() { // remove session from all rooms - for (_id, sessions) in &mut self.rooms { + for sessions in self.rooms.values_mut() { if sessions.remove(&msg.id) { // rooms.push(*id); } @@ -293,7 +297,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result Result { @@ -414,7 +418,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { @@ -422,7 +426,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { @@ -459,7 +463,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { @@ -476,7 +480,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { @@ -487,7 +491,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { @@ -504,7 +508,7 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 34ec0dfbb..b3ca682b2 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -42,6 +42,8 @@ interface CommentNodeState { banType: BanType; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; + showConfirmAppointAsMod: boolean; + showConfirmAppointAsAdmin: boolean; collapsed: boolean; viewSource: boolean; } @@ -71,6 +73,8 @@ export class CommentNode extends Component { viewSource: false, showConfirmTransferSite: false, showConfirmTransferCommunity: false, + showConfirmAppointAsMod: false, + showConfirmAppointAsAdmin: false, }; constructor(props: any, context: any) { @@ -206,6 +210,18 @@ export class CommentNode extends Component { /> )}
    + {this.props.markable && ( +
  • + + {node.comment.read + ? i18n.t('mark_as_unread') + : i18n.t('mark_as_read')} + +
  • + )} {UserService.Instance.user && !this.props.viewOnly && ( <>
  • @@ -246,28 +262,51 @@ export class CommentNode extends Component {
  • )} +
  • •
  • +
  • + + # + +
  • +
  • + + # + +
  • {/* Admins and mods can remove comments */} {(this.canMod || this.canAdmin) && ( -
  • - {!node.comment.removed ? ( - - # - - ) : ( - - # - - )} -
  • + <> +
  • •
  • +
  • + {!node.comment.removed ? ( + + # + + ) : ( + + # + + )} +
  • + )} {/* Mods can ban from community, and appoint as mods to community */} {this.canMod && ( @@ -299,17 +338,43 @@ export class CommentNode extends Component { )} {!node.comment.banned_from_community && (
  • - - {this.isMod - ? i18n.t('remove_as_mod') - : i18n.t('appoint_as_mod')} - + {!this.state.showConfirmAppointAsMod ? ( + + {this.isMod + ? i18n.t('remove_as_mod') + : i18n.t('appoint_as_mod')} + + ) : ( + <> + + # + + + # + + + # + + + )}
  • )} @@ -381,14 +446,40 @@ export class CommentNode extends Component { )} {!node.comment.banned && (
  • - - {this.isAdmin - ? i18n.t('remove_as_admin') - : i18n.t('appoint_as_admin')} - + {!this.state.showConfirmAppointAsAdmin ? ( + + {this.isAdmin + ? i18n.t('remove_as_admin') + : i18n.t('appoint_as_admin')} + + ) : ( + <> + + # + + + # + + + # + + + )}
  • )} @@ -432,34 +523,6 @@ export class CommentNode extends Component { )} )} -
  • - - # - -
  • -
  • - - # - -
  • - {this.props.markable && ( -
  • - - {node.comment.read - ? i18n.t('mark_as_unread') - : i18n.t('mark_as_read')} - -
  • - )}
)} @@ -725,13 +788,13 @@ export class CommentNode extends Component { } handleModBanFromCommunityShow(i: CommentNode) { - i.state.showBanDialog = true; + i.state.showBanDialog = !i.state.showBanDialog; i.state.banType = BanType.Community; i.setState(i.state); } handleModBanShow(i: CommentNode) { - i.state.showBanDialog = true; + i.state.showBanDialog = !i.state.showBanDialog; i.state.banType = BanType.Site; i.setState(i.state); } @@ -784,6 +847,16 @@ export class CommentNode extends Component { i.setState(i.state); } + handleShowConfirmAppointAsMod(i: CommentNode) { + i.state.showConfirmAppointAsMod = true; + i.setState(i.state); + } + + handleCancelConfirmAppointAsMod(i: CommentNode) { + i.state.showConfirmAppointAsMod = false; + i.setState(i.state); + } + handleAddModToCommunity(i: CommentNode) { let form: AddModToCommunityForm = { user_id: i.props.node.comment.creator_id, @@ -791,6 +864,17 @@ export class CommentNode extends Component { added: !i.isMod, }; WebSocketService.Instance.addModToCommunity(form); + i.state.showConfirmAppointAsMod = false; + i.setState(i.state); + } + + handleShowConfirmAppointAsAdmin(i: CommentNode) { + i.state.showConfirmAppointAsAdmin = true; + i.setState(i.state); + } + + handleCancelConfirmAppointAsAdmin(i: CommentNode) { + i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } @@ -800,6 +884,7 @@ export class CommentNode extends Component { added: !i.isAdmin, }; WebSocketService.Instance.addAdmin(form); + i.state.showConfirmAppointAsAdmin = false; i.setState(i.state); } diff --git a/ui/src/components/footer.tsx b/ui/src/components/footer.tsx index 237830dd9..8aa05072d 100644 --- a/ui/src/components/footer.tsx +++ b/ui/src/components/footer.tsx @@ -23,8 +23,8 @@ export class Footer extends Component {