About The Project

Front PagePost
main screenchat screen

Lemmy is similar to sites like Reddit, Lobste.rs, Raddle, or Hacker News: 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.

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?

Built With

Features

  • Open source, AGPL License.
  • Self hostable, easy to deploy.
  • 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.
    • Supports arm64 / Raspberry Pi.

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?.
    • 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

Activitypub guides

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.

Reddit Sorting

Reddit's comment sorting algorithm, the wilson confidence sort, is inadequate, because it completely ignores time. What ends up happening, especially in smaller subreddits, is that the early comments end up getting upvoted, and newer comments stay at the bottom, never to be seen. Research showed that nearly all top comments are just the first ones posted.

Hacker News Sorting

The Hacker New's ranking algorithm is great, but it doesn't use a log scale for the scores.

My Algorithm

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.

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

TypeDescription
HotShows trending posts, based on the score, and the most recent comment time.
NewNewest posts.
TopShows the highest scoring posts in the given time frame.

For more detail, check the Post and Comment Ranking details.

Markdown Guide

TypeOr… to Get
*Italic*_Italic_Italic
**Bold**__Bold__Bold
# Heading 1Heading 1
=========

Heading 1

## Heading 2Heading 2
---------
Heading 2
[Link](http://a.com)[Link][1]

[1]: http://b.org
Link
![Image](http://url/a.png)![Image][1]

[1]: http://url/b.jpg
Markdown
> Blockquote
Blockquote
* List
* List
* List
- List
- List
- List
* List
* List
* List
1. One
2. Two
3. Three
1) One
2) Two
3) Three
1. One
2. Two
3. Three
Horizontal Rule
---
Horizontal Rule
***
Horizontal Rule

`Inline code` with backticksInline code with backticks
```
# code block
print '3 backticks or'
print 'indent 4 spaces'
```
····# code block
····print '3 backticks or'
····print 'indent 4 spaces'
# code block
print '3 backticks or'
print 'indent 4 spaces'
::: spoiler hidden or nsfw stuff
a bunch of spoilers here
:::
hidden or nsfw stuff

a bunch of spoilers here

Some ~subscript~ textSome subscript text
Some ^superscript^ textSome superscript text

CommonMark Tutorial

Admin info

Information for Lemmy instance admins, and those who want to start an instance.

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,

# 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

After this, have a look at the config file named lemmy.hjson, and adjust it, in particular the hostname, and possibly the db password. Then run:

docker-compose up -d

To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. A sample nginx config, could be setup with:

wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf

You will also need to setup TLS, for example with Let's Encrypt. 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:

wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
docker-compose up -d

Ansible Installation

This is the same as the Docker installation, 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 (e.g. using sudo apt install ansible) or the equivalent for you platform.

Then run the following commands on your local computer:

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:

git pull origin main
cd ansible
ansible-playbook lemmy.yml --become

Kubernetes Installation

You'll need to have an existing Kubernetes cluster and storage class. Setting this up will vary depending on your provider. To try it locally, you can use MicroK8s or Minikube.

Once you have a working cluster, edit the environment variables and volume sizes in docker/k8s/*.yml. You may also want to change the service types to use LoadBalancers depending on where you're running your cluster (add type: LoadBalancer to ports), or NodePorts. By default they will use ClusterIPs, which will allow access only within the cluster. See the docs for more on networking in Kubernetes.

Important Running a database in Kubernetes will work, but is generally not recommended. If you're deploying on any of the common cloud providers, you should consider using their managed database service instead (RDS, Cloud SQL, Azure Databse, etc.).

Now you can deploy:

# Add `-n foo` if you want to deploy into a specific namespace `foo`;
# otherwise your resources will be created in the `default` namespace.
kubectl apply -f docker/k8s/db.yml
kubectl apply -f docker/k8s/pictshare.yml
kubectl apply -f docker/k8s/lemmy.yml

If you used a LoadBalancer, you should see it in your cloud provider's console.

Configuration

The configuration is based on the file 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.

To use a different config.hjson location than the current directory, set the environment variable LEMMY_CONFIG_LOCATION. Make sure you copy the defaults.hjson if you do this, otherwise you will be missing settings.

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:

cd server
./db-init.sh

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:

docker-compose exec postgres pg_dumpall -c -U lemmy >  lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql

A Sample backup script

#!/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

# 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

Contributing

Information about contributing to Lemmy, whether it is translating, testing, designing or programming.

Issue tracking / Repositories

Translating

Check out Lemmy's Weblate for translations.

Architecture

Front end

  • The front end is written in typescript, using a react-like framework called inferno. All UI elements are reusable .tsx components.
  • The main page and routing are in ui/src/index.tsx.
  • The components are located in ui/src/components.

Back end

  • The back end is written in rust, using diesel, and actix.
  • The server source code is split into main sections in server/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.

Docker Development

Running

sudo apt install git docker-compose
git clone https://github.com/LemmyNet/lemmy
cd lemmy/docker/dev
sudo docker-compose up --no-deps --build

and go to http://localhost:8536.

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 instead.

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 (rustup).

Then, install Homebrew if you don't already have it installed.

Finally, install Node and Yarn.

brew install node yarn

Get the source code

git clone https://github.com/LemmyNet/lemmy.git
# or alternatively from gitea
# git clone https://yerbamate.dev/LemmyNet/lemmy.git

All the following commands need to be run either in lemmy/server or lemmy/ui, as indicated by the cd command.

Build the backend (Rust)

cd server
cargo build
# for development, use `cargo check` instead)

Build the frontend (Typescript)

cd ui
yarn
yarn build

Setup postgresql

Ubuntu

sudo apt install postgresql
sudo systemctl start postgresql

# Either execute server/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 server/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

# run each of these in a seperate terminal
cd server && cargo run
cd ui && yarn start

Then open localhost:4444 in your browser. It will auto-refresh if you edit any frontend files. For backend coding, you will have to rerun cargo run. You can use cargo check as a faster way to find compilation errors.

To speed up incremental builds, you can add the following to ~/.cargo/config:

[target.x86_64-unknown-linux-gnu]
rustflags = ["-Clink-arg=-fuse-ld=lld"]

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.

Tests

Rust

After installing local development dependencies, run the following commands in the server subfolder:

psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
./test.sh

Federation

Install the Docker development dependencies, and execute docker/federation-test/run-tests.sh

Federation Development

Setup

If you don't have a local clone of the Lemmy repo yet, just run the following command:

git clone https://github.com/LemmyNet/lemmy

Running locally

You need to have the following packages installed, the Docker service needs to be running.

  • docker
  • docker-compose
  • cargo
  • yarn

Then run the following

cd docker/federation
./run-federation-test.bash -yarn

The federation test sets up 3 instances:

Instance / UsernameLocation
lemmy_alpha127.0.0.1:8540
lemmy_beta127.0.0.1:8550
lemmy_gamma127.0.0.1:8560

You can log into each using the instance name, and lemmy as the password, IE (lemmy_alpha, lemmy).

Firefox containers are a good way to test them interacting.

Integration tests

To run a suite of suite of federation integration tests:

cd docker/federation-test
./run-tests.sh

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 or manually. 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.

Lemmy API

Note: this may lag behind the actual API endpoints here. The API should be considered unstable and may change any time.

Data types

  • i16, i32 and i64 are respectively 16-bit, 32-bit and 64-bit integers.
  • Option<SomeType> designates an option which may be omitted in requests and not be present in responses. It will be of type SomeType.
  • Vec<SomeType> is a list which contains objects of type SomeType.
  • chrono::NaiveDateTime is a timestamp string in ISO 8601 format. Timestamps will be UTC.
  • Other data types are listed here.

Basic usage

Request and response strings are in JSON format.

WebSocket

Connect to ws://host/api/v1/ws to get started.

If the host supports secure connections, you can use wss://host/api/v1/ws.

Testing with Websocat

Websocat link

websocat ws://127.0.0.1:8536/api/v1/ws -nt

A simple test command: {"op": "ListCategories"}

Testing with the WebSocket JavaScript API

WebSocket JavaScript API

var ws = new WebSocket("ws://" + host + "/api/v1/ws");
ws.onopen = function () {
  console.log("Connection succeed!");
  ws.send(JSON.stringify({
    op: "ListCategories"
  }));
};

HTTP

Endpoints are at http://host/api/v1/endpoint. They'll be listed below for each action.

Testing with Curl

Get Example
curl /community/list?sort=Hot
Post Example
curl -i -H \
"Content-Type: application/json" \
-X POST \
-d '{
  "comment_id": X,
  "post_id": X,
  "score": X,
  "auth": "..."
}' \
/comment/like

Rate limits

  • 1 per hour for signups and community creation.
  • 1 per 10 minutes for post creation.
  • 30 actions per minute for post voting and comment creation.
  • Everything else is not rate-limited.

Errors


#![allow(unused_variables)]
fn main() {
{
  op: String,
  message: String,
}
}

API documentation

Sort Types

These go wherever there is a sort field. The available sort types are:

  • Active - the hottest posts/communities, depending on votes, and newest comment publish date.
  • Hot - the hottest posts/communities, depending on votes and publish date.
  • New - the newest posts/communities
  • TopDay - the most upvoted posts/communities of the current day.
  • TopWeek - the most upvoted posts/communities of the current week.
  • TopMonth - the most upvoted posts/communities of the current month.
  • TopYear - the most upvoted posts/communities of the current year.
  • TopAll - the most upvoted posts/communities on the current instance.

Undoing actions

Whenever you see a deleted: bool, removed: bool, read: bool, locked: bool, etc, you can undo this action by sending false.

Websocket vs HTTP

  • Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside data.
  • For example, an http login will be a POST {username_or_email: X, password: X}

User / Authentication / Admin actions

Login

The jwt string should be stored and used anywhere auth is called for.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "Login",
  data: {
    username_or_email: String,
    password: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "Login",
  data: {
    jwt: String,
  }
}
}
HTTP

POST /user/login

Register

Only the first user will be able to be the admin.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "Register",
  data: {
    username: String,
    email: Option<String>,
    password: String,
    password_verify: String,
    admin: bool,
    captcha_uuid: Option<String>, // Only checked if these are enabled in the server
    captcha_answer: Option<String>,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "Register",
  data: {
    jwt: String,
  }
}
}
HTTP

POST /user/register

Get Captcha

These expire after 10 minutes.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetCaptcha",
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetCaptcha",
  data: {
    ok?: { // Will be undefined if captchas are disabled
      png: String, // A Base64 encoded png
      wav: Option<String>, // A Base64 encoded wav audio file
      uuid: String,
    }
  }
}
}
HTTP

GET /user/get_captcha

Get User Details

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetUserDetails",
  data: {
    user_id: Option<i32>,
    username: Option<String>,
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    community_id: Option<i32>,
    saved_only: bool,
    auth: Option<String>,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetUserDetails",
  data: {
    user: UserView,
    follows: Vec<CommunityFollowerView>,
    moderates: Vec<CommunityModeratorView>,
    comments: Vec<CommentView>,
    posts: Vec<PostView>,
  }
}
}
HTTP

GET /user

Save User Settings

Request

#![allow(unused_variables)]
fn main() {
{
  op: "SaveUserSettings",
  data: {
    show_nsfw: bool,
    theme: String, // Default 'darkly'
    default_sort_type: i16, // The Sort types from above, zero indexed as a number
    default_listing_type: i16, // Post listing types are `All, Subscribed, Community`
    lang: String,
    avatar: Option<String>,
    banner: Option<String>,
    preferred_username: Option<String>,
    email: Option<String>,
    bio: Option<String>,
    matrix_user_id: Option<String>,
    new_password: Option<String>,
    new_password_verify: Option<String>,
    old_password: Option<String>,
    show_avatars: bool,
    send_notifications_to_email: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "SaveUserSettings",
  data: {
    jwt: String
  }
}
}
HTTP

PUT /save_user_settings

Get Replies / Inbox

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetReplies",
  data: {
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    unread_only: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetReplies",
  data: {
    replies: Vec<ReplyView>,
  }
}
}
HTTP

GET /user/replies

Get User Mentions

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetUserMentions",
  data: {
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    unread_only: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetUserMentions",
  data: {
    mentions: Vec<UserMentionView>,
  }
}
}
HTTP

GET /user/mentions

Mark User Mention as read

Only the recipient can do this.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "MarkUserMentionAsRead",
  data: {
    user_mention_id: i32,
    read: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "MarkUserMentionAsRead",
  data: {
    mention: UserMentionView,
  }
}
}
HTTP

POST /user/mention/mark_as_read

Get Private Messages

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetPrivateMessages",
  data: {
    unread_only: bool,
    page: Option<i64>,
    limit: Option<i64>,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetPrivateMessages",
  data: {
    messages: Vec<PrivateMessageView>,
  }
}
}
HTTP

GET /private_message/list

Create Private Message

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePrivateMessage",
  data: {
    content: String,
    recipient_id: i32,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePrivateMessage",
  data: {
    message: PrivateMessageView,
  }
}
}
HTTP

POST /private_message

Edit Private Message

Request

#![allow(unused_variables)]
fn main() {
{
  op: "EditPrivateMessage",
  data: {
    edit_id: i32,
    content: String,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "EditPrivateMessage",
  data: {
    message: PrivateMessageView,
  }
}
}
HTTP

PUT /private_message

Delete Private Message

Request

#![allow(unused_variables)]
fn main() {
{
  op: "DeletePrivateMessage",
  data: {
    edit_id: i32,
    deleted: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "DeletePrivateMessage",
  data: {
    message: PrivateMessageView,
  }
}
}
HTTP

POST /private_message/delete

Mark Private Message as Read

Only the recipient can do this.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "MarkPrivateMessageAsRead",
  data: {
    edit_id: i32,
    read: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "MarkPrivateMessageAsRead",
  data: {
    message: PrivateMessageView,
  }
}
}
HTTP

POST /private_message/mark_as_read

Mark All As Read

Marks all user replies and mentions as read.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "MarkAllAsRead",
  data: {
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "MarkAllAsRead",
  data: {
    replies: Vec<ReplyView>,
  }
}
}
HTTP

POST /user/mark_all_as_read

Delete Account

Permananently deletes your posts and comments

Request

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteAccount",
  data: {
    password: String,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteAccount",
  data: {
    jwt: String,
  }
}
}
HTTP

POST /user/delete_account

Add admin

Request

#![allow(unused_variables)]
fn main() {
{
  op: "AddAdmin",
  data: {
    user_id: i32,
    added: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "AddAdmin",
  data: {
    admins: Vec<UserView>,
  }
}
}
HTTP

POST /admin/add

Ban user

Request

#![allow(unused_variables)]
fn main() {
{
  op: "BanUser",
  data: {
    user_id: i32,
    ban: bool,
    reason: Option<String>,
    expires: Option<i64>,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "BanUser",
  data: {
    user: UserView,
    banned: bool,
  }
}
}
HTTP

POST /user/ban

Site

List Categories

Request

#![allow(unused_variables)]
fn main() {
{
  op: "ListCategories"
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "ListCategories",
  data: {
    categories: Vec<Category>
  }
}
}
HTTP

GET /categories

Search

Search types are All, Comments, Posts, Communities, Users, Url

Request

#![allow(unused_variables)]
fn main() {
{
  op: "Search",
  data: {
    q: String,
    type_: String,
    community_id: Option<i32>,
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    auth?: Option<String>,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "Search",
  data: {
    type_: String,
    comments: Vec<CommentView>,
    posts: Vec<PostView>,
    communities: Vec<CommunityView>,
    users: Vec<UserView>,
  }
}
}
HTTP

POST /search

Get Modlog

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetModlog",
  data: {
    mod_user_id: Option<i32>,
    community_id: Option<i32>,
    page: Option<i64>,
    limit: Option<i64>,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetModlog",
  data: {
    removed_posts: Vec<ModRemovePostView>,
    locked_posts: Vec<ModLockPostView>,
    removed_comments: Vec<ModRemoveCommentView>,
    removed_communities: Vec<ModRemoveCommunityView>,
    banned_from_community: Vec<ModBanFromCommunityView>,
    banned: Vec<ModBanView>,
    added_to_community: Vec<ModAddCommunityView>,
    added: Vec<ModAddView>,
  }
}
}
HTTP

GET /modlog

Create Site

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreateSite",
  data: {
    name: String,
    description: Option<String>,
    icon: Option<String>,
    banner: Option<String>,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreateSite",
    data: {
    site: SiteView,
  }
}
}
HTTP

POST /site

Edit Site

Request

#![allow(unused_variables)]
fn main() {
{
  op: "EditSite",
  data: {
    name: String,
    description: Option<String>,
    icon: Option<String>,
    banner: Option<String>,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "EditSite",
  data: {
    site: SiteView,
  }
}
}
HTTP

PUT /site

Get Site

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetSite"
  data: {
    auth: Option<String>,
  }

}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetSite",
  data: {
    site: Option<SiteView>,
    admins: Vec<UserView>,
    banned: Vec<UserView>,
    online: usize, // This is currently broken
    version: String,
    my_user: Option<User_>, // Gives back your user and settings if logged in
  }
}
}
HTTP

GET /site

Transfer Site

Request

#![allow(unused_variables)]
fn main() {
{
  op: "TransferSite",
  data: {
    user_id: i32,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "TransferSite",
  data: {
    site: Option<SiteView>,
    admins: Vec<UserView>,
    banned: Vec<UserView>,
  }
}
}
HTTP

POST /site/transfer

Get Site Config

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetSiteConfig",
  data: {
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetSiteConfig",
  data: {
    config_hjson: String,
  }
}
}
HTTP

GET /site/config

Save Site Config

Request

#![allow(unused_variables)]
fn main() {
{
  op: "SaveSiteConfig",
  data: {
    config_hjson: String,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "SaveSiteConfig",
  data: {
    config_hjson: String,
  }
}
}
HTTP

PUT /site/config

Community

Get Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetCommunity",
  data: {
    id: Option<i32>,
    name: Option<String>,
    auth: Option<String>
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetCommunity",
  data: {
    community: CommunityView,
    moderators: Vec<CommunityModeratorView>,
  }
}
}
HTTP

GET /community

Create Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreateCommunity",
  data: {
    name: String,
    title: String,
    description: Option<String>,
    icon: Option<String>,
    banner: Option<String>,
    category_id: i32 ,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreateCommunity",
  data: {
    community: CommunityView
  }
}
}
HTTP

POST /community

List Communities

Request

#![allow(unused_variables)]
fn main() {
{
  op: "ListCommunities",
  data: {
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    auth: Option<String>
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "ListCommunities",
  data: {
    communities: Vec<CommunityView>
  }
}
}
HTTP

GET /community/list

Ban from Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "BanFromCommunity",
  data: {
    community_id: i32,
    user_id: i32,
    ban: bool,
    reason: Option<String>,
    expires: Option<i64>,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "BanFromCommunity",
  data: {
    user: UserView,
    banned: bool,
  }
}
}
HTTP

POST /community/ban_user

Add Mod to Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "AddModToCommunity",
  data: {
    community_id: i32,
    user_id: i32,
    added: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "AddModToCommunity",
  data: {
    moderators: Vec<CommunityModeratorView>,
  }
}
}
HTTP

POST /community/mod

Edit Community

Only mods can edit a community.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "EditCommunity",
  data: {
    edit_id: i32,
    title: String,
    description: Option<String>,
    icon: Option<String>,
    banner: Option<String>,
    category_id: i32,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "EditCommunity",
  data: {
    community: CommunityView
  }
}
}
HTTP

PUT /community

Delete Community

Only a creator can delete a community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteCommunity",
  data: {
    edit_id: i32,
    deleted: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteCommunity",
  data: {
    community: CommunityView
  }
}
}
HTTP

POST /community/delete

Remove Community

Only admins can remove a community.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "RemoveCommunity",
  data: {
    edit_id: i32,
    removed: bool,
    reason: Option<String>,
    expires: Option<i64>,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "RemoveCommunity",
  data: {
    community: CommunityView
  }
}
}
HTTP

POST /community/remove

Follow Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "FollowCommunity",
  data: {
    community_id: i32,
    follow: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "FollowCommunity",
  data: {
    community: CommunityView
  }
}
}
HTTP

POST /community/follow

Get Followed Communities

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetFollowedCommunities",
  data: {
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetFollowedCommunities",
  data: {
    communities: Vec<CommunityFollowerView>
  }
}
}
HTTP

GET /user/followed_communities

Transfer Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "TransferCommunity",
  data: {
    community_id: i32,
    user_id: i32,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "TransferCommunity",
  data: {
    community: CommunityView,
    moderators: Vec<CommunityModeratorView>,
    admins: Vec<UserView>,
  }
}
}
HTTP

POST /community/transfer

Post

Create Post

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePost",
  data: {
    name: String,
    url: Option<String>,
    body: Option<String>,
    nsfw: bool,
    community_id: i32,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post

Get Post

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetPost",
  data: {
    id: i32,
    auth: Option<String>
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetPost",
  data: {
    post: PostView,
    comments: Vec<CommentView>,
    community: CommunityView,
    moderators: Vec<CommunityModeratorView>,
  }
}
}
HTTP

GET /post

Get Posts

Post listing types are All, Subscribed, Community

Request

#![allow(unused_variables)]
fn main() {
{
  op: "GetPosts",
  data: {
    type_: String,
    sort: String,
    page: Option<i64>,
    limit: Option<i64>,
    community_id: Option<i32>,
    community_name: Option<String>,
    auth: Option<String>
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "GetPosts",
  data: {
    posts: Vec<PostView>,
  }
}
}
HTTP

GET /post/list

Create Post Like

score can be 0, -1, or 1

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePostLike",
  data: {
    post_id: i32,
    score: i16,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreatePostLike",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/like

Edit Post

Request

#![allow(unused_variables)]
fn main() {
{
  op: "EditPost",
  data: {
    edit_id: i32,
    name: String,
    url: Option<String>,
    body: Option<String>,
    nsfw: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "EditPost",
  data: {
    post: PostView
  }
}
}
HTTP

PUT /post

Delete Post

Request

#![allow(unused_variables)]
fn main() {
{
  op: "DeletePost",
  data: {
    edit_id: i32,
    deleted: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "DeletePost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/delete

Remove Post

Only admins and mods can remove a post.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "RemovePost",
  data: {
    edit_id: i32,
    removed: bool,
    reason: Option<String>,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "RemovePost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/remove

Lock Post

Only admins and mods can lock a post.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "LockPost",
  data: {
    edit_id: i32,
    locked: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "LockPost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/lock

Sticky Post

Only admins and mods can sticky a post.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "StickyPost",
  data: {
    edit_id: i32,
    stickied: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "StickyPost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/sticky

Save Post

Request

#![allow(unused_variables)]
fn main() {
{
  op: "SavePost",
  data: {
    post_id: i32,
    save: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "SavePost",
  data: {
    post: PostView
  }
}
}
HTTP

POST /post/save

Comment

Create Comment

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreateComment",
  data: {
    content: String,
    parent_id: Option<i32>,
    post_id: i32,
    form_id: Option<String>, // An optional form id, so you know which message came back
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreateComment",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment

Edit Comment

Only the creator can edit the comment.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "EditComment",
  data: {
    content: String,
    edit_id: i32,
    form_id: Option<String>,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "EditComment",
  data: {
    comment: CommentView
  }
}
}
HTTP

PUT /comment

Delete Comment

Only the creator can delete the comment.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteComment",
  data: {
    edit_id: i32,
    deleted: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "DeleteComment",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment/delete

Remove Comment

Only a mod or admin can remove the comment.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "RemoveComment",
  data: {
    edit_id: i32,
    removed: bool,
    reason: Option<String>,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "RemoveComment",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment/remove

Mark Comment as Read

Only the recipient can do this.

Request

#![allow(unused_variables)]
fn main() {
{
  op: "MarkCommentAsRead",
  data: {
    edit_id: i32,
    read: bool,
    auth: String,
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "MarkCommentAsRead",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment/mark_as_read

Save Comment

Request

#![allow(unused_variables)]
fn main() {
{
  op: "SaveComment",
  data: {
    comment_id: i32,
    save: bool,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "SaveComment",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment/save

Create Comment Like

score can be 0, -1, or 1

Request

#![allow(unused_variables)]
fn main() {
{
  op: "CreateCommentLike",
  data: {
    comment_id: i32,
    score: i16,
    auth: String
  }
}
}
Response

#![allow(unused_variables)]
fn main() {
{
  op: "CreateCommentLike",
  data: {
    comment: CommentView
  }
}
}
HTTP

POST /comment/like

RSS / Atom feeds

All

/feeds/all.xml?sort=Hot

Community

/feeds/c/community-name.xml?sort=Hot

User

/feeds/u/user-name.xml?sort=Hot

Activitypub API outline

Actors

User / Person

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Person",
  "id": "https://instance_url/api/v1/user/sally_smith",
  "inbox": "https://instance_url/api/v1/user/sally_smith/inbox",
  "outbox": "https://instance_url/api/v1/user/sally_smith/outbox",
  "liked": "https://instance_url/api/v1/user/sally_smith/liked",
  // TODO disliked?
  "following": "https://instance_url/api/v1/user/sally_smith/following",
  "name": "sally_smith", 
  "preferredUsername": "Sally",
  "icon"?: {
    "type": "Image",
    "name": "User icon",
    "url": "https://instance_url/api/v1/user/sally_smith/icon.png",
    "width": 32,
    "height": 32
  },
  "published": "2014-12-31T23:00:00-08:00",
  "summary"?: "This is sally's profile."
}

Community / Group

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Group",
  "id": "https://instance_url/api/v1/community/today_i_learned",
  "name": "today_i_learned"
  "attributedTo": [ // The moderators
    "http://joe.example.org",
  ],
  "followers": "https://instance_url/api/v1/community/today_i_learned/followers",
  "published": "2014-12-31T23:00:00-08:00",
  "summary"?: "The group's tagline",
  "attachment: [{}] // TBD, these would be where strong types for custom styles, and images would work.
}

Objects

Post / Page

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Page",
  "id": "https://instance_url/api/v1/post/1",
  "name": "The title of a post, maybe a link to imgur",
  "url": "https://news.blah.com"
  "attributedTo": "http://joe.example.org", // The poster
  "published": "2014-12-31T23:00:00-08:00",
}

Post Listings / Ordered CollectionPage

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "OrderedCollectionPage",
  "id": "https://instance_url/api/v1/posts?type={all, best, front}&sort={}&page=1,
  "partOf": "http://example.org/foo",
  "orderedItems": [Posts]
}

Comment / Note

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Note",
  "id": "https://instance_url/api/v1/comment/1",
  "mediaType": "text/markdown",
  "content": "Looks like it is going to rain today. Bring an umbrella *if necessary*!"
  "attributedTo": john_id,
  "inReplyTo": "comment or post id",
  "published": "2014-12-31T23:00:00-08:00",
  "updated"?: "2014-12-12T12:12:12Z"
  "replies" // TODO, not sure if these objects should embed all replies in them or not.
  "to": [sally_id, group_id]
}

Comment Listings / Ordered CollectionPage

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "OrderedCollectionPage",
  "id": "https://instance_url/api/v1/comments?type={all,user,community,post,parent_comment}&id=1&page=1,
  "partOf": "http://example.org/foo",
  "orderedItems": [Comments]
}

Deleted thing / Tombstone

{
  "type": "Tombstone",
  "formerType": "Note / Post",
  "id": note / post_id,
  "deleted": "2016-03-17T00:00:00Z"
}

Actions

  • These are all posts to a user's outbox.
  • The server then creates a post to the necessary inbox of the recipient, or the followers.
  • Whenever a user accesses the site, they do a get from their inbox.

Comments

Create

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "object": comment_id, or post_id
}

Delete

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Delete",
  "actor": id,
  "object": comment_id, or post_id
}

Update

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "object": comment_id, or post_id
  "content": "New comment",
  "updated": "New Date"
}

Read

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Read",
  "actor": user_id
  "object": comment_id
}

Like

  • TODO: Should likes be notifications? IE, have a to?
{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Like",
  "actor": user_id
  "object": comment_id
  // TODO different types of reactions, or no?
}

Dislike

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Dislike",
  "actor": user_id
  "object": comment_id
  // TODO different types of reactions, or no?
}

Posts

Create

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "to": community_id/followers
  "object": post_id
}

Delete

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Delete",
  "actor": id,
  "object": comment_id, or post_id
}

Update

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "object": comment_id, or post_id
  TODO fields.
}

Read

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Read",
  "actor": user_id
  "object": post_id
}

Communities

Create

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "object": community_id
}

Delete

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Delete",
  "actor": id,
  "object": community_id
}

Update

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Create",
  "actor": id,
  "object": community_id
  TODO fields.
}

Follow / Subscribe

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Follow",
  "actor": id
  "object": community_id
}

Ignore/ Unsubscribe

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Follow",
  "actor": id
  "object": community_id
}

Join / Become a Mod

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Join",
  "actor": user_id,
  "object": community_id
}

Leave

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Leave",
  "actor": user_id,
  "object": community_id
}

Moderator

Ban user from community / Block

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Remove",
  "actor": mod_id,
  "object": user_id,
  "origin": group_id
}

Delete Comment

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Delete",
  "actor": id,
  "object": community_id
}

Invite a moderator

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Invite",
  "id": "https://instance_url/api/v1/invite/1",
  "actor": sally_id,
  "object": group_id,
  "target": john_id
}

Accept Invitation

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Accept",
  "actor": john_id,
  "object": invite_id
}

Reject Invitation

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Reject",
  "actor": john_id,
  "object": invite_id
}

Theming Guide

Lemmy uses Bootstrap v4, and very few custom css classes, so any bootstrap v4 compatible theme should work fine.

Creating

  • Use a tool like 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. Copy {my-theme-name}.min.css to ui/assets/css/themes. (You can also copy the _variables.scss here if you want).
  2. Go to ui/src/utils.ts and add {my-theme-name} to the themes list.
  3. Test locally
  4. Do a pull request with those changes.

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.
  • Council members are also added as administrators to any official Lemmy instances.

1. What gets voted on

This section describes all the aspects of Lemmy where the council has decision making power, namely:

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.

2. 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 dev.lemmy.ml for issues.

8. Member List / Contact Info

General Contact @LemmyDev Mastodon