├── core ├── __init__.py ├── jsonld.py ├── remote.py ├── db.py ├── indexes.py ├── feed.py ├── tasks.py ├── meta.py ├── outbox.py ├── notifications.py ├── shared.py ├── gc.py └── inbox.py ├── blueprints ├── __init__.py ├── well_known.py └── indieauth.py ├── data ├── .gitignore ├── mongodb │ └── .gitignore └── poussetaches │ └── .gitignore ├── sass ├── theme.scss ├── dark.scss ├── light.scss └── base_theme.scss ├── setup.cfg ├── static ├── emojis │ └── .gitignore ├── media │ └── .gitignore ├── nopic.png └── favicon.png ├── .dockerignore ├── .env ├── tests ├── fixtures │ ├── instance1 │ │ └── config │ │ │ ├── .gitignore │ │ │ └── me.yml │ ├── instance2 │ │ └── config │ │ │ ├── .gitignore │ │ │ └── me.yml │ └── me.yml └── integration_test.py ├── .isort.cfg ├── setup_wizard ├── requirements.txt ├── Dockerfile └── wizard.py ├── .gitignore ├── dev-requirements.txt ├── Dockerfile ├── docs ├── head.html ├── activitypub.md └── api.md ├── run.sh ├── startup.py ├── docker-compose-dev.yml ├── run_dev.sh ├── requirements.txt ├── templates ├── authorize_remote_follow.html ├── stream_debug.html ├── remote_follow.html ├── direct_messages.html ├── error.html ├── about.html ├── admin.html ├── u2f.html ├── login.html ├── followers.html ├── liked.html ├── admin_indieauth.html ├── note_debug.html ├── indieauth_flow.html ├── note.html ├── tags.html ├── header.html ├── lists.html ├── following.html ├── lookup.html ├── layout.html ├── admin_tasks.html ├── index.html ├── new.html └── stream.html ├── ENVVARS.md ├── utils ├── local_actor_cache.py ├── __init__.py ├── highlight.py ├── blacklist.py ├── key.py ├── webmentions.py ├── migrations.py ├── emojis.py ├── lookup.py ├── nodeinfo.py ├── opengraph.py ├── media.py └── template_filters.py ├── config └── me.sample.yml ├── docker-compose.yml ├── Makefile ├── .drone.yml ├── config.py └── README.md /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /data/mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /sass/theme.scss: -------------------------------------------------------------------------------- 1 | @import 'base_theme.scss' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /static/emojis/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /static/media/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /data/poussetaches/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | data/ 3 | data2/ 4 | tests/ 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | WEB_PORT=5005 2 | CONFIG_DIR=./config 3 | DATA_DIR=./data 4 | -------------------------------------------------------------------------------- /tests/fixtures/instance1/config/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixtures/instance2/config/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=120 3 | force_single_line=true 4 | -------------------------------------------------------------------------------- /setup_wizard/requirements.txt: -------------------------------------------------------------------------------- 1 | prompt_toolkit 2 | bcrypt 3 | markdown 4 | -------------------------------------------------------------------------------- /static/nopic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperezdc/microblog.pub/master/static/nopic.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aperezdc/microblog.pub/master/static/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[op] 2 | key_*.pem 3 | data/* 4 | config/* 5 | static/media/* 6 | 7 | .mypy_cache/ 8 | __pycache__/ 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/tsileo/little-boxes.git 2 | pytest 3 | requests 4 | html2text 5 | pyyaml 6 | flake8 7 | mypy 8 | black 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | COPY requirements.txt /app/requirements.txt 3 | WORKDIR /app 4 | RUN pip install -r requirements.txt 5 | ADD . /app 6 | ENV FLASK_APP=app.py 7 | CMD ["./run.sh"] 8 | -------------------------------------------------------------------------------- /setup_wizard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | WORKDIR /app 3 | ADD . /app 4 | RUN pip install -r requirements.txt 5 | LABEL maintainer="t@a4.io" 6 | LABEL pub.microblog.oneshot=true 7 | CMD ["python", "wizard.py"] 8 | -------------------------------------------------------------------------------- /sass/dark.scss: -------------------------------------------------------------------------------- 1 | $background-color: #060606; 2 | $background-light: #222; 3 | $color: #808080; 4 | $color-title-link: #fefefe; 5 | $color-summary: #ddd; 6 | $color-light: #bbb; 7 | $color-menu-background: #222; 8 | $color-note-link: #666; 9 | -------------------------------------------------------------------------------- /sass/light.scss: -------------------------------------------------------------------------------- 1 | $background-color: #eee; 2 | $background-light: #ccc; 3 | $color: #111; 4 | $color-title-link: #333; 5 | $color-light: #555; 6 | $color-summary: #111; 7 | $color-note-link: #333; 8 | $color-menu-background: #ddd; 9 | // $primary-color: #1d781d; 10 | -------------------------------------------------------------------------------- /tests/fixtures/me.yml: -------------------------------------------------------------------------------- 1 | username: 'ci' 2 | name: 'CI tests' 3 | icon_url: 'https://sos-ch-dk-2.exo.io/microblogpub/microblobpub.png' 4 | domain: 'localhost:5005' 5 | summary: 'test instance summary' 6 | pass: '$2b$12$nEgJMgaYbXSPOvgnqM4jSeYnleKhXqsFgv/o3hg12x79uEdsR4cUy' # hello 7 | https: false 8 | -------------------------------------------------------------------------------- /docs/head.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/instance1/config/me.yml: -------------------------------------------------------------------------------- 1 | username: 'instance1' 2 | name: 'Instance 1' 3 | icon_url: 'https://sos-ch-dk-2.exo.io/microblogpub/microblobpub.png' 4 | domain: 'instance1_web:5005' 5 | summary: 'instance1 summary' 6 | pass: '$2b$12$nEgJMgaYbXSPOvgnqM4jSeYnleKhXqsFgv/o3hg12x79uEdsR4cUy' # hello 7 | https: false 8 | hide_following: false 9 | -------------------------------------------------------------------------------- /tests/fixtures/instance2/config/me.yml: -------------------------------------------------------------------------------- 1 | username: 'instance2' 2 | name: 'Instance 2' 3 | icon_url: 'https://sos-ch-dk-2.exo.io/microblogpub/microblobpub.png' 4 | domain: 'instance2_web:5005' 5 | summary: 'instance2 summary' 6 | pass: '$2b$12$nEgJMgaYbXSPOvgnqM4jSeYnleKhXqsFgv/o3hg12x79uEdsR4cUy' # hello 7 | https: false 8 | hide_following: false 9 | -------------------------------------------------------------------------------- /docs/activitypub.md: -------------------------------------------------------------------------------- 1 | ## ActivityPub 2 | 3 | _microblog.pub_ implements an [ActivityPub](http://activitypub.rocks/) server, it implements both the client to server API and the federated server to server API. 4 | 5 | Activities are verified using HTTP Signatures or by fetching the content on the remote server directly. 6 | 7 | WebFinger is also required. 8 | 9 | TODO 10 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -c "import logging; logging.basicConfig(level=logging.DEBUG); from core import migrations; migrations.perform()" 3 | python -c "from core import indexes; indexes.create_indexes()" 4 | python startup.py 5 | (sleep 5 && curl -X POST -u :$POUSETACHES_AUTH_KEY $MICROBLOGPUB_POUSSETACHES_HOST/resume)& 6 | gunicorn -t 600 -w 5 -b 0.0.0.0:5005 --log-level debug app:app 7 | -------------------------------------------------------------------------------- /startup.py: -------------------------------------------------------------------------------- 1 | import app # noqa: F401 # here to init the backend 2 | from core.activitypub import _actor_hash 3 | from core.shared import MY_PERSON 4 | from core.shared import p 5 | from core.tasks import Tasks 6 | from utils.local_actor_cache import is_actor_updated 7 | 8 | h = _actor_hash(MY_PERSON, local=True) 9 | if is_actor_updated(h): 10 | Tasks.send_actor_update() 11 | 12 | p.push({}, "/task/cleanup", schedule="@every 1h") 13 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongo: 4 | image: "mongo:latest" 5 | volumes: 6 | - "./data:/data/db" 7 | ports: 8 | - "27017:27017" 9 | poussetaches: 10 | image: "poussetaches/poussetaches:latest" 11 | volumes: 12 | - "${DATA_DIR}/poussetaches:/app/poussetaches_data" 13 | environment: 14 | - POUSSETACHES_AUTH_KEY=${POUSSETACHES_AUTH_KEY} 15 | ports: 16 | - "7991:7991" 17 | -------------------------------------------------------------------------------- /run_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEV_POUSSETACHES_AUTH_KEY="1234567890" 3 | MICROBLOGPUB_INTERNAL_HOST="http://host.docker.internal:5005" 4 | 5 | 6 | env POUSSETACHES_AUTH_KEY=${DEV_POUSSETACHES_AUTH_KEY} docker-compose -f docker-compose-dev.yml up -d 7 | FLASK_DEBUG=1 MICROBLOGPUB_DEBUG=1 FLASK_APP=app.py POUSSETACHES_AUTH_KEY=${DEV_POUSSETACHES_AUTH_KEY} MICROBLOGPUB_INTERNAL_HOST=${MICROBLOGPUB_INTERNAL_HOST} flask run -p 5005 --with-threads 8 | docker-compose down 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cachetools 2 | poussetaches 3 | python-dateutil 4 | libsass 5 | tornado<6.0.0 6 | gunicorn 7 | piexif 8 | requests 9 | python-u2flib-server 10 | Flask 11 | Flask-WTF 12 | pymongo 13 | timeago 14 | bleach 15 | html2text 16 | feedgen 17 | itsdangerous 18 | bcrypt 19 | mf2py 20 | passlib 21 | git+https://github.com/erikriver/opengraph.git#egg=opengraph 22 | git+https://github.com/tsileo/little-boxes.git@litepub#egg=little_boxes 23 | pyyaml 24 | pillow 25 | emoji-unicode 26 | html5lib 27 | Pygments 28 | -------------------------------------------------------------------------------- /templates/authorize_remote_follow.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% import 'utils.html' as utils %} 3 | {% block header %} 4 | {% endblock %} 5 | {% block content %} 6 |
{{ item |remove_mongo_id|tojson(indent=4) }}
12 | {% endfor %}
13 |
14 | {{ utils.display_pagination(older_than, newer_than) }}
15 | Something went wrong :(
14 | {% if tb %} 15 |Please consider opening an issue on GitHub.
Here is the traceback:
18 | {{ tb }}
19 |
20 | {{action.scope}},{% endif %}redirect_uri={{action.redirect_uri}}).
17 | {% if action.token_expires %}
18 | {{action.token[:20]}}...
20 | {% if action.token_expires|gt_ts%}has expired on{% else %}expires{% endif %} {{ action.token_expires|format_ts }}
21 | {% endif %}
22 |
23 | {{ thread | remove_mongo_id | tojson(indent=4) }}
20 | wants you to login as {{ me }}
18 |Lists and its members are private.
12 |Manage list members in the Following section
22 | 23 |Interact with an ActivityPub object via its URL or look for a user using @user@domain.tld
9 | 10 | 14 | 15 | {% if data %} 16 | {% set data = data.to_dict() %} 17 |55 | 56 |
57 | {{ utils.display_note(meta.object, meta=meta) }} 58 | 59 | {% endif %} 60 || # | 14 |URL | 15 |Payload | 16 |Schedule | 17 |Next run | 18 |Response | 19 |
|---|---|---|---|---|---|
| {{ task.task_id }} | 26 |{{ task.url }} ({{ task.expected }}) | 27 |{{ task.payload }} | 28 |{{ task.schedule }} | 29 |{{ task.next_run }} | 30 |Tries #{{ task.tries }}: {{ task.last_error_body }} ({{ task.last_error_status_code }}) | 31 |
| # | 41 |URL | 42 |Payload | 43 |Next run | 44 |Response | 45 |
|---|---|---|---|---|
| {{ task.task_id }} | 52 |{{ task.url }} ({{ task.expected }}) | 53 |{{ task.payload }} | 54 |{{ task.next_run }} | 55 |Tries #{{ task.tries }}: {{ task.last_error_body }} ({{ task.last_error_status_code }}) | 56 |
| # | 67 |URL | 68 |Payload | 69 |Next run | 70 |Response | 71 |
|---|---|---|---|---|
| {{ task.task_id }} | 78 |{{ task.url }} ({{ task.expected }}) | 79 |{{ task.payload }} | 80 |{{ task.next_run }} | 81 |Tries #{{ task.tries }}: {{ task.last_error_body }} ({{ task.last_error_status_code }}) | 82 |
| # | 92 |URL | 93 |Payload | 94 |Next run | 95 |Response | 96 |
|---|---|---|---|---|
| {{ task.task_id }} | 103 |{{ task.url }} ({{ task.expected }}) | 104 |{{ task.payload }} | 105 |{{ task.next_run }} | 106 |Tries #{{ task.tries }}: {{ task.last_error_body }} ({{ task.last_error_status_code }}) | 107 |
27 | 28 |
29 | {% endif %} 30 | 31 | {{ utils.display_note(item.activity.object, meta=item.meta, no_color=True) }} 32 | {% endfor %} 33 | 34 | {% for item in outbox_data %} 35 | 36 | {% if item | has_type('Announce') %} 37 | {% if "actor" in item.meta %} 38 | {% set boost_actor = item.meta.actor %} 39 | {% if session.logged_in %} 40 |52 | {{ utils.display_actor_box(boost_actor, after="boosted") }} 53 | {{ utils.display_in_reply_to(item.meta, item.activity.object) }} 54 |
55 | {% endif %} 56 | {% endif %} 57 | {% if item.meta.object %} 58 | {{ utils.display_note(item.meta.object, meta=item.meta) }} 59 | {% endif %} 60 | {% elif item | has_type('Create') %} 61 | {% if item.activity.object.inReplyTo %} 62 |63 | {{ utils.display_in_reply_to(item.meta, item.activity.object) }} 64 |
65 | {% endif %} 66 | {{ utils.display_note(item.activity.object, meta=item.meta, no_color=True) }} 67 | {% endif %} 68 | 69 | {% endfor %} 70 | 71 | {{ utils.display_pagination(older_than, newer_than) }} 72 |
4 |
7 |
A self-hosted, single-user, ActivityPub powered microblog.
9 | 15 | 16 | **Still in early development/I do not recommend to run an instance yet.** 17 | 18 | 19 | 20 | ## Features 21 | 22 | - Implements a basic [ActivityPub](https://activitypub.rocks/) server (with federation) 23 | - S2S (Server to Server) and C2S (Client to Server) protocols 24 | - Compatible with [Mastodon](https://joinmastodon.org/) and others ([Pleroma](https://pleroma.social/), Misskey, Plume, PixelFed, Hubzilla...) 25 | - Exposes your outbox as a basic microblog 26 | - Support all content types from the Fediverse (`Note`, `Article`, `Page`, `Video`, `Image`, `Question`...) 27 | - Markdown support 28 | - Server-side code syntax highlighting 29 | - Comes with an admin UI with notifications and the stream of people you follow 30 | - Private "bookmark" support 31 | - List support 32 | - Allows you to attach files to your notes 33 | - Custom emojis support 34 | - Cares about your privacy 35 | - The image upload endpoint strips EXIF meta data before storing the file 36 | - Every attachment/media is cached (or proxied) by the server 37 | - No JavaScript, **that's it**. Even the admin UI is pure HTML/CSS 38 | - (well except for the Emoji picker within the admin, but it's only few line of hand-written JavaScript) 39 | - Easy to customize (the theme is written Sass) 40 | - mobile-friendly theme 41 | - with dark and light version 42 | - IndieWeb citizen 43 | - Microformats aware (exports `h-feed`, `h-entry`, `h-cards`, ...) 44 | - Export a feed in the HTML that is WebSub compatible 45 | - Partial [Micropub](https://www.w3.org/TR/micropub/) support ([implementation report](https://micropub.rocks/implementation-reports/servers/416/s0BDEXZiX805btoa47sz)) 46 | - Implements [IndieAuth](https://indieauth.spec.indieweb.org/) endpoints (authorization and token endpoint) 47 | - You can use your ActivityPub identity to login to other websites/app (with U2F support) 48 | - Send [Webmentions](https://www.w3.org/TR/webmention/) to linked website (only for public notes) 49 | - Exports RSS/Atom/[JSON](https://jsonfeed.org/) feeds 50 | - You stream/timeline is also available in an (authenticated) JSON feed 51 | - Comes with a tiny HTTP API to help posting new content and and read your inbox/notifications 52 | - Deployable with Docker (Docker compose for everything: dev, test and deployment) 53 | - Focused on testing 54 | - Tested against the [official ActivityPub test suite](https://test.activitypub.rocks/), see [the results](https://activitypub.rocks/implementation-report/) 55 | - [CI runs "federation" tests against two instances](https://d.a4.io/tsileo/microblog.pub) 56 | - Project is running 2 up-to-date instances ([here](https://microblog.pub) and [there](https://a4.io)) 57 | - Manually tested against other major platforms 58 | 59 | 60 | ## User Guide 61 | 62 | Remember that _microblog.pub_ is still in early development. 63 | 64 | The easiest and recommended way to run _microblog.pub_ in production is to use the provided docker-compose config. 65 | 66 | First install [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/). 67 | Python is not needed on the host system. 68 | 69 | Note that all the generated data (config included) will be stored on the host (i.e. not only in Docker) in `config/` and `data/`. 70 | 71 | ### Installation 72 | 73 | ```shell 74 | $ git clone https://github.com/tsileo/microblog.pub 75 | $ cd microblog.pub 76 | $ make config 77 | ``` 78 | 79 | Once the initial configuration is done, you can still tweak the config by editing `config/me.yml` directly. 80 | 81 | 82 | ### Deployment 83 | 84 | To spawn the docker-compose project (running this command will also update _microblog.pub_ to latest and restart everything if it's already running): 85 | 86 | ```shell 87 | $ make run 88 | ``` 89 | 90 | By default, the server will listen on `localhost:5005` (http://localhost:5005 should work if you're running locally). 91 | 92 | For production, you need to setup a reverse proxy (nginx, caddy) to forward your domain to the local server 93 | (and check [certbot](https://certbot.eff.org/) for getting a free TLS certificate). 94 | 95 | ### Backup 96 | 97 | The easiest way to backup all of your data is to backup the `microblog.pub/` directory directly (that's what I do and I have been able to restore super easily). 98 | It should be safe to copy the directory while the Docker compose project is running. 99 | 100 | 101 | ## Development 102 | 103 | The project requires Python3.7+. 104 | 105 | The most convenient way to hack on _microblog.pub_ is to run the Python server on the host directly, and evetything else in Docker. 106 | 107 | ```shell 108 | # One-time setup (in a new virtual env) 109 | $ pip install -r requirements.txt 110 | # Start MongoDB and poussetaches 111 | $ make poussetaches 112 | $ env POUSSETACHES_AUTH_KEY="
100 | {% else %}
101 | Manually approves followers
{% endif %} 107 |255 | {{ utils.display_note(item.activity, meta={"object_visibility": "PUBLIC"}) }} 256 | {% endif %} 257 | 258 | {% endif %} 259 | {% endfor %} 260 | 261 | {{ utils.display_pagination(older_than, newer_than) }} 262 |