Move docs from main Lemmy repo

This commit is contained in:
Felix Ableitner 2021-01-04 13:08:16 +01:00
commit 0c98e259d8
30 changed files with 4008 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
book

6
book.toml Normal file
View file

@ -0,0 +1,6 @@
[book]
authors = ["Felix Ableitner"]
language = "en"
multilingual = false
src = "src"
title = "Lemmy Documentation"

BIN
img/chat_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
img/main_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
img/rank_algorithm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

26
src/SUMMARY.md Normal file
View file

@ -0,0 +1,26 @@
# Summary
- [About](about/about.md)
- [Features](about/features.md)
- [Goals](about/goals.md)
- [Post and Comment Ranking](about/ranking.md)
- [Guide](about/guide.md)
- [Administration](administration/administration.md)
- [Install with Docker](administration/install_docker.md)
- [Install with Ansible](administration/install_ansible.md)
- [Configuration](administration/configuration.md)
- [Backup and Restore](administration/backup_and_restore.md)
- [Federation](federation/federation.md)
- [Federation Overview](federation/overview.md)
- [Administration](federation/administration.md)
- [Resources](federation/resources.md)
- [Lemmy Protocol](federation/lemmy_protocol.md)
- [Contributing](contributing/contributing.md)
- [Docker Development](contributing/docker_development.md)
- [Local Development](contributing/local_development.md)
- [Theming Guide](contributing/theming.md)
- [Websocket/HTTP API](contributing/websocket_http_api.md)
- [Creating a Custom Frontend](contributing/custom_frontend.md)
- [Tests](contributing/tests.md)
- [Federation Development](contributing/federation_development.md)
- [Lemmy Council](lemmy_council.md)

30
src/about/about.md Normal file
View file

@ -0,0 +1,30 @@
## About The Project
Front Page|Post
---|---
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
[Lemmy](https://github.com/LemmyNet/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.
*Note: Federation is still in active development*
### Why's it called Lemmy?
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
### 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/)

35
src/about/features.md Normal file
View file

@ -0,0 +1,35 @@
# Features
- Open source, [AGPL License](/LICENSE).
- Self hostable, easy to deploy.
- Comes with [Docker](#docker) and [Ansible](#ansible).
- Clean, mobile-friendly interface.
- Only a minimum of a username and password is required to sign up!
- User avatar support.
- 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 `!`.
- 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 can be sent via email.
- 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.
- Can sticky posts to the top of communities.
- 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.
- Front end works without javascript (read-only).
- Supports arm64 / Raspberry Pi.

39
src/about/goals.md Normal file
View file

@ -0,0 +1,39 @@
# Goals
- Come up with a name / codename.
- Must have communities.
- Must have threaded comments.
- Must be federated: liking and following communities across instances.
- Be live-updating: have a right pane for new comments, and a main pain for the full threaded view.
- Use websockets for post / gets to your own instance.
# Questions
- How does voting work? Should we go back to the old way of showing up and downvote counts? Or just a score?
- Decide on tech to be used
- Backend: Actix, Diesel.
- Frontend: inferno, typescript and bootstrap for now.
- Should it allow bots?
- Should the comments / votes be static, or feel like a chat, like [flowchat?](https://flow-chat.com).
- Two pane model - Right pane is live comments, left pane is live tree view.
- On mobile, allow you to switch between them. Default?
# Resources / Potential Libraries
- [Diesel to Postgres data types](https://kotiri.com/2018/01/31/postgresql-diesel-rust-types.html)
- [helpful diesel examples](http://siciarz.net/24-days-rust-diesel/)
- [Recursive query for adjacency list for nested comments](https://stackoverflow.com/questions/192220/what-is-the-most-efficient-elegant-way-to-parse-a-flat-table-into-a-tree/192462#192462)
- https://github.com/sparksuite/simplemde-markdown-editor
- [Markdown-it](https://github.com/markdown-it/markdown-it)
- [Sticky Sidebar](https://stackoverflow.com/questions/38382043/how-to-use-css-position-sticky-to-keep-a-sidebar-visible-with-bootstrap-4/49111934)
- [RXJS websocket](https://stackoverflow.com/questions/44060315/reconnecting-a-websocket-in-angular-and-rxjs/44067972#44067972)
- [Rust JWT](https://github.com/Keats/jsonwebtoken)
- [Hierarchical tree building javascript](https://stackoverflow.com/a/40732240/1655478)
- [Hot sorting discussion](https://meta.stackexchange.com/questions/11602/what-formula-should-be-used-to-determine-hot-questions) [2](https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9)
- [Classification types.](https://www.reddit.com/r/ModeratorDuck/wiki/subreddit_classification)
- [RES expando - Possibly make this into a switching react component.](https://github.com/honestbleeps/Reddit-Enhancement-Suite/tree/d21f55c21e734f47d8ed03fe0ebce5b16653b0bd/lib/modules/hosts)
- [Temp Icon](https://www.flaticon.com/free-icon/mouse_194242)
- [Rust docker build](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)
- [Zurb mentions](https://github.com/zurb/tribute)
- [TippyJS](https://github.com/atomiks/tippyjs)
- [SQL function indexes](https://sorentwo.com/2013/12/30/let-postgres-do-the-work.html)

43
src/about/guide.md Normal file
View file

@ -0,0 +1,43 @@
# Lemmy Guide
Start typing...
- `@a_user_name` to get a list of usernames.
- `!a_community` to get a list of communities.
- `:emoji` to get a list of emojis.
## Sorting
*Applies to both posts and comments*
Type | Description
--- | ---
Active | Trending sort based on the score, and the most recent comment time.
Hot | Trending sort based on the score, and the post creation time.
New | Newest items.
Top | The highest scoring items in the given time frame.
For more detail, check the [Post and Comment Ranking details](ranking.md).
## Markdown Guide
Type | Or | … to Get
--- | --- | ---
\*Italic\* | \_Italic\_ | _Italic_
\*\*Bold\*\* | \_\_Bold\_\_ | **Bold**
\# Heading 1 | Heading 1 <br> ========= | <h4>Heading 1</h4>
\## Heading 2 | Heading 2 <br>--------- | <h5>Heading 2</h5>
\[Link\](http://a.com) | \[Link\]\[1\]<br><br>\[1\]: http://b.org | [Link](https://commonmark.org/)
!\[Image\](http://url/a.png) | !\[Image\]\[1\]<br><br>\[1\]: http://url/b.jpg | ![Markdown](https://commonmark.org/help/images/favicon.png)
\> Blockquote | | <blockquote>Blockquote</blockquote>
\* List <br>\* List <br>\* List | \- List <br>\- List <br>\- List <br> | * List <br>* List <br>* List <br>
1\. One <br>2\. Two <br>3\. Three | 1) One<br>2) Two<br>3) Three | 1. One<br>2. Two<br>3. Three
Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule <br><hr>
\`Inline code\` with backticks | |`Inline code` with backticks
\`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces'
::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details>
Some ~subscript~ text | | Some <sub>subscript</sub> text
Some ^superscript^ text | | Some <sup>superscript</sup> text
[CommonMark Tutorial](https://commonmark.org/help/tutorial/)

37
src/about/ranking.md Normal file
View file

@ -0,0 +1,37 @@
# Trending / Hot / Best Sorting algorithm
## Goals
- During the day, new posts and comments should be near the top, so they can be voted on.
- After a day or so, the time factor should go away.
- Use a log scale, since votes tend to snowball, and so the first 10 votes are just as important as the next hundred.
## Implementations
### Reddit
Does not take the lifetime of the thread into account, [giving early comments an overwhelming advantage over later ones,](https://minimaxir.com/2016/11/first-comment/) with the effect being even worse in small communities. New comments pool at the bottom of the thread, effectively killing off discussion and making each thread a race to comment early. This lowers the quality of conversation and rewards comments that are repetitive and spammy.
### Hacker News
While far superior to Reddit's implementation for its decay of scores over time, [Hacker News' ranking algorithm](https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d) does not use a logarithmic scale for scores.
### Lemmy
Counterbalances the snowballing effect of votes over time with a logarithmic scale. Negates the inherent advantage of early comments while still ensuring that votes still matter in the long-term, not nuking older popular comments.
```
Rank = ScaleFactor * log(Max(1, 3 + Score)) / (Time + 2)^Gravity
Score = Upvotes - Downvotes
Time = time since submission (in hours)
Gravity = Decay gravity, 1.8 is default
```
- Lemmy uses the same `Rank` algorithm above, in two sorts: `Active`, and `Hot`.
- `Active` uses the post votes, and latest comment time (limited to two days).
- `Hot` uses the post votes, and the post published time.
- Use Max(1, score) to make sure all comments are affected by time decay.
- Add 3 to the score, so that everything that has less than 3 downvotes will seem new. Otherwise all new comments would stay at zero, near the bottom.
- The sign and abs of the score are necessary for dealing with the log of negative scores.
- A scale factor of 10k gets the rank in integer form.
A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k.
![](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/rank_algorithm.png)

View file

@ -0,0 +1,11 @@
# Administration info
Information for Lemmy instance admins, and those who want to run a server.
## Install
Lemmy has two primary installation methods, [manually with Docker](install_docker.md), and [automated with Ansible](install_ansible.md). We recommend using Ansible, because it simplifies the installation and also makes updating easier.
### Manual install (without Docker)
Manual installs are *possible*, but not preferred, since Lemmy is dependent on other local services: The [lemmy-ui](https://github.com/LemmyNet/lemmy-ui), [a Postgresql Database](https://www.postgresql.org/), [pict-rs](https://git.asonix.dog/asonix/pict-rs/) for images, and [iframely](https://iframely.com/) for embeds. To see how these are wired together, look at the docker-compose.yml files. Due to the complexity of different systems, we will not support manual installs.

View file

@ -0,0 +1,83 @@
# Backup and Restore Guide
## Docker and Ansible
When using docker or ansible, there should be a `volumes` folder, which contains both the database, and all the pictures. Copy this folder to the new instance to restore your data.
### Incremental Database backup
To incrementally backup the DB to an `.sql` file, you can run:
```bash
docker-compose exec postgres pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
```
### A Sample backup script
```bash
#!/bin/sh
# DB Backup
ssh MY_USER@MY_IP "docker-compose exec postgres pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
# Volumes folder Backup
rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME
```
### Restoring the DB
If you need to restore from a `pg_dumpall` file, you need to first clear out your existing database
```bash
# Drop the existing DB
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# Restore from the .sql backup
cat db_dump.sql | docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restores the db
# This also might be necessary when doing a db import with a different password.
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'"
```
### Changing your domain name
If you haven't federated yet, you can change your domain name in the DB. **Warning: do not do this after you've federated, or it will break federation.**
Get into `psql` for your docker:
`docker-compose exec postgres psql -U lemmy`
```
-- Post
update post set ap_id = replace (ap_id, 'old_domain', 'new_domain');
update post set url = replace (url, 'old_domain', 'new_domain');
update post set body = replace (body, 'old_domain', 'new_domain');
update post set thumbnail_url = replace (thumbnail_url, 'old_domain', 'new_domain');
delete from post_aggregates_fast;
insert into post_aggregates_fast select * from post_aggregates_view;
-- Comments
update comment set ap_id = replace (ap_id, 'old_domain', 'new_domain');
update comment set content = replace (content, 'old_domain', 'new_domain');
delete from comment_aggregates_fast;
insert into comment_aggregates_fast select * from comment_aggregates_view;
-- User
update user_ set actor_id = replace (actor_id, 'old_domain', 'new_domain');
update user_ set avatar = replace (avatar, 'old_domain', 'new_domain');
delete from user_fast;
insert into user_fast select * from user_view;
-- Community
update community set actor_id = replace (actor_id, 'old_domain', 'new_domain');
delete from community_aggregates_fast;
insert into community_aggregates_fast select * from community_aggregates_view;
```
## More resources
- https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database

View file

@ -0,0 +1,19 @@
# Configuration
The configuration is based on the file [defaults.hjson](https://yerbamate.ml/LemmyNet/lemmy/src/branch/main/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 `defaults.hjson` and `config.hjson` files are located at `config/defaults.hjson` and`config/config.hjson`, respectively. To change these default locations, you can set these two environment variables:
- LEMMY_CONFIG_LOCATION # config.hjson
- LEMMY_CONFIG_DEFAULTS_LOCATION # defaults.hjson
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.
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

@ -0,0 +1,25 @@
# Ansible Installation
This is the same as the [Docker installation](install_docker.md), except that Ansible handles all of it automatically. It also does some extra things like setting up TLS and email for your Lemmy instance.
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/LemmyNet/lemmy.git
cd lemmy/ansible/
cp inventory.example inventory
nano inventory # enter your server, domain, contact email
# If the command below fails, you may need to comment out this line
# In the ansible.cfg file:
# interpreter_python=/usr/bin/python3
ansible-playbook lemmy.yml --become
```
To update to a new version, just run the following in your local Lemmy repo:
```bash
git pull origin main
cd ansible
ansible-playbook lemmy.yml --become
```

View file

@ -0,0 +1,55 @@
# Docker Installation
Make sure you have both docker and docker-compose(>=`1.24.0`) installed. On Ubuntu, just run `apt install docker-compose docker.io`. Next,
```bash
# create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want
mkdir /lemmy
cd /lemmy
# download default config files
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/lemmy.hjson
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/iframely.config.local.js
# Set correct permissions for pictrs folder
mkdir -p volumes/pictrs
sudo chown -R 991:991 volumes/pictrs
```
Open up your `docker-compose.yml`, and make sure `LEMMY_EXTERNAL_HOST` for `lemmy-ui` is set to your correct host.
```
- LEMMY_INTERNAL_HOST=lemmy:8536
- LEMMY_EXTERNAL_HOST=your-domain.com
- LEMMY_HTTPS=false
```
If you'd like a different database password, you should also change it in the `docker-compose.yml` **before** your first run.
After this, have a look at the [config file](configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname, and possibly the db password. Then run:
`docker-compose up -d`
You can access the lemmy-ui at `http://localhost:1235`
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf), could be setup with:
```bash
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf
# Replace the {{ vars }}
# The default lemmy_port is 8536
# The default lemmy_ui_port is 1235
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
```
You will also need to setup TLS, for example with [Let's Encrypt](https://letsencrypt.org/). After this you need to restart Nginx to reload the config.
## Updating
To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo:
```bash
wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
docker-compose up -d
```

View file

@ -0,0 +1,40 @@
# Contributing
Information about contributing to Lemmy, whether it is translating, testing, designing or programming.
## Issue tracking / Repositories
- [GitHub (for issues and pull requests)](https://github.com/LemmyNet/lemmy)
- [Gitea (only for pull requests)](https://yerbamate.ml/LemmyNet/lemmy)
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
## Translating
Check out [Lemmy's Weblate](https://weblate.yerbamate.ml/projects/lemmy/) for translations.
## Architecture
### Front end
- The front end is written in `typescript`, using a react-like framework called [inferno](https://infernojs.org/). All UI elements are reusable `.tsx` components.
- The front end repository is [lemmy-ui](https://github.com/LemmyNet/lemmy-ui).
- The routes are at `src/shared/routes.ts`.
- The components are located in `src/shared/components`.
### Back end
- The back end is written in `rust`, using `diesel`, and `actix`.
- The server source code is split into main sections in `src`. These include:
- `db` - The low level database actions.
- Database additions are done using diesel migrations. Run `diesel migration generate xxxxx` to add new things.
- `api` - The high level user interactions (things like `CreateComment`)
- `routes` - The server endpoints .
- `apub` - The activitypub conversions.
- `websocket` - Creates the websocket server.
## Linting / Formatting
- Every front and back end commit is automatically formatted then linted using `husky`, and `lint-staged`.
- Rust with `cargo fmt` and `cargo clippy`.
- Typescript with `prettier` and `eslint`.

View file

@ -0,0 +1,66 @@
# Creating a Custom Frontend
The backend and frontend are completely decoupled, and run in independent Docker containers. They only communicate over the [Lemmy API](websocket_http_api.md), which makes it quite easy to write alternative frontends.
This creates a lot of potential for custom frontends, which could change much of the design and user experience of Lemmy. For example, it would be possible to create a frontend in the style of a traditional forum like [phpBB](https://www.phpbb.com/), or a question-and-answer site like [stackoverflow](https://stackoverflow.com/). All without having to think about database queries, authentification or ActivityPub, which you essentially get for free.
## Development
You can use any language to create a custom frontend. The easiest option would be forking our [official frontend](https://github.com/LemmyNet/lemmy-ui), [lemmy-lite](https://github.com/IronOxidizer/lemmy-lite), or the [lemmy-frontend-example](https://github.com/LemmyNet/lemmy-front-end-example). In any case, the principle is the same: bind to `LEMMY_EXTERNAL_HOST` (default: `localhost:8536`) and handle requests using the Lemmy API at `LEMMY_INTERNAL_HOST` (default: `lemmy:8536`). Also use `LEMMY_HTTPS` to generate links with the correct protocol.
The next step is building a Docker image from your frontend. If you forked an existing project, it should already include a `Dockerfile` and instructions to build it. Otherwise, try searching for your language on [dockerhub](https://hub.docker.com/), official images usually have build instructions in their readme. Build a Docker image with a tag, then look for the following section in `docker/dev/docker-compose.yml`:
```
lemmy-ui:
image: dessalines/lemmy-ui:v0.8.10
ports:
- "1235:1234"
restart: always
environment:
- LEMMY_INTERNAL_HOST=lemmy:8536
- LEMMY_EXTERNAL_HOST=localhost:8536
- LEMMY_HTTPS=false
depends_on:
- lemmy
```
All you need to do is replace the value for `image` with the tag of your own Docker image (and possibly the environment variables if you need different ones). Then run `./docker_update.sh`, and after compilation, your frontend will be available on `http://localhost:1235`. You can also make the same change to `docker/federation/docker-compose.yml` and run `./start-local-instances.bash` to test federation with your frontend.
## Deploy with Docker
After building the Docker image, you need to push it to a Docker registry (such as [dockerhub](https://hub.docker.com/)). Then update the `docker-compose.yml` on your server, replacing the `image` for `lemmy-ui`, just as described above. Run `docker-compose.yml`, and after a short wait, your instance will use the new frontend.
Note, if your instance is deployed with Ansible, it will override `docker-compose.yml` with every run, reverting back to the default frontend. In that case you should copy the `ansible/` folder from this project to your own repository, and adjust `docker-compose.yml` directly in the repo.
It is also possible to use multiple frontends for the same Lemmy instance, either using subdomains or subfolders. To do that, don't edit the `lemmy-ui` section in `docker-compose.yml`, but duplicate it, adjusting the name, image and port so they are distinct for each. Then edit your nginx config to pass requests to the appropriate frontend, depending on the subdomain or path.
## Translations
You can add the [lemmy-translations](https://github.com/LemmyNet/lemmy-translations) repository to your project as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules). That way you can take advantage of same translations used in the official frontend, and you will also receive new translations contributed via weblate.
## Rate limiting
Lemmy does rate limiting for many actions based on the client IP. But if you make any API calls on the server side (eg in the case of server-side rendering, or javascript pre-rendering), Lemmy will take the IP of the Docker container. Meaning that all requests come from the same IP, and get rate limited much earlier. To avoid this problem, you need to pass the headers `X-REAL-IP` and `X-FORWARDED-FOR` on to Lemmy (the headers are set by our nginx config).
Here is an example snipped for NodeJS:
```javascript
function setForwardedHeaders(
headers: IncomingHttpHeaders
): { [key: string]: string } {
let out = {
host: headers.host,
};
if (headers['x-real-ip']) {
out['x-real-ip'] = headers['x-real-ip'];
}
if (headers['x-forwarded-for']) {
out['x-forwarded-for'] = headers['x-forwarded-for'];
}
return out;
}
let headers = setForwardedHeaders(req.headers);
let client = new LemmyHttp(httpUri, headers);
```

View file

@ -0,0 +1,32 @@
# Docker Development
## Dependencies (on Ubuntu)
```bash
sudo apt install git docker-compose
sudo systemctl start docker
git clone https://github.com/LemmyNet/lemmy
```
## Running
```bash
cd docker/dev
./docker_update.sh
```
and go to http://localhost:1235.
*Note: many features (like docs and pictures) will not work without using an nginx profile like that in `ansible/templates/nginx.conf`.
To speed up the Docker compile, add the following to `/etc/docker/daemon.json` and restart Docker.
```
{
"features": {
"buildkit": true
}
}
```
If the build is still too slow, you will have to use a
[local build](local_development.md) instead.

View file

@ -0,0 +1,69 @@
# Federation Development
## Running locally
Install the dependencies as described in [Docker development](docker_development.md). Then run the following
```bash
cd docker/federation
./start-local-instances.bash
```
The federation test sets up 5 instances:
Instance | Username | Location | Notes
--- | --- | --- | ---
lemmy-alpha | lemmy_alpha | [127.0.0.1:8540](http://127.0.0.1:8540) | federated with all other instances
lemmy-beta | lemmy_beta | [127.0.0.1:8550](http://127.0.0.1:8550) | federated with all other instances
lemmy-gamma | lemmy_gamma | [127.0.0.1:8560](http://127.0.0.1:8560) | federated with all other instances
lemmy-delta | lemmy_delta | [127.0.0.1:8570](http://127.0.0.1:8570) | only allows federation with lemmy-beta
lemmy-epsilon | lemmy_epsilon | [127.0.0.1:8580](http://127.0.0.1:8580) | uses blocklist, has lemmy-alpha blocked
You can log into each using the instance name, and `lemmy` as the password, IE (`lemmy_alpha`, `lemmy`).
To start federation between instances, visit one of them and search for a user, community or post, like this. Note that
the Lemmy backend runs on a different port than the frontend, so you have to increment the port number from
the URL bar by one.
- `!main@lemmy-alpha:8541`
- `http://lemmy-beta:8551/post/3`
- `@lemmy-gamma@lemmy-gamma:8561`
Firefox containers are a good way to test them interacting.
## Running on a server
Note that federation is currently in alpha. **Only use it for testing**, not on any production server, and be aware that turning on federation may break your instance.
Follow the normal installation instructions, either with [Ansible](../administration/install_ansible.md) or
[manually](../administration/install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in
`/lemmy/docker-compose.yml` with `image: dessalines/lemmy:federation`. Also add the following in
`/lemmy/lemmy.hjson`:
```
federation: {
enabled: true
tls_enabled: true,
allowed_instances: example.com,
}
```
Afterwards, and whenever you want to update to the latest version, run these commands on the server:
```
cd /lemmy/
sudo docker-compose pull
sudo docker-compose up -d
```
## Security Model
- HTTP signature verify: This ensures that activity really comes from the activity that it claims
- check_is_apub_valid : Makes sure its in our allowed instances list
- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.

View file

@ -0,0 +1,88 @@
### Install build requirements
#### Ubuntu
```
sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 espeak
# install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install yarn
```
#### macOS
Install Rust using [the recommended option on rust-lang.org](https://www.rust-lang.org/tools/install) (rustup).
Then, install [Homebrew](https://brew.sh/) if you don't already have it installed.
Finally, install Node and Yarn.
```
brew install node yarn
```
### Get the back end source code
```
git clone https://github.com/LemmyNet/lemmy.git
# or alternatively from gitea
# git clone https://yerbamate.ml/LemmyNet/lemmy.git
```
### Build the backend (Rust)
```
cargo build
# for development, use `cargo check` instead)
```
### Get the front end source code
```
git clone https://github.com/LemmyNet/lemmy-ui.git
# get the translations
git submodule init
git submodule update --remote
```
### Setup postgresql
#### Ubuntu
```
sudo apt install postgresql
sudo systemctl start postgresql
# Either execute db-init.sh, or manually initialize the postgres database:
sudo -u postgres psql -c "create user lemmy with password 'password' superuser;" -U postgres
sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -U postgres
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
```
#### macOS
```
brew install postgresql
brew services start postgresql
/usr/local/opt/postgres/bin/createuser -s postgres
# Either execute db-init.sh, or manually initialize the postgres database:
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
```
### Run a local development instance
```
cd lemmy
cargo run
```
Then open [localhost:1235](http://localhost:1235) in your browser. To reload back-end changes, you will have to rerun `cargo run`. You can use `cargo check` as a faster way to find compilation errors.
To do front end development:
```
cd lemmy-ui
yarn
yarn dev
```
and go to [localhost:1234](http://localhost:1234). Front end saves should rebuild the project.
Note that this setup doesn't include image uploads or link previews (provided by pict-rs and
iframely respectively). If you want to test those, you should use the
[Docker development](docker_development.md).

20
src/contributing/tests.md Normal file
View file

@ -0,0 +1,20 @@
### Tests
#### Rust
After installing [local development dependencies](local_development.md), run the
following commands:
```bash
psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
./test.sh
```
### Federation
Install the [Docker development dependencies](docker_development.md), and execute:
```
cd docker/federation
./run-tests.bash
```

View file

@ -0,0 +1,19 @@
# Theming Guide
Lemmy uses [Bootstrap v4](https://getbootstrap.com/), and very few custom css classes, so any bootstrap v4 compatible theme should work fine.
## Creating
- Use a tool like [bootstrap.build](https://bootstrap.build/) to create a bootstrap v4 theme. Export the `bootstrap.min.css` once you're done, and save the `_variables.scss` too.
## Testing
- To test out a theme, you can either use your browser's web tools, or a plugin like stylus to copy-paste a theme, when viewing Lemmy.
## Adding
1. Fork the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui).
1. Copy `{my-theme-name}.min.css` to `src/assets/css/themes`. (You can also copy the `_variables.scss` here if you want).
1. Go to `src/shared/utils.ts` and add `{my-theme-name}` to the themes list.
1. Test locally
1. Do a pull request with those changes.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
# Federation Administration
Note: ActivityPub federation is still under development. We recommend that you only enable it on test instances for now.
To enable federation, change the setting `federation.enabled` to `true` in `lemmy.hjson`, and restart Lemmy.
Federation does not start automatically, but needs to be triggered manually through the search. To do this you have to enter a reference to a remote object, such as:
- `!main@lemmy.ml` (Community)
- `@nutomic@lemmy.ml` (User)
- `https://lemmy.ml/c/programming` (Community)
- `https://lemmy.ml/u/nutomic` (User)
- `https://lemmy.ml/post/123` (Post)
- `https://lemmy.ml/comment/321` (Comment)
For an overview of how federation in Lemmy works on a technical level, check out our [Federation Overview](contributing_federation_overview.md).
## Instance allowlist and blocklist
The federation section of Lemmy's config has two variables `allowed_instances` and `blocked_instances`. These control which other instances Lemmy will federate with. Both settings take a comma separated list of domains, eg `lemmy.ml,example.com`. You can either change those settings via `/admin`, or directly on the server filesystem.
It is important to note that these settings only affect sending and receiving of data between instances. If allow federation with a certain instance, and then remove it from the allowlist, this will not affect previously federated data. These communities, users, posts and comments will still be shown. They will just not be updated anymore. And even if an instance is blocked, it can still fetch and display public data from your instance.
By default, both `allowed_instances` and `blocked_instances` values are empty, which means that Lemmy will federate with every compatible instance. We do not recommend this, because the moderation tools are not yet ready to deal with malicious instances.
What we do recommend is putting a list of trusted instances into `allowed_instances`, and only federating with those. Note that both sides need to add each other to their `allowed_instances` to allow two-way federation.
Alternatively you can also use blocklist based federation. In this case, add the domains of instances you do *not* want to federate with. You can only set one of `allowed_instances` and `blocked_instances`, as setting both doesn't make sense.

View file

@ -0,0 +1,16 @@
# Federation
Lemmy uses the ActivityPub protocol (a W3C standard) to enable federation between different servers (often called instances). This is very similar to the way email works. For example, if you use gmail.com, then you can not only send mails to other gmail.com users, but also to yahoo.com, yandex.ru and so on. Email uses the SMTP protocol to achieve this, so you can think of ActivityPub as "SMTP for social media". The amount of different actions possible on social media (post, comment, like, share, etc) means that ActivityPub is much more complicated than SMTP.
As with email, ActivityPub federation happens only between servers. So if you are registered on `enterprise.lemmy.ml`, you only connect to the API of `enterprise.lemmy.ml`, while the server takes care of sending and receiving data from other instances (eg `voyager.lemmy.ml`). The great advantage of this approach is that the average user doesn't have to do anything to use federation. In fact if you are using Lemmy, you are likely already using it. One way to confirm is by going to a community or user profile. If you are on `enterprise.lemmy.ml` and you see a user like `@nutomic@voyager.lemmy.ml`, or a community like `!main@ds9.lemmy.ml`, then those are federated, meaning they use a different instance from yours.
One way you can take advantage of federation is by opening a different instance, like `ds9.lemmy.ml`, and browsing it. If you see an interesting community, post or user that you want to interact with, just copy its URL and paste it into the search of your own instance. Your instance will connect to the other one (assuming the allowlist/blocklist allows it), and directly display the remote content to you, so that you can follow a community or comment on a post. Here are some examples of working searches:
- `!main@lemmy.ml` (Community)
- `@nutomic@lemmy.ml` (User)
- `https://lemmy.ml/c/programming` (Community)
- `https://lemmy.ml/u/nutomic` (User)
- `https://lemmy.ml/post/123` (Post)
- `https://lemmy.ml/comment/321` (Comment)
You can see the list of linked instances by following the "Instances" link at the bottom of any Lemmy page.

View file

@ -0,0 +1,694 @@
# Lemmy Federation Protocol
The Lemmy Protocol (or Lemmy Federation Protocol) is a strict subset of the [ActivityPub Protocol](https://www.w3.org/TR/activitypub/). Any deviation from the ActivityPub protocol is a bug in Lemmy or in this documentation (or both).
This document is targeted at developers who are familiar with the ActivityPub and ActivityStreams protocols. It gives a detailed outline of the actors, objects and activities used by Lemmy.
Before reading this, have a look at our [Federation Overview](contributing_federation_overview.md) to get an idea how Lemmy federation works on a high level.
Lemmy does not yet follow the ActivityPub spec in all regards. For example, we don't set a valid context indicating our context fields. We also ignore fields like `inbox`, `outbox` or `endpoints` for remote actors, and assume that everything is Lemmy. For an overview of deviations, read [#698](https://github.com/LemmyNet/lemmy/issues/698). They will be fixed in the near future.
Lemmy is also really inflexible when it comes to incoming activities and objects. They need to be exactly identical to the examples below. Things like having an array instead of a single value, or an object ID instead of the full object will result in an error.
In the following tables, "mandatory" refers to whether or not Lemmy will accept an incoming activity without this field. Lemmy itself will always include all non-empty fields.
<!-- toc -->
- [Actors](#actors)
* [Community](#community)
* [User](#user)
- [Objects](#objects)
* [Post](#post)
* [Comment](#comment)
* [Private Message](#private-message)
- [Activities](#activities)
* [Follow](#follow)
* [Accept Follow](#accept-follow)
* [Unfollow](#unfollow)
* [Create or Update Post](#create-or-update-post)
* [Create or Update Comment](#create-or-update-comment)
* [Like Post or Comment](#like-post-or-comment)
* [Dislike Post or Comment](#dislike-post-or-comment)
* [Delete Post or Comment](#delete-post-or-comment)
* [Remove Post or Comment](#remove-post-or-comment)
* [Undo](#undo)
* [Announce](#announce)
* [Create or Update Private message](#create-or-update-private-message)
* [Delete Private Message](#delete-private-message)
* [Undo Delete Private Message](#undo-delete-private-message)
<!-- tocstop -->
## Actors
### Community
An automated actor. Users can send posts or comments to it, which the community forwards to its followers in the form of `Announce`.
Sends activities to user: `Accept/Follow`, `Announce`
Receives activities from user: `Follow`, `Undo/Follow`, `Create`, `Update`, `Like`, `Dislike`, `Remove` (only admin/mod), `Delete` (only creator), `Undo` (only for own actions)
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/c/main",
"type": "Group",
"preferredUsername": "main",
"name": "The Main Community",
"category": {
"identifier": "1",
"name": "Discussion"
},
"sensitive": false,
"attributedTo": [
"https://enterprise.lemmy.ml/u/picard",
"https://enterprise.lemmy.ml/u/riker"
],
"content": "Welcome to the default community!",
"source": {
"content": "Welcome to the default community!",
"mediaType": "text/markdown"
},
"icon": {
"type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/Z8pFFb21cl.png"
},
"image": {
"type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/Wt8zoMcCmE.jpg"
},
"inbox": "https://enterprise.lemmy.ml/c/main/inbox",
"outbox": "https://enterprise.lemmy.ml/c/main/outbox",
"followers": "https://enterprise.lemmy.ml/c/main/followers",
"endpoints": {
"sharedInbox": "https://enterprise.lemmy.ml/inbox"
},
"published": "2020-10-06T17:27:43.282386+00:00",
"updated": "2020-10-08T11:57:50.545821+00:00",
"publicKey": {
"id": "https://enterprise.lemmy.ml/c/main#main-key",
"owner": "https://enterprise.lemmy.ml/c/main",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9JJ7Ybp/H7iXeLkWFepg\ny4PHyIXY1TO9rK3lIBmAjNnkNywyGXMgUiiVhGyN9yU7Km8aWayQsNHOkPL7wMZK\nnY2Q+CTQv49kprEdcDVPGABi6EbCSOcRFVaUjjvRHf9Olod2QP/9OtX0oIFKN2KN\nPIUjeKK5tw4EWB8N1i5HOuOjuTcl2BXSemCQLAlXerLjT8xCarGi21xHPaQvAuns\nHt8ye7fUZKPRT10kwDMapjQ9Tsd+9HeBvNa4SDjJX1ONskNh2j4bqHHs2WUymLpX\n1cgf2jmaXAsz6jD9u0wfrLPelPJog8RSuvOzDPrtwX6uyQOl5NK00RlBZwj7bMDx\nzwIDAQAB\n-----END PUBLIC KEY-----\n"
}
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `preferredUsername` | yes | Name of the actor |
| `name` | yes | Title of the community |
| `category` | yes | Hardcoded list of categories, see https://lemmy.ml/api/v1/categories |
| `sensitive` | yes | True indicates that all posts in the community are nsfw |
| `attributedTo` | yes | First the community creator, then all the remaining moderators |
| `content` | no | Text for the community sidebar, usually containing a description and rules |
| `icon` | no | Icon, shown next to the community name |
| `image` | no | Banner image, shown on top of the community page |
| `inbox` | no | ActivityPub inbox URL |
| `outbox` | no | ActivityPub outbox URL, only contains up to 20 latest posts, no comments, votes or other activities |
| `followers` | no | Follower collection URL, only contains the number of followers, no references to individual followers |
| `endpoints` | no | Contains URL of shared inbox |
| `published` | no | Datetime when the community was first created |
| `updated` | no | Datetime when the community was last changed |
| `publicKey` | yes | The public key used to verify signatures from this actor |
### User
A person, interacts primarily with the community where it sends and receives posts/comments. Can also create and moderate communities, and send private messages to other users.
Sends activities to Community: `Follow`, `Undo/Follow`, `Create`, `Update`, `Like`, `Dislike`, `Remove` (only admin/mod), `Delete` (only creator), `Undo` (only for own actions)
Receives activities from Community: `Accept/Follow`, `Announce`
Sends and receives activities from/to other users: `Create/Note`, `Update/Note`, `Delete/Note`, `Undo/Delete/Note` (all those related to private messages)
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/u/picard",
"type": "Person",
"preferredUsername": "picard",
"name": "Jean-Luc Picard",
"content": "The user bio",
"source": {
"content": "The user bio",
"mediaType": "text/markdown"
},
"icon": {
"type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/DS3q0colRA.jpg"
},
"image": {
"type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/XenaYI5hTn.png"
},
"inbox": "https://enterprise.lemmy.ml/u/picard/inbox",
"endpoints": {
"sharedInbox": "https://enterprise.lemmy.ml/inbox"
},
"published": "2020-10-06T17:27:43.234391+00:00",
"updated": "2020-10-08T11:27:17.905625+00:00",
"publicKey": {
"id": "https://enterprise.lemmy.ml/u/picard#main-key",
"owner": "https://enterprise.lemmy.ml/u/picard",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyH9iH83+idw/T4QpuRSY\n5YgQ/T5pJCNxvQWb6qcCu3gEVigfbreqZKJpOih4YT36wu4GjPfoIkbWJXcfcEzq\nMEQoYbPStuwnklpN2zj3lRIPfGLht9CAlENLWikTUoW5kZLyU6UQtOGdT2b1hDuK\nsUEn67In6qYx6pal8fUbO6X3O2BKzGeofnXgHCu7QNIuH4RPzkWsLhvwqEJYP0zG\nodao2j+qmhKFsI4oNOUCGkdJejO7q+9gdoNxAtNNKilIOwUFBYXeZJb+XGlzo0X+\n70jdJ/xQCPlPlItU4pD/0FwPLtuReoOpMzLi20oDsPXJBvn+/NJaxqDINuywcN5p\n4wIDAQAB\n-----END PUBLIC KEY-----\n"
}
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `preferredUsername` | yes | Name of the actor |
| `name` | no | The user's displayname |
| `content` | no | User bio |
| `icon` | no | The user's avatar, shown next to the username |
| `image` | no | The user's banner, shown on top of the profile |
| `inbox` | no | ActivityPub inbox URL |
| `endpoints` | no | Contains URL of shared inbox |
| `published` | no | Datetime when the user signed up |
| `updated` | no | Datetime when the user profile was last changed |
| `publicKey` | yes | The public key used to verify signatures from this actor |
## Objects
### Post
A page with title, and optional URL and text content. The URL often leads to an image, in which case a thumbnail is included. Each post belongs to exactly one community.
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://voyager.lemmy.ml/post/29",
"type": "Page",
"attributedTo": "https://voyager.lemmy.ml/u/picard",
"to": "https://voyager.lemmy.ml/c/main",
"summary": "Test thumbnail 2",
"content": "blub blub",
"source": {
"content": "blub blub",
"mediaType": "text/markdown"
},
"url": "https://voyager.lemmy.ml:/pictrs/image/fzGwCsq7BJ.jpg",
"image": {
"type": "Image",
"url": "https://voyager.lemmy.ml/pictrs/image/UejwBqrJM2.jpg"
},
"commentsEnabled": true,
"sensitive": false,
"stickied": false,
"published": "2020-09-24T17:42:50.396237+00:00",
"updated": "2020-09-24T18:31:14.158618+00:00"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `attributedTo` | yes | ID of the user which created this post |
| `to` | yes | ID of the community where it was posted to |
| `summary` | yes | Title of the post |
| `content` | no | Body of the post |
| `url` | no | An arbitrary link to be shared |
| `image` | no | Thumbnail for `url`, only present if it is an image link |
| `commentsEnabled` | yes | False indicates that the post is locked, and no comments can be added |
| `sensitive` | yes | True marks the post as NSFW, blurs the thumbnail and hides it from users with NSFW settign disabled |
| `stickied` | yes | True means that it is shown on top of the community |
| `published` | no | Datetime when the post was created |
| `updated` | no | Datetime when the post was edited (not present if it was never edited) |
### Comment
A reply to a post, or reply to another comment. Contains only text (including references to other users or communities). Lemmy displays comments in a tree structure.
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/comment/95",
"type": "Note",
"attributedTo": "https://enterprise.lemmy.ml/u/picard",
"to": "https://enterprise.lemmy.ml/c/main",
"content": "mmmk",
"source": {
"content": "mmmk",
"mediaType": "text/markdown"
},
"inReplyTo": [
"https://enterprise.lemmy.ml/post/38",
"https://voyager.lemmy.ml/comment/73"
],
"published": "2020-10-06T17:53:22.174836+00:00",
"updated": "2020-10-06T17:53:22.174836+00:00"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `attributedTo` | yes | ID of the user who created the comment |
| `to` | yes | Community where the comment was made |
| `content` | yes | The comment text |
| `inReplyTo` | yes | IDs of the post where this comment was made, and the parent comment. If this is a top-level comment, `inReplyTo` only contains the post |
| `published` | no | Datetime when the comment was created |
| `updated` | no | Datetime when the comment was edited (not present if it was never edited) |
### Private Message
A direct message from one user to another. Can not include additional users. Threading is not implemented yet, so the `inReplyTo` field is missing.
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/private_message/34",
"type": "Note",
"attributedTo": "https://enterprise.lemmy.ml/u/picard",
"to": "https://voyager.lemmy.ml/u/janeway",
"content": "test",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"mediaType": "text/markdown",
"published": "2020-10-08T19:10:46.542820+00:00",
"updated": "2020-10-08T20:13:52.547156+00:00"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `attributedTo` | ID of the user who created this private message |
| `to` | ID of the recipient |
| `content` | yes | The text of the private message |
| `published` | no | Datetime when the message was created |
| `updated` | no | Datetime when the message was edited (not present if it was never edited) |
## Activities
### Follow
When the user clicks "Subscribe" in a community, a `Follow` is sent. The community automatically responds with an `Accept/Follow`.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/follow/2e4784b7-4edf-4fa1-a352-674d5d5f8891",
"type": "Follow",
"actor": "https://enterprise.lemmy.ml/u/picard",
"to": "https://ds9.lemmy.ml/c/main",
"object": "https://ds9.lemmy.ml/c/main"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `actor` | yes | The user that is sending the follow request |
| `object` | yes | The community to be followed |
### Accept Follow
Automatically sent by the community in response to a `Follow`. At the same time, the community adds this user to its followers list.
Sent by: Community
Sent to: User
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/accept/5314bf7c-dab8-4b01-baf2-9be11a6a812e",
"type": "Accept",
"actor": "https://ds9.lemmy.ml/c/main",
"to": "https://enterprise.lemmy.ml/u/picard",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/follow/2e4784b7-4edf-4fa1-a352-674d5d5f8891",
"type": "Follow",
"object": "https://ds9.lemmy.ml/c/main",
"actor": "https://enterprise.lemmy.ml/u/picard"
}
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `actor` | yes | The same community as in the `Follow` activity |
| `to` | no | ID of the user which sent the `Follow` |
| `object` | yes | The previously sent `Follow` activity |
### Unfollow
Clicking on the unsubscribe button in a community causes an `Undo/Follow` to be sent. The community removes the user from its follower list after receiving it.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://lemmy-alpha:8541/activities/undo/2c624a77-a003-4ed7-91cb-d502eb01b8e8",
"type": "Undo",
"actor": "http://lemmy-alpha:8541/u/lemmy_alpha",
"to": "http://lemmy-beta:8551/c/main",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://lemmy-alpha:8541/activities/follow/f0d732e7-b1e7-4857-a5e0-9dc83c3f7ee8",
"type": "Follow",
"actor": "http://lemmy-alpha:8541/u/lemmy_alpha",
"object": "http://lemmy-beta:8551/c/main"
}
}
```
### Create or Update Post
When a user creates a new post, it is sent to the respective community. Editing a previously created post sends an almost identical activity, except the `type` being `Update`. We don't support mentions in posts yet.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/create/6e11174f-501a-4531-ac03-818739bfd07d",
"type": "Create",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/"
],
"object": ...
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `type` | yes | either `Create` or `Update` |
| `cc` | yes | Community where the post is being made |
| `object` | yes | The post being created |
### Create or Update Comment
A reply to a post, or to another comment. Can contain mentions of other users. Editing a previously created post sends an almost identical activity, except the `type` being `Update`.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/create/6f52d685-489d-4989-a988-4faedaed1a70",
"type": "Create",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"tag": [{
"type": "Mention",
"name": "@sisko@ds9.lemmy.ml",
"href": "https://ds9.lemmy.ml/u/sisko"
}],
"cc": [
"https://ds9.lemmy.ml/c/main/",
"https://ds9.lemmy.ml/u/sisko"
],
"object": ...
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `tag` | no | List of users which are mentioned in the comment (like `@user@example.com`) |
| `cc` | yes | Community where the post is being made, the user being replied to (creator of the parent post/comment), as well as any mentioned users |
| `object` | yes | The comment being created |
### Like Post or Comment
An upvote for a post or comment.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/like/8f3f48dd-587d-4624-af3d-59605b7abad3",
"type": "Like",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/"
],
"object": "https://enterprise.lemmy.ml/p/123"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `cc` | yes | ID of the community where the post/comment is |
| `object` | yes | The post or comment being upvoted |
### Dislike Post or Comment
A downvote for a post or comment.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/dislike/fd2b8e1d-719d-4269-bf6b-2cadeebba849",
"type": "Dislike",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/"
],
"object": "https://enterprise.lemmy.ml/p/123"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `cc` | yes | ID of the community where the post/comment is |
| `object` | yes | The post or comment being upvoted |
### Delete Post or Comment
Deletes a previously created post or comment. This can only be done by the original creator of that post/comment.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://enterprise.lemmy.ml/activities/delete/f1b5d57c-80f8-4e03-a615-688d552e946c",
"type": "Delete",
"actor": "https://enterprise.lemmy.ml/u/riker",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://enterprise.lemmy.ml/c/main/"
],
"object": "https://enterprise.lemmy.ml/post/32"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `cc` | yes | ID of the community where the post/comment is |
| `object` | yes | ID of the post or comment being deleted |
### Remove Post or Comment
Removes a post or comment. This can only be done by a community mod, or by an admin on the instance where the community is hosted.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/remove/aab93b8e-3688-4ea3-8212-d00d29519218",
"type": "Remove",
"actor": "https://ds9.lemmy.ml/u/sisko",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/"
],
"object": "https://enterprise.lemmy.ml/comment/32"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `cc` | yes | ID of the community where the post/comment is |
| `object` | yes | ID of the post or comment being removed |
### Undo
Reverts a previous activity, can only be done by the `actor` of `object`. In case of a `Like` or `Dislike`, the vote count is changed back. In case of a `Delete` or `Remove`, the post/comment is restored. The `object` is regenerated from scratch, as such the activity ID and other fields are different.
Sent by: User
Sent to: Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/undo/70ca5fb2-e280-4fd0-a593-334b7f8a5916",
"type": "Undo",
"actor": "https://ds9.lemmy.ml/u/sisko",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/"
],
"object": ...
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `object` | yes | Any `Like`, `Dislike`, `Delete` or `Remove` activity as described above |
### Announce
When the community receives a post or comment activity, it wraps that into an `Announce` and sends it to all followers.
Sent by: Community
Sent to: User
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/announce/b98382e8-6cb1-469e-aa1f-65c5d2c31cc4",
"type": "Announce",
"actor": "https://ds9.lemmy.ml/c/main",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"https://ds9.lemmy.ml/c/main/followers"
],
"object": ...
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `object` | yes | Any `Create`, `Update`, `Like`, `Dislike`, `Delete` `Remove` or `Undo` activity as described above |
### Remove or Delete Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://ds9.lemmy.ml/activities/remove/e4ca7688-af9d-48b7-864f-765e7f9f3591",
"type": "Remove",
"actor": "http://ds9.lemmy.ml/c/some_community",
"cc": [
"http://ds9.lemmy.ml/c/some_community/followers"
],
"to": "https://www.w3.org/ns/activitystreams#Public",
"object": "http://ds9.lemmy.ml/c/some_community"
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `type` | yes | Either `Remove` or `Delete` |
### Restore Removed or Deleted Community
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://ds9.lemmy.ml/activities/like/0703668c-8b09-4a85-aa7a-f93621936901",
"type": "Undo",
"actor": "http://ds9.lemmy.ml/c/some_community",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"http://ds9.lemmy.ml/c/testcom/followers"
],
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://ds9.lemmy.ml/activities/remove/1062b5e0-07e8-44fc-868c-854209935bdd",
"type": "Remove",
"actor": "http://ds9.lemmy.ml/c/some_community",
"object": "http://ds9.lemmy.ml/c/testcom",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": [
"http://ds9.lemmy.ml/c/testcom/followers"
]
}
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `object.type` | yes | Either `Remove` or `Delete` |
### Create or Update Private message
Creates a new private message between two users.
Sent by: User
Sent to: User
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/create/202daf0a-1489-45df-8d2e-c8a3173fed36",
"type": "Create",
"actor": "https://ds9.lemmy.ml/u/sisko",
"to": "https://enterprise.lemmy.ml/u/riker/inbox",
"object": ...
}
```
| Field Name | Mandatory | Description |
|---|---|---|
| `type` | yes | Either `Create` or `Update` |
### Delete Private Message
Deletes a previous private message.
Sent by: User
Sent to: User
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/delete/2de5a5f3-bf26-4949-a7f5-bf52edfca909",
"type": "Delete",
"actor": "https://ds9.lemmy.ml/u/sisko",
"to": "https://enterprise.lemmy.ml/u/riker/inbox",
"object": "https://ds9.lemmy.ml/private_message/341"
}
```
### Undo Delete Private Message
Restores a previously deleted private message. The `object` is regenerated from scratch, as such the activity ID and other fields are different.
Sent by: User
Sent to: User
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://ds9.lemmy.ml/activities/undo/b24bc56d-5db1-41dd-be06-3f1db8757842",
"type": "Undo",
"actor": "https://ds9.lemmy.ml/u/sisko",
"to": "https://enterprise.lemmy.ml/u/riker/inbox",
"object": ...
}
```

127
src/federation/overview.md Normal file
View file

@ -0,0 +1,127 @@
# Federation Overview
This document is for anyone who wants to know how Lemmy federation works, without being overly technical. It is meant provide a high-level overview of ActivityPub federation in Lemmy. If you are implementing ActivityPub yourself and want to be compatible with Lemmy, read our [ActivityPub API outline](contributing_apub_api_outline.md).
## Documentation conventions
To keep things simple, sometimes you will see things formatted like `Create/Note` or `Delete/Event` or `Undo/Follow`. The thing before the slash is the Activity, and the thing after the slash is the Object inside the Activity, in an `object` property. So these are to be read as follows:
* `Create/Note`: a `Create` activity containing a `Note` in the `object` field
* `Delete/Event`: a `Delete` activity containing an `Event` in the `object` field
* `Undo/Follow`: an `Undo` activity containing a `Follow` in the `object` field
In Lemmy we use some specific terms to refer to ActivityPub items. They are essentially our specific implementations of well-known ActivityPub concepts:
- Community: `Group`
- User: `Person`
- Post: `Page`
- Comment: `Note`
This document has three main sections:
* __Federation philosophy__ lays out the general model of how this is intended to federate
* __User Activities__ describes which actions that a User can take to interact
* __Community Activities__ describes what the Community does in response to certain User actions
## Federation philosophy
The primary Actor in Lemmy is the Community. Each community resides on a single instance, and consists of a list of Posts and a list of followers. The primary interaction is that of a User sending a Post or Comment related activity to the Community inbox, which then announces it to all its followers.
Each Community has a specific creator User, who is responsible for setting rules, appointing moderators, and removing content that violates the rules.
Besides moderation on the community level, each instance has a set of administrator Users, who have the power to do site-wide removals and bans.
Users follow Communities that they are interested in, in order to receive Posts and Comments. They also vote on Posts and Comments, as well as creating new ones. Comments are organised in a tree structure and commonly sorted by number of votes. Direct messages between Users are also supported.
Users can not follow each other, and neither can Communities follow anything.
Our federation implementation is already feature complete, but so far we haven't focused at all on complying with the ActivityPub spec. As such, Lemmy is likely not compatible with implementations which expect to send and receive valid activities. This is something we plan to fix in the near future. Check out [#698](https://github.com/LemmyNet/lemmy/issues/698) for an overview of our deviations.
## User Activities
### Follow a Community
Each Community page has a "Follow" button. Clicking this triggers a `Follow` activity to be sent from the user to the Community inbox. The Community will automatically respond with an `Accept/Follow` activity to the user inbox. It will also add the user to its list of followers, and deliver any activities about Posts/Comments in the Community to the user.
### Unfollow a Community
After following a Community, the "Follow" button is replaced by "Unfollow". Clicking this sends an `Undo/Follow` activity to the Community inbox. The Community removes the User from its followers list and doesn't send any activities to it anymore.
### Create a Post
When a user creates a new Post in a given Community, it is sent as `Create/Page` to the Community
inbox.
### Create a Comment
When a new Comment is created for a Post, both the Post ID and the parent Comment ID (if it exists)
are written to the `in_reply_to` field. This allows assigning it to the correct Post, and building
the Comment tree. It is then sent to the Community inbox as `Create/Note`
The origin instance also scans the Comment for any User mentions, and sends the `Create/Note` to
those Users as well.
### Edit a Post
Changes the content of an existing Post. Can only be done by the creating User.
### Edit a Comment
Changes the content of an existing Comment. Can only be done by the creating User.
### Likes and Dislikes
Users can like or dislike any Post or Comment. These are sent as `Like/Page`, `Dislike/Note` etc to the Community inbox.
### Deletions
The creator of a Post, Comment or Community can delete it. It is then sent to the Community followers. The item is then hidden from all users.
### Removals
Mods can remove Posts and Comments from their Communities. Admins can remove any Posts or Comments on the entire site. Communities can also be removed by admins. The item is then hidden from all users.
Removals are sent to all followers of the Community, so that they also take effect there. The exception is if an admin removes an item from a Community which is hosted on a different instance. In this case, the removal only takes effect locally.
### Revert a previous Action
We don't delete anything from our database, just hide it from users. Deleted or removed Communities/Posts/Comments have a "restore" button. This button generates an `Undo` activity which sets the original delete/remove activity as object, such as `Undo/Remove/Post` or `Undo/Delete/Community`.
Clicking on the upvote button of an already upvoted post/comment (or the downvote button of an already downvoted post/comment) also generates an `Undo`. In this case and `Undo/Like/Post` or `Undo/Dislike/Comment`.
### Create private message
User profiles have a "Send Message" button, which opens a dialog permitting to send a private message to this user. It is sent as a `Create/Note` to the user inbox. Private messages can only be directed at a single User.
### Edit private message
`Update/Note` changes the text of a previously sent message
### Delete private message
`Delete/Note` deletes a private message.
### Restore private message
`Undo/Delete/Note` reverts the deletion of a private message.
## Community Activities
The Community is essentially a bot, which will only do anything in reaction to actions from Users. The User who first created the Community becomes the first moderator, and can add additional moderators. In general, whenever the Community receives a valid activity in its inbox, that activity is forwarded to all its followers.
### Accept follow
If the Community receives a `Follow` activity, it automatically responds with `Accept/Follow`. It also adds the User to its list of followers.
### Unfollow
Upon receiving an `Undo/Follow`, the Community removes the User from its followers list.
### Announce
If the Community receives any Post or Comment related activity (Create, Update, Like, Dislike, Remove, Delete, Undo), it will Announce this to its followers. For this, an Announce is created with the Community as actor, and the received activity as object. Following instances thus stay updated about any actions in Communities they follow.
### Delete Community
If the creator or an admin deletes the Community, it sends a `Delete/Group` to all its followers.

View file

@ -0,0 +1,22 @@
# ActivityPub Resources
## Official Documents
- [ActivityPub standard](https://www.w3.org/TR/activitypub/)
- [Activitypub vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/)
## Explanations
- [ActivityPub - one protocol to rule them all?](https://schub.io/blog/2018/02/01/activitypub-one-protocol-to-rule-them-all.html)
- [A highly opinionated guide to learning about ActivityPub](https://tinysubversions.com/notes/reading-activitypub/)
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
- [Mastodon Blog: How to implement a basic ActivityPub server](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/)
- [Mastodon Blog: Implementing an ActivityPub inbox](https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/)
- [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579)
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)
## Examples and Libraries
- [ActivityPub example server](https://github.com/tOkeshu/activitypub-example)
- [ActivityStreams crate](https://docs.rs/activitystreams/)
- [HTTP Signatures crate](https://git.asonix.dog/Aardwolf/http-signature-normalization)

80
src/lemmy_council.md Normal file
View file

@ -0,0 +1,80 @@
# Lemmy Council
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
## 1. What gets voted on
This section describes all the aspects of Lemmy where the council has decision making power, namely:
- Coding direction
- Priorities / Emphasis
- Controversial features (For example, an unpopular feature should be removed)
- Moderation and conflict resolution on:
- [lemmy.ml](https://lemmy.ml/)
- [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy)
- [yerbamate.ml/LemmyNet/lemmy](https://yerbamate.ml/LemmyNet/lemmy)
- [weblate.yerbamate.ml/projects/lemmy/](https://weblate.yerbamate.ml/projects/lemmy/)
- Technical administration of lemmy.ml
- Official Lemmy accounts
- [Mastodon](https://mastodon.social/@LemmyDev)
- [Liberapay](https://liberapay.com/Lemmy/)
- [Patreon](https://www.patreon.com/dessalines)
- Council membership changes
- Changes to these rules
## 2. Feedback and Activity Reports
Every week, the council should make a thread on Lemmy that details its activity during the past week, be it development, moderation, or anything else mentioned in 1.
At the same time, users can give feedback and suggestions in this thread. This should be taken into account by the council. Council members can call for a vote on any controversial issues, if they can't be resolved by discussion.
## 3. Voting Process
Most of the time, we keep each other up to date through the Matrix chat, and take informal decisions on uncontroversial issues. For example, a user clearly violating the site rules could be banned by a single person, or ideally after discussing it with at least one other member.
If an issue can not be resolved in this way, then any council member can call for a vote, which works in the following way:
- Any council member can call for a vote, on any topic mentioned in 1.
- This should be used if there is any controversy in the community, or between council members.
- Before taking any decision, there needs to be a discussion where every council member can
explain their position.
- Discussion should be taken with the goal of reaching a compromise that is acceptable for
everyone.
- After the discussion, voting is done through Matrix emojis (👍: yes, 👎: no, X: abstain) and must
stay open for at least two days.
- All members of the Lemmy council have equal voting power.
- Decisions should be reached unanimously, or nearly so. If this is not possible, at least
2/3 of votes must be in favour for the motion to pass.
- Once a decision is reached in this way, every member needs to abide by it.
## 4. Joining
- We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council.
- Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole.
-> people should have joined at least a month ago.
- The member list is public.
- Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote.
- Limit growth to one new member per month at most.
## 5. Removing members
- Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this.
- Members that dont follow binding council decisions should be removed.
- Any member can be removed in a vote.
## 6. Goals
- We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups.
- The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc,
## 7. Communication
- A private Matrix chat for all council members.
- (Once private communities are done) A private community on lemmy.ml for issues.
## 8. Member List / Contact Info
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
- [Dessalines](https://lemmy.ml/u/dessalines)
- [Nutomic](https://lemmy.ml/u/nutomic)
- [AgreeableLandscape](https://lemmy.ml/u/AgreeableLandscape)
- [fruechtchen](https://lemmy.ml/u/fruechtchen)
- [kixiQu](https://lemmy.ml/u/kixiQu)
- [Karanja](https://baraza.africa/u/mwalimu)
- [zedi](https://lemmy.161.social/u/zedi)