Pinhole

You can use Git to clone the repository via the web URL. Download snapshot (zip)
 
descriptionPinhole is a tiny single-user Bluesky→ActivityPub one-way bridge.
websitehttps://fietkau.software/pinhole
last changeWed, 25 Oct 2023 21:02:04 +0000 (23:02 +0200)
shortlog
2023-10-25 Julian FietkauPrevent unintended crash if sending AP payload fails... main
2023-10-23 Julian FietkauFix minor brainfart in readme (WebMention -> WebFinger)
2023-10-22 Julian FietkauEscape special HTML characters in profile bio
2023-10-22 Julian FietkauInitial public release: version 1 v1
tags
6 months ago v1
readme

Pinhole

Pinhole is a tiny single-user Bluesky→ActivityPub one-way bridge. It can be self-hosted to provide a mirror bot for exactly one Bluesky profile followable by exactly one ActivityPub (e.g. Mastodon) account.

ActivityPub is a W3C standardized protocol for interoperable social networks used by Mastodon and others. Bluesky is a social network that has announced plans to enable federation using its own AT protocol in lieu of ActivityPub, but is still entirely non-interoperable at time of writing. However, all post data on Bluesky’s network can currently be easily accessed through APIs offered by its central server. Pinhole makes use of these interfaces to fetch the posts of a specific Bluesky profile and make them available as an ActivityPub actor that can be followed from compatible networks. Due to it being a one-way bridge, the posts it mirrors can be read, but interactions such as likes and replies will not be visible to the original author.

This software is deliberately constructed to be anti-scalable. You can set it up for personal use to follow exactly one Bluesky profile from exactly one ActivityPub profile. There are no options to configure Pinhole to provide several mirror profiles or to make them followable by more than one person. Of course you are free to run multiple copies of this software if you want. More information on the thoughts behind this architecture can be found on the project website: https://fietkau.software/pinhole

It should be noted that, while the ActivityPub half of this bridge is relatively by-the-books, the Bluesky half is not a clean implementation of the AT protocol at all. Instead, it relies on known patterns in current Bluesky API URLs and light response parsing. As Bluesky makes changes to its architecture, it might break. I will probably update it to keep it working for as long as I personally need it, but in the long term (especially if Bluesky sticks to its plan to provide federation through the AT protocol), the community should move to proper bi-directional bridges built by other people. If this is an area of interest to you, you probably want to follow the work of Ryan Barrett.

Requirements

A Pinhole installation requires a reasonably recent Python 3 environment. I have tested it on Python 3.11. Beyond the standard library, it requires the modules flask, requests, and Crypto (from pycryptodome or a compatible package). If a required module is missing, it will tell you when you try to run it.

You can run Pinhole through Flask’s local development server for testing and configuration, but a long-term installation will need a proper WSGI server. I personally use Apache httpd 2.4 with the wsgi module, because I already use the same web server for all my other projects. If you prefer nginx or something else, check its WSGI capabilities. I can’t give any informed recommendations on this.

In addition to a WSGI-capable web server, Pinhole relies on its update task getting run periodically behind the scenes. This is most easily accomplished by using something like cron. More on this in the “Server installation” section.

Pinhole is not a “Bluesky client/viewer” and does not provide a web frontend of its own. The only way to use it is via an ActivityPub-enabled platform like Mastodon.

Getting Started

To begin with, you need to clone the repository (git clone https://fietkau.software/Pinhole.git). In pinhole.py, below the header comment, you’ll find the spot where you need to set the Bluesky profile you wish to bridge and your own ActivityPub account which will be allowed to follow it:

BLUESKY_PROFILE = '@example.bsky.social'
ACTIVITYPUB_RECIPIENT = '@you@example.com'

Configure these as needed. The Bluesky profile needs to be given as a handle (with one @ in front) and the ActivityPub address in WebFinger format (double @). After that, run pinhole.py. If your Python environment lacks any required modules, that will be the first thing you’ll find out. The script will also let you know if its directory needs to be made writable. When you get the intro text and the Flask development server information after starting the script, the basic preconditions are met.

The Flask development server will tell you the URL to connect to in your web browser, where (if you’re on localhost) you should see the JSON version of the new ActivityPub actor after a few seconds of automatic setup. If you get no error messages at this point, everything is ready for installation on an actual server.

Server Installation

Pinhole should be compatible with any WSGI-capable web server, although I have only tested it with Apache httpd, where you will need the wsgi module (available in the libapache2-mod-wsgi-py3 package on a recent Ubuntu Linux). Create a virtual host for your Pinhole instance and configure it as necessary. My configuration looks like this:

ServerName pinhole.example.com

WSGIDaemonProcess pinhole user=www-data group=www-data threads=5 home=/var/www/pinhole/
WSGIScriptAlias / /var/www/pinhole/pinhole.py

<Directory /var/www/pinhole>
    WSGIProcessGroup pinhole
    WSGIApplicationGroup %{GLOBAL}
    WSGIScriptReloading On
    Require all granted
</Directory>

# Plus the normal stuff for logging, certs, etc...

Make sure the pinhole.py script can be found by the web server. The user account running the web server needs to be able to write to the directory the script resides in. On my Ubuntu server, I usually change the directory’s group to www-data and make it group-writable. The script itself can be set as read-only. If you want, you can copy over your local files from the previous section, but if you do, you will need to edit data.json to set the app->urlRoot key to null. Enable the virtual host, follow any instructions your web server gives you, and you should have an accessible Pinhole installation.

If you use certbot for your website certificate management, note that it is easiest to generate certificates for your virtual host before you do the WSGI configuration. Because certbot copies your virtual host configuration file, clashes arise if the virtual host configures a WSGI environment. Pinhole assumes that the connecting ActivityPub server is using HTTPS. Running Pinhole itself via HTTPS is technically not required, but heavily recommended, as some ActivityPub servers will refuse to connect via unencrypted HTTP.

The last thing to do is to configure the periodic update call. pinhole.py needs to be run with the --update parameter to check the configured Bluesky profile for new posts and perform other maintenance tasks. The easiest way to do this is to use a cron-compatible service. Here is an example configuration for /etc/crontab:

* * * * * www-data python3 pinhole.py --update

The five asterisks mean that the update tasks are run once every minute, which should get new posts to your feed pretty quickly. If Bluesky ever introduces rate limits for external clients, you may need to increase this interval.

And with that your Pinhole installation should be fully functional! Go and follow it from your ActivityPub platform. On Mastodon, this can be done by putting the URL into the search box. The profile will show as follow-restricted, but will automatically approve a follow request by the profile you configured at the start. From here on, new posts should appear on your feed.

License

Pinhole (c) Julian Fietkau

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See LICENSE for details.