Merge branch 'dev' into federation

This commit is contained in:
Dessalines 2020-02-07 12:34:14 -05:00
commit 8c2e3ea227
36 changed files with 581 additions and 170 deletions

1
.github/FUNDING.yml vendored
View file

@ -1,3 +1,4 @@
# These are supported funding model platforms # These are supported funding model platforms
patreon: dessalines patreon: dessalines
liberapay: Lemmy

159
README.md vendored
View file

@ -1,34 +1,35 @@
<p align="center">
<a href="https://dev.lemmy.ml/" rel="noopener">
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
</p>
<h3 align="center">Lemmy</h3>
<div align="center"> <div align="center">
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
[![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg) ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy) [![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/lemmy)
[![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues) [![GitHub issues](https://img.shields.io/github/issues-raw/dessalines/lemmy.svg)](https://github.com/dessalines/lemmy/issues)
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/) [![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dessalines/lemmy.svg)
![GitHub repo size](https://img.shields.io/github/repo-size/dessalines/lemmy.svg)
[![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE) [![License](https://img.shields.io/github/license/dessalines/lemmy.svg)](LICENSE)
[![Patreon](https://img.shields.io/badge/-Support%20on%20Patreon-blueviolet.svg)](https://www.patreon.com/dessalines) ![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
</div> </div>
--- <p align="center">
<a href="https://dev.lemmy.ml/" rel="noopener">
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
<p align="center">A link aggregator / reddit clone for the fediverse. <h3 align="center"><a href="https://dev.lemmy.ml">Lemmy</a></h3>
<br> <p align="center">
A link aggregator / reddit clone for the fediverse.
<br />
<br />
<a href="https://dev.lemmy.ml">View Site</a>
·
<a href="https://dev.lemmy.ml/docs/index.html">Documentation</a>
·
<a href="https://github.com/dessalines/lemmy/issues">Report Bug</a>
·
<a href="https://github.com/dessalines/lemmy/issues">Request Feature</a>
·
<a href="https://github.com/dessalines/lemmy/blob/master/RELEASES.md">Releases</a>
</p>
</p> </p>
[Lemmy Dev instance](https://dev.lemmy.ml) *This data is being backed up, and once federation is working, it will be the basis for a main instance.* ## About The Project
This is a **very early beta version**, and a lot of features are currently broken or in active development, such as federation.
Front Page|Post Front Page|Post
---|--- ---|---
@ -42,17 +43,22 @@ The overall goal is to create an easily self-hostable, decentralized alternative
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. 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://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). *Note: Federation is still in active development*
- [Documentation](https://dev.lemmy.ml/docs/index.html) ### Why's it called Lemmy?
- [Releases / Changelog](/RELEASES.md)
- [Contributing](https://dev.lemmy.ml/docs/contributing.html)
## Repository Mirrors - Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
- [GitHub](https://github.com/dessalines/lemmy) ### Built With
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy) - [Rust](https://www.rust-lang.org)
- [Actix](https://actix.rs/)
- [Diesel](http://diesel.rs/)
- [Inferno](https://infernojs.org)
- [Typescript](https://www.typescriptlang.org/)
## Features ## Features
@ -89,60 +95,11 @@ Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Infern
- Front end is `~80kB` gzipped. - Front end is `~80kB` gzipped.
- Supports arm64 / Raspberry Pi. - Supports arm64 / Raspberry Pi.
## Why's it called Lemmy? ## Installation
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
## Install
### Docker
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) (Image uploading won't work without this), 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
```
### Ansible
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
```
- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
## Support / Donate ## Support / Donate
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
@ -157,7 +114,13 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01` - ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV` - monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
## Translations ## Contributing
- [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html)
- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
- [Local Development](https://dev.lemmy.ml/docs/contributing_local_development.html)
### Translations
If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts). If you'd like to add translations, take a look at the [English translation file](ui/src/translations/en.ts).
@ -167,19 +130,19 @@ If you'd like to add translations, take a look at the [English translation file]
lang | done | missing lang | done | missing
---- | ---- | ------- ---- | ---- | -------
ca | 98% | cross_posted_to,old,support_on_liberapay,time,action ca | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
fa | 72% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
eo | 74% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action eo | 74% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
es | 99% | cross_posted_to,support_on_liberapay es | 99% | cross_posted_to,post_title_too_long
fi | 98% | cross_posted_to,old,support_on_liberapay,time,action fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
fr | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action fr | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
nl | 98% | cross_posted_to,support_on_liberapay,time,action nl | 98% | cross_posted_to,post_title_too_long,time,action
pt-br | 100% | support_on_liberapay pt-br | 100% | post_title_too_long
ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
<!-- translationsstop --> <!-- translationsstop -->
If you'd like to update this report, run: If you'd like to update this report, run:
@ -189,6 +152,14 @@ cd ui
ts-node translation_report.ts ts-node translation_report.ts
``` ```
## Contact
- [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
- [GitHub](https://github.com/dessalines/lemmy)
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy)
## Credits ## Credits
Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license. Logo made by Andy Cuccaro (@andycuccaro) under the CC-BY-SA 4.0 license.

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.6.11 v0.6.13

2
docker/lemmy.hjson vendored
View file

@ -29,7 +29,7 @@
# rate limits for various user actions, by user ip # rate limits for various user actions, by user ip
rate_limit: { rate_limit: {
# maximum number of messages created in interval # maximum number of messages created in interval
message: 30 message: 180
# interval length for message limit # interval length for message limit
message_per_second: 60 message_per_second: 60
# maximum number of posts created in interval # maximum number of posts created in interval

View file

@ -11,7 +11,7 @@ services:
- lemmy_db:/var/lib/postgresql/data - lemmy_db:/var/lib/postgresql/data
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.6.11 image: dessalines/lemmy:v0.6.13
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always

16
docs/src/about.md vendored
View file

@ -1,6 +1,8 @@
# Lemmy - A link aggregator / reddit clone for the fediverse. ## About The Project
[Lemmy Dev instance](https://dev.lemmy.ml) *for testing purposes only* Front Page|Post
---|---
![main screen](https://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
[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). [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).
@ -10,6 +12,8 @@ The overall goal is to create an easily self-hostable, decentralized alternative
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. 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.
*Note: Federation is still in active development*
### Why's it called Lemmy? ### Why's it called Lemmy?
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U). - Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
@ -17,4 +21,10 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa). - The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/). - The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
Made with [Rust](https://www.rust-lang.org), [Actix](https://actix.rs/), [Inferno](https://infernojs.org), [Typescript](https://www.typescriptlang.org/) and [Diesel](http://diesel.rs/). ### Built With
- [Rust](https://www.rust-lang.org)
- [Actix](https://actix.rs/)
- [Diesel](http://diesel.rs/)
- [Inferno](https://infernojs.org)
- [Typescript](https://www.typescriptlang.org/)

View file

@ -1,20 +1,27 @@
# Features # Features
- Open source, [AGPL License](/LICENSE). - Open source, [AGPL License](/LICENSE).
- Self hostable, easy to deploy. - Self hostable, easy to deploy.
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes). - Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
- Clean, mobile-friendly interface. - Clean, mobile-friendly interface.
- Only a minimum of a username and password is required to sign up!
- User avatar support.
- Live-updating Comment threads. - Live-updating Comment threads.
- Full vote scores `(+/-)` like old reddit. - Full vote scores `(+/-)` like old reddit.
- Themes, including light, dark, and solarized. - Themes, including light, dark, and solarized.
- Emojis with autocomplete support. Start typing `:` - Emojis with autocomplete support. Start typing `:`
- User tagging using `@`, Community tagging using `#`. - User tagging using `@`, Community tagging using `#`.
- Integrated image uploading in both posts and comments.
- A post can consist of a title and any combination of self text, a URL, or nothing else.
- Notifications, on comment replies and when you're tagged. - Notifications, on comment replies and when you're tagged.
- Notifications can be sent via email.
- i18n / internationalization support. - i18n / internationalization support.
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`. - RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
- Cross-posting support. - Cross-posting support.
- A *similar post search* when creating new posts. Great for question / answer communities. - A *similar post search* when creating new posts. Great for question / answer communities.
- Moderation abilities. - Moderation abilities.
- Public Moderation Logs. - Public Moderation Logs.
- Can sticky posts to the top of communities.
- Both site admins, and community moderators, who can appoint other moderators. - Both site admins, and community moderators, who can appoint other moderators.
- Can lock, remove, and restore posts and comments. - Can lock, remove, and restore posts and comments.
- Can ban and unban users from communities and the site. - Can ban and unban users from communities and the site.

View file

@ -1 +1,3 @@
Information for Lemmy instance admins, and those who want to start an instance. # Admin info
Information for Lemmy instance admins, and those who want to start an instance.

View file

@ -1,6 +1,15 @@
# 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. 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 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`. `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. 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.
If the Docker container is not used, manually create the database specified above by running the following commands:
```bash
cd server
./db-init.sh
```

View file

@ -1,3 +1,5 @@
# Ansible Installation
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. 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: Then run the following commands on your local computer:

View file

@ -1,3 +1,5 @@
# Docker Installation
Make sure you have both docker and docker-compose(>=`1.24.0`) installed: Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
```bash ```bash
@ -5,20 +7,20 @@ mkdir lemmy/
cd lemmy/ cd lemmy/
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
# Edit lemmy.hjson to do more configuration # Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
docker-compose up -d docker-compose up -d
``` ```
and go to http://localhost:8536. and go to http://localhost:8536.
[A sample nginx config](/ansible/templates/nginx.conf), could be setup with: [A sample nginx config](/ansible/templates/nginx.conf) (Note: Avatar / Image uploading won't work without this), could be setup with:
```bash ```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }} # Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
``` ```
#### Updating ## Updating
To update to the newest version, run: To update to the newest version, run:

View file

@ -1,3 +1,5 @@
# Kubernetes Installation
You'll need to have an existing Kubernetes cluster and [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/). 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. 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/). To try it locally, you can use [MicroK8s](https://microk8s.io/) or [Minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/).

View file

@ -1,4 +1,6 @@
Run: # Docker Development
## Running
```bash ```bash
git clone https://github.com/dessalines/lemmy git clone https://github.com/dessalines/lemmy
@ -8,4 +10,4 @@ cd lemmy/docker/dev
and go to http://localhost:8536. 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). Note that compile times when changing `Cargo.toml` are relatively long with Docker, because builds can't be incrementally cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).

View file

@ -7,9 +7,16 @@
#### Set up Postgres DB #### Set up Postgres DB
```bash ```bash
psql -c "create user lemmy with password 'password' superuser;" -U postgres cd server
psql -c 'create database lemmy with owner lemmy;' -U postgres ./db-init.sh
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy ```
Or run the commands manually:
```bash
psql -c "create user lemmy with password 'password' superuser;" -U postgres
psql -c 'create database lemmy with owner lemmy;' -U postgres
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
``` ```
#### Running #### Running

View file

@ -1,4 +1,5 @@
# Lemmy API # Lemmy API
*Note: this may lag behind the actual API endpoints [here](../server/src/api).* *Note: this may lag behind the actual API endpoints [here](../server/src/api).*
<!-- toc --> <!-- toc -->

View file

@ -32,7 +32,7 @@
# rate limits for various user actions, by user ip # rate limits for various user actions, by user ip
rate_limit: { rate_limit: {
# maximum number of messages created in interval # maximum number of messages created in interval
message: 30 message: 180
# interval length for message limit # interval length for message limit
message_per_second: 60 message_per_second: 60
# maximum number of posts created in interval # maximum number of posts created in interval

43
server/db-init.sh vendored Normal file
View file

@ -0,0 +1,43 @@
#!/bin/bash
username=lemmy
dbname=lemmy
port=5432
password=""
password_confirm=""
password_valid=0
while [ "$password_valid" == 0 ]
do
read -p "Enter database password: " -s password
echo
read -p "Verify database password: " -s password_confirm
echo
echo
# Start the loop from the top if either check fails
if [ -z "$password" ]
then
echo "Error: Password cannot be empty." 1>&2
echo
continue
fi
if [ "$password" != "$password_confirm" ]
then
echo "Error: Passwords don't match." 1>&2
echo
continue
fi
# Set the password_valid variable to break out of the loop
password_valid=1
done
psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres
psql -c 'CREATE DATABASE $dbname WITH OWNER $username;' -U postgres
export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname
echo $LEMMY_DATABASE_URL

View file

@ -0,0 +1,132 @@
-- Drop the dependent views
drop view post_view;
drop view post_mview;
drop materialized view post_aggregates_mview;
drop view post_aggregates_view;
drop view mod_remove_post_view;
drop view mod_sticky_post_view;
drop view mod_lock_post_view;
drop view mod_remove_comment_view;
alter table post alter column name type varchar(100);
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
from post p
left join post_like pl on p.id = pl.post_id
group by p.id;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
-- The mod views
create view mod_remove_post_view as
select mrp.*,
(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mrp.post_id = p.id) as post_name,
(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
from mod_remove_post mrp;
create view mod_lock_post_view as
select mlp.*,
(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mlp.post_id = p.id) as post_name,
(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
from mod_lock_post mlp;
create view mod_remove_comment_view as
select mrc.*,
(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
(select content from comment c where mrc.comment_id = c.id) as comment_content,
(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id,
(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
from mod_remove_comment mrc;
create view mod_sticky_post_view as
select msp.*,
(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name,
(select name from post p where msp.post_id = p.id) as post_name,
(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name
from mod_sticky_post msp;

View file

@ -0,0 +1,133 @@
-- Drop the dependent views
drop view post_view;
drop view post_mview;
drop materialized view post_aggregates_mview;
drop view post_aggregates_view;
drop view mod_remove_post_view;
drop view mod_sticky_post_view;
drop view mod_lock_post_view;
drop view mod_remove_comment_view;
-- Add the extra post limit
alter table post alter column name type varchar(200);
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
from post p
left join post_like pl on p.id = pl.post_id
group by p.id;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
-- The mod views
create view mod_remove_post_view as
select mrp.*,
(select name from user_ u where mrp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mrp.post_id = p.id) as post_name,
(select c.id from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mrp.post_id = p.id and p.community_id = c.id) as community_name
from mod_remove_post mrp;
create view mod_lock_post_view as
select mlp.*,
(select name from user_ u where mlp.mod_user_id = u.id) as mod_user_name,
(select name from post p where mlp.post_id = p.id) as post_name,
(select c.id from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where mlp.post_id = p.id and p.community_id = c.id) as community_name
from mod_lock_post mlp;
create view mod_remove_comment_view as
select mrc.*,
(select name from user_ u where mrc.mod_user_id = u.id) as mod_user_name,
(select c.id from comment c where mrc.comment_id = c.id) as comment_user_id,
(select name from user_ u, comment c where mrc.comment_id = c.id and u.id = c.creator_id) as comment_user_name,
(select content from comment c where mrc.comment_id = c.id) as comment_content,
(select p.id from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_id,
(select p.name from post p, comment c where mrc.comment_id = c.id and c.post_id = p.id) as post_name,
(select co.id from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_id,
(select co.name from comment c, post p, community co where mrc.comment_id = c.id and c.post_id = p.id and p.community_id = co.id) as community_name
from mod_remove_comment mrc;
create view mod_sticky_post_view as
select msp.*,
(select name from user_ u where msp.mod_user_id = u.id) as mod_user_name,
(select name from post p where msp.post_id = p.id) as post_name,
(select c.id from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_id,
(select c.name from post p, community c where msp.post_id = p.id and p.community_id = c.id) as community_name
from mod_sticky_post msp;

View file

@ -126,7 +126,15 @@ impl Perform<PostResponse> for Oper<CreatePost> {
let inserted_post = match Post::create(&conn, &post_form) { let inserted_post = match Post::create(&conn, &post_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_create_post"
};
return Err(APIError::err(err_type).into());
}
}; };
// They like their own post by default // They like their own post by default
@ -361,7 +369,15 @@ impl Perform<PostResponse> for Oper<EditPost> {
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) { let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_update_post").into()), Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_update_post"
};
return Err(APIError::err(err_type).into());
}
}; };
// Mod tables // Mod tables

View file

@ -178,7 +178,11 @@ impl<'a> PostQueryBuilder<'a> {
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self { pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
use super::post_view::post_mview::dsl::*; use super::post_view::post_mview::dsl::*;
if let Some(search_term) = search_term.get_optional() { if let Some(search_term) = search_term.get_optional() {
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term))); let searcher = fuzzy_search(&search_term);
self.query = self
.query
.filter(name.ilike(searcher.to_owned()))
.or_filter(body.ilike(searcher));
} }
self self
} }

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.6.11"; pub const VERSION: &str = "v0.6.13";

View file

@ -12,6 +12,7 @@ use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::time::SystemTime; use std::time::SystemTime;
use strum::IntoEnumIterator;
use crate::api::comment::*; use crate::api::comment::*;
use crate::api::community::*; use crate::api::community::*;
@ -72,6 +73,13 @@ pub struct SessionInfo {
pub ip: IPAddr, pub ip: IPAddr,
} }
#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)]
pub enum RateLimitType {
Message,
Register,
Post,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat /// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. /// session.
pub struct ChatServer { pub struct ChatServer {
@ -88,8 +96,8 @@ pub struct ChatServer {
/// sessions (IE clients) /// sessions (IE clients)
user_rooms: HashMap<UserId, HashSet<ConnectionId>>, user_rooms: HashMap<UserId, HashSet<ConnectionId>>,
/// Rate limiting based on IP addr /// Rate limiting based on rate type and IP addr
rate_limits: HashMap<IPAddr, RateLimitBucket>, rate_limit_buckets: HashMap<RateLimitType, HashMap<IPAddr, RateLimitBucket>>,
rng: ThreadRng, rng: ThreadRng,
db: Pool<ConnectionManager<PgConnection>>, db: Pool<ConnectionManager<PgConnection>>,
@ -99,7 +107,7 @@ impl ChatServer {
pub fn startup(db: Pool<ConnectionManager<PgConnection>>) -> ChatServer { pub fn startup(db: Pool<ConnectionManager<PgConnection>>) -> ChatServer {
ChatServer { ChatServer {
sessions: HashMap::new(), sessions: HashMap::new(),
rate_limits: HashMap::new(), rate_limit_buckets: HashMap::new(),
post_rooms: HashMap::new(), post_rooms: HashMap::new(),
community_rooms: HashMap::new(), community_rooms: HashMap::new(),
user_rooms: HashMap::new(), user_rooms: HashMap::new(),
@ -260,60 +268,80 @@ impl ChatServer {
to_json_string(&user_operation, post) to_json_string(&user_operation, post)
} }
fn check_rate_limit_register(&mut self, id: usize) -> Result<(), Error> { fn check_rate_limit_register(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full( self.check_rate_limit_full(
RateLimitType::Register,
id, id,
Settings::get().rate_limit.register, Settings::get().rate_limit.register,
Settings::get().rate_limit.register_per_second, Settings::get().rate_limit.register_per_second,
check_only,
) )
} }
fn check_rate_limit_post(&mut self, id: usize) -> Result<(), Error> { fn check_rate_limit_post(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full( self.check_rate_limit_full(
RateLimitType::Post,
id, id,
Settings::get().rate_limit.post, Settings::get().rate_limit.post,
Settings::get().rate_limit.post_per_second, Settings::get().rate_limit.post_per_second,
check_only,
) )
} }
fn check_rate_limit_message(&mut self, id: usize) -> Result<(), Error> { fn check_rate_limit_message(&mut self, id: usize, check_only: bool) -> Result<(), Error> {
self.check_rate_limit_full( self.check_rate_limit_full(
RateLimitType::Message,
id, id,
Settings::get().rate_limit.message, Settings::get().rate_limit.message,
Settings::get().rate_limit.message_per_second, Settings::get().rate_limit.message_per_second,
check_only,
) )
} }
#[allow(clippy::float_cmp)] #[allow(clippy::float_cmp)]
fn check_rate_limit_full(&mut self, id: usize, rate: i32, per: i32) -> Result<(), Error> { fn check_rate_limit_full(
&mut self,
type_: RateLimitType,
id: usize,
rate: i32,
per: i32,
check_only: bool,
) -> Result<(), Error> {
if let Some(info) = self.sessions.get(&id) { if let Some(info) = self.sessions.get(&id) {
if let Some(rate_limit) = self.rate_limits.get_mut(&info.ip) { if let Some(bucket) = self.rate_limit_buckets.get_mut(&type_) {
// The initial value if let Some(rate_limit) = bucket.get_mut(&info.ip) {
if rate_limit.allowance == -2f64 { let current = SystemTime::now();
rate_limit.allowance = rate as f64; let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
};
let current = SystemTime::now(); // The initial value
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64; if rate_limit.allowance == -2f64 {
rate_limit.last_checked = current; rate_limit.allowance = rate as f64;
rate_limit.allowance += time_passed * (rate as f64 / per as f64); };
if rate_limit.allowance > rate as f64 {
rate_limit.allowance = rate as f64;
}
if rate_limit.allowance < 1.0 { rate_limit.last_checked = current;
println!( rate_limit.allowance += time_passed * (rate as f64 / per as f64);
"Rate limited IP: {}, time_passed: {}, allowance: {}", if !check_only && rate_limit.allowance > rate as f64 {
&info.ip, time_passed, rate_limit.allowance rate_limit.allowance = rate as f64;
); }
Err(
APIError { if rate_limit.allowance < 1.0 {
message: format!("Too many requests. {} per {} seconds", rate, per), println!(
"Rate limited IP: {}, time_passed: {}, allowance: {}",
&info.ip, time_passed, rate_limit.allowance
);
Err(
APIError {
message: format!("Too many requests. {} per {} seconds", rate, per),
}
.into(),
)
} else {
if !check_only {
rate_limit.allowance -= 1.0;
} }
.into(), Ok(())
) }
} else { } else {
rate_limit.allowance -= 1.0;
Ok(()) Ok(())
} }
} else { } else {
@ -351,14 +379,24 @@ impl Handler<Connect> for ChatServer {
}, },
); );
if self.rate_limits.get(&msg.ip).is_none() { for rate_limit_type in RateLimitType::iter() {
self.rate_limits.insert( if self.rate_limit_buckets.get(&rate_limit_type).is_none() {
msg.ip, self
RateLimitBucket { .rate_limit_buckets
last_checked: SystemTime::now(), .insert(rate_limit_type, HashMap::new());
allowance: -2f64, }
},
); if let Some(bucket) = self.rate_limit_buckets.get_mut(&rate_limit_type) {
if bucket.get(&msg.ip).is_none() {
bucket.insert(
msg.ip.to_owned(),
RateLimitBucket {
last_checked: SystemTime::now(),
allowance: -2f64,
},
);
}
}
} }
id id
@ -447,11 +485,18 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
// TODO: none of the chat messages are going to work if stuff is submitted via http api, // TODO: none of the chat messages are going to work if stuff is submitted via http api,
// need to move that handling elsewhere // need to move that handling elsewhere
// A DDOS check
chat.check_rate_limit_message(msg.id, false)?;
match user_operation { match user_operation {
UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn), UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn),
UserOperation::Register => { UserOperation::Register => {
chat.check_rate_limit_register(msg.id)?; chat.check_rate_limit_register(msg.id, true)?;
do_user_operation::<Register, LoginResponse>(user_operation, data, &conn) let register: Register = serde_json::from_str(data)?;
let res = Oper::new(register).perform(&conn)?;
chat.check_rate_limit_register(msg.id, false)?;
to_json_string(&user_operation, &res)
} }
UserOperation::GetUserDetails => { UserOperation::GetUserDetails => {
do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn) do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn)
@ -527,8 +572,11 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
} }
} }
UserOperation::CreateCommunity => { UserOperation::CreateCommunity => {
chat.check_rate_limit_register(msg.id)?; chat.check_rate_limit_register(msg.id, true)?;
do_user_operation::<CreateCommunity, CommunityResponse>(user_operation, data, &conn) let create_community: CreateCommunity = serde_json::from_str(data)?;
let res = Oper::new(create_community).perform(&conn)?;
chat.check_rate_limit_register(msg.id, false)?;
to_json_string(&user_operation, &res)
} }
UserOperation::EditCommunity => { UserOperation::EditCommunity => {
let edit_community: EditCommunity = serde_json::from_str(data)?; let edit_community: EditCommunity = serde_json::from_str(data)?;
@ -590,14 +638,14 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
to_json_string(&user_operation, &res) to_json_string(&user_operation, &res)
} }
UserOperation::CreatePost => { UserOperation::CreatePost => {
chat.check_rate_limit_post(msg.id)?; chat.check_rate_limit_post(msg.id, true)?;
let create_post: CreatePost = serde_json::from_str(data)?; let create_post: CreatePost = serde_json::from_str(data)?;
let res = Oper::new(create_post).perform(&conn)?; let res = Oper::new(create_post).perform(&conn)?;
chat.check_rate_limit_post(msg.id, false)?;
chat.post_sends(UserOperation::CreatePost, res, msg.id) chat.post_sends(UserOperation::CreatePost, res, msg.id)
} }
UserOperation::CreatePostLike => { UserOperation::CreatePostLike => {
chat.check_rate_limit_message(msg.id)?;
let create_post_like: CreatePostLike = serde_json::from_str(data)?; let create_post_like: CreatePostLike = serde_json::from_str(data)?;
let res = Oper::new(create_post_like).perform(&conn)?; let res = Oper::new(create_post_like).perform(&conn)?;
@ -613,7 +661,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn) do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn)
} }
UserOperation::CreateComment => { UserOperation::CreateComment => {
chat.check_rate_limit_message(msg.id)?;
let create_comment: CreateComment = serde_json::from_str(data)?; let create_comment: CreateComment = serde_json::from_str(data)?;
let res = Oper::new(create_comment).perform(&conn)?; let res = Oper::new(create_comment).perform(&conn)?;
@ -629,7 +676,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn) do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn)
} }
UserOperation::CreateCommentLike => { UserOperation::CreateCommentLike => {
chat.check_rate_limit_message(msg.id)?;
let create_comment_like: CreateCommentLike = serde_json::from_str(data)?; let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
let res = Oper::new(create_comment_like).perform(&conn)?; let res = Oper::new(create_comment_like).perform(&conn)?;
@ -673,7 +719,6 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn) do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
} }
UserOperation::CreatePrivateMessage => { UserOperation::CreatePrivateMessage => {
chat.check_rate_limit_message(msg.id)?;
let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?; let create_private_message: CreatePrivateMessage = serde_json::from_str(data)?;
let recipient_id = create_private_message.recipient_id; let recipient_id = create_private_message.recipient_id;
let res = Oper::new(create_private_message).perform(&conn)?; let res = Oper::new(create_private_message).perform(&conn)?;

View file

@ -37,6 +37,7 @@
} }
.md-div img { .md-div img {
max-height: 90vh;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
@ -170,3 +171,7 @@ hr {
-o-filter: blur(10px); -o-filter: blur(10px);
-ms-filter: blur(10px); -ms-filter: blur(10px);
} }
.img-expanded {
max-height: 90vh;
}

View file

@ -37,6 +37,8 @@ import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
const MAX_POST_TITLE_LENGTH = 200;
interface PostFormProps { interface PostFormProps {
post?: Post; // If a post is given, that means this is an edit post?: Post; // If a post is given, that means this is an edit
params?: PostFormParams; params?: PostFormParams;
@ -232,7 +234,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
required required
rows={2} rows={2}
minLength={3} minLength={3}
maxLength={100} maxLength={MAX_POST_TITLE_LENGTH}
/> />
{this.state.suggestedPosts.length > 0 && ( {this.state.suggestedPosts.length > 0 && (
<> <>
@ -360,7 +362,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
} }
copySuggestedTitle(i: PostForm) { copySuggestedTitle(i: PostForm) {
i.state.postForm.name = i.state.suggestedTitle; i.state.postForm.name = i.state.suggestedTitle.substring(
0,
MAX_POST_TITLE_LENGTH
);
i.state.suggestedTitle = undefined; i.state.suggestedTitle = undefined;
i.setState(i.state); i.setState(i.state);
} }
@ -511,12 +516,16 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.CreatePost) { } else if (res.op == UserOperation.CreatePost) {
let data = res.data as PostResponse; let data = res.data as PostResponse;
this.state.loading = false; if (data.post.creator_id == UserService.Instance.user.id) {
this.props.onCreate(data.post.id); this.state.loading = false;
this.props.onCreate(data.post.id);
}
} else if (res.op == UserOperation.EditPost) { } else if (res.op == UserOperation.EditPost) {
let data = res.data as PostResponse; let data = res.data as PostResponse;
this.state.loading = false; if (data.post.creator_id == UserService.Instance.user.id) {
this.props.onEdit(data.post); this.state.loading = false;
this.props.onEdit(data.post);
}
} else if (res.op == UserOperation.Search) { } else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse; let data = res.data as SearchResponse;

View file

@ -237,7 +237,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
class="pointer" class="pointer"
onClick={linkEvent(this, this.handleImageExpandClick)} onClick={linkEvent(this, this.handleImageExpandClick)}
> >
<img class="img-fluid" src={post.url} /> <img class="img-fluid img-expanded" src={post.url} />
</span> </span>
</div> </div>
</span> </span>

View file

@ -150,6 +150,7 @@ export const de = {
sponsor_message: sponsor_message:
'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:', 'Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:',
support_on_patreon: 'Auf Patreon unterstützen', support_on_patreon: 'Auf Patreon unterstützen',
support_on_liberapay: 'Auf Liberapay unterstützen',
general_sponsors: general_sponsors:
'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.', 'Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.',
crypto: 'Kryptowährung', crypto: 'Kryptowährung',

View file

@ -211,6 +211,7 @@ export const en = {
community_follower_already_exists: 'Community follower already exists.', community_follower_already_exists: 'Community follower already exists.',
community_user_already_banned: 'Community user already banned.', community_user_already_banned: 'Community user already banned.',
couldnt_create_post: "Couldn't create post.", couldnt_create_post: "Couldn't create post.",
post_title_too_long: 'Post title too long.',
couldnt_like_post: "Couldn't like post.", couldnt_like_post: "Couldn't like post.",
couldnt_find_post: "Couldn't find post.", couldnt_find_post: "Couldn't find post.",
couldnt_get_posts: "Couldn't get posts", couldnt_get_posts: "Couldn't get posts",

View file

@ -169,6 +169,7 @@ export const es = {
sponsor_message: sponsor_message:
'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:', 'Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:',
support_on_patreon: 'Apoyo en Patreon', support_on_patreon: 'Apoyo en Patreon',
support_on_liberapay: 'Apoyo en Liberapay',
donate_to_lemmy: 'Donar a Lemmy', donate_to_lemmy: 'Donar a Lemmy',
donate: 'Donar', donate: 'Donar',
general_sponsors: general_sponsors:

View file

@ -139,6 +139,7 @@ export const fr = {
sponsor_message: sponsor_message:
"Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.", "Lemmy est gratuit et <1>open-source</1>, c'est à dire sans publicité et sans monétisation. Pour toujours. Vos dons soutiennent directement le développement du projet. Merci à nos soutiens.",
support_on_patreon: 'Soutenir sur Patreon', support_on_patreon: 'Soutenir sur Patreon',
support_on_liberapay: 'Soutenir sur Liberapay',
general_sponsors: general_sponsors:
'General Sponsors are those that pledged $10 to $39 to Lemmy.', 'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: 'Cryptomonnaies', crypto: 'Cryptomonnaies',

View file

@ -138,6 +138,7 @@ export const it = {
sponsors_of_lemmy: 'Sponsors di Lemmy', sponsors_of_lemmy: 'Sponsors di Lemmy',
sponsor_message: 'Lemmy è un software gratuito e <1>open-source</1>, il che significa nessuna pubblicità, monetizzazione o investitori esterni, per sempre. Le tue donazioni supportano direttamente lo sviluppo full-time del progetto. Si ringraziano le seguenti persone:', sponsor_message: 'Lemmy è un software gratuito e <1>open-source</1>, il che significa nessuna pubblicità, monetizzazione o investitori esterni, per sempre. Le tue donazioni supportano direttamente lo sviluppo full-time del progetto. Si ringraziano le seguenti persone:',
support_on_patreon: 'Supporta su Patreon', support_on_patreon: 'Supporta su Patreon',
support_on_liberapay: 'Supporta su Liberapay',
general_sponsors: 'I "General Sponsors" sono quelli che hanno investito dai 10$ ai 39$ su Lemmy.', general_sponsors: 'I "General Sponsors" sono quelli che hanno investito dai 10$ ai 39$ su Lemmy.',
crypto: 'Crypto', crypto: 'Crypto',
bitcoin: 'Bitcoin', bitcoin: 'Bitcoin',

View file

@ -125,6 +125,7 @@ export const nl = {
sponsor_message: sponsor_message:
'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:', 'Lemmy is vrije, <1>open-source</1> software, dus zonder reclame, winstoogmerk en durfkapitaal, punt. Jouw donaties gaan direct naar de full-time-ontwikkeling van het project. Met veel dank aan de volgende mensen:',
support_on_patreon: 'Ondersteun op Patreon', support_on_patreon: 'Ondersteun op Patreon',
support_on_liberapay: 'Ondersteun op Liberapay',
general_sponsors: general_sponsors:
'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.', 'Algemene sponsors zijn sponsors die tussen de $10 en $39 hebben gegeven aan Lemmy.',
crypto: 'Cryptovaluta', crypto: 'Cryptovaluta',

View file

@ -171,6 +171,7 @@ export const pt_BR = {
sponsor_message: sponsor_message:
'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:', 'Lemmy é um programa livre e de código aberto, o que significa que não haverá publicidade, monetização ou capital de risco, jamais. Suas doações apoiam de forma direta o desenvolvimento em tempo integral do projeto. Muitos agradecimentos às sequintes pessoas:',
support_on_patreon: 'Colabore no Patreon', support_on_patreon: 'Colabore no Patreon',
support_on_liberapay: 'Colabore no Liberapay',
donate_to_lemmy: 'Faça uma doação ao Lemmy', donate_to_lemmy: 'Faça uma doação ao Lemmy',
donate: 'Doar', donate: 'Doar',
general_sponsors: general_sponsors:

View file

@ -116,6 +116,7 @@ export const zh = {
sponsor_message: sponsor_message:
'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:', 'Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:',
support_on_patreon: '在 Patreon 赞助', support_on_patreon: '在 Patreon 赞助',
support_on_liberapay: '在 on 赞助',
general_sponsors: general_sponsors:
'General Sponsors are those that pledged $10 to $39 to Lemmy.', 'General Sponsors are those that pledged $10 to $39 to Lemmy.',
crypto: '加密', crypto: '加密',

2
ui/src/utils.ts vendored
View file

@ -384,7 +384,7 @@ export function showAvatars(): boolean {
export function imageThumbnailer(url: string): string { export function imageThumbnailer(url: string): string {
let split = url.split('pictshare'); let split = url.split('pictshare');
if (split.length > 1) { if (split.length > 1) {
let out = `${split[0]}pictshare/140x140${split[1]}`; let out = `${split[0]}pictshare/192x192${split[1]}`;
return out; return out;
} else { } else {
return url; return url;

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.6.11'; export const version: string = 'v0.6.13';