<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/rss-style.xsl" type="text/xsl"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	     xmlns:dc="http://purl.org/dc/elements/1.1/"
	   xmlns:atom="http://www.w3.org/2005/Atom"
	     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>
<channel>
	<title>ActivityBot &#8211; Terence Eden’s Blog</title>
	<atom:link href="https://shkspr.mobi/blog/tag/activitybot/feed/" rel="self" type="application/rss+xml" />
	<link>https://shkspr.mobi/blog</link>
	<description>Regular nonsense about tech and its effects 🙃</description>
	<lastBuildDate>Fri, 04 Sep 2026 13:36:29 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://shkspr.mobi/blog/wp-content/uploads/2023/07/cropped-avatar-32x32.jpeg</url>
	<title>ActivityBot &#8211; Terence Eden’s Blog</title>
	<link>https://shkspr.mobi/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title><![CDATA[A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP]]></title>
		<link>https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/</link>
					<comments>https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Thu, 03 Sep 2026 11:34:12 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityBot]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[webdev]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=74429</guid>

					<description><![CDATA[If you&#039;re reading this, you&#039;ve probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.  This is a basic and somewhat incomplete guide to accepting these signatures. I&#039;m sure there are various gotchas, but it works with the signatures I&#039;ve seen in the wild.  Shut Up And Show Me The Code!  OK, wow, no…]]></description>
										<content:encoded><![CDATA[<p>If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.</p>

<p>This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild.</p>

<h2 id="shut-up-and-show-me-the-code"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#shut-up-and-show-me-the-code">Shut Up And Show Me The Code!</a></h2>

<p>OK, wow, no need to be a dick about it! Here's how I validated a real signature that my server received. This requires PHP 8.4 or newer.</p>

<pre><code class="language-php">$verified = openssl_verify(
    data:       '"@method": POST
"@target-uri": https://example.viii.fi/inbox
"content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=:
"@signature-params": ("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key"',
    signature:  base64_decode( "sIfmNsM/Q8iG6AJlne1IkZVjQSVFDEYIPsnoSOXQY+W3Eb4+SOn9o4J5SQmFOP+Jecjf3ioFwUdsrFjAGkUUOHPvSbNWkGKtNuGm+C6r3aI3JBCFGPqX3ITgZYV76CF7JJJ5hPGaG8YH/XdmxVIeFfD3M39FQCncMyyq7xJJvwKKP1mzS5s1vNQie8hbQ9owRjtqvoWcmM9GEYCUHNcMPLjZc+CBrj8sfBbNTYgIFI4UtirOaRJvYymxXjmXuzeVYxQujMjAjgobxQ8QFv0zlYsHk+gS5EYyafpJG9zmfCFSoF9+ZwqKNADmuADbISD9LZIH/bmkPoNXhxaeFPqYog==" ),
    public_key: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsYMEs4waqk/6gaS+xn1T\nYygElTtNIFNkBcEdEBMaeoGVhyZiVKtSjJCS4z+X+394PKvcfSTcFILIt2GI2jOB\nHD0M2fFgxc8mmdSdCQkgEh9jF3bFI3kopDvzYf726iioYKlHXKpfPKvFt7EJgKH7\naCtS25NQkek3YUd6y3VBcT3R6Xhze9P3QNoZMIsFXklgXDKj+EllfbUqLf1vxt3s\nmD9ETxy2bJi9FheE0uY2WhARn49XAvwczM5Wzt+zqxVEtgpi5v2+ZZAVKhDnJkiC\nCCuI6hrSnKNIx/5mSlX0a0S5h5d03djrCkYsqmwelu01rhOXP2grsz4BXp0y2wrO\n3QIDAQAB\n-----END PUBLIC KEY-----\n",
    algorithm:  "sha256"
);

echo $verified;
</code></pre>

<p>Copy and paste that into PHP and you should see that <code>$verified</code> is true.</p>

<h2 id="now-explain-the-code"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#now-explain-the-code">NOW EXPLAIN THE CODE</a></h2>

<p>Say please.</p>

<h2 id="please"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#please">PLEASE!!!</a></h2>

<p>Along with the message sent to your server, you will have received HTTP headers like this:</p>

<pre><code class="language-_">content-digest: sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=:
signature: sig1=:sIfmNsM/Q8iG6AJlne1IkZVjQSVFDEYIPsnoSOXQY+W3Eb4+SOn9o4J5SQmFOP+Jecjf3ioFwUdsrFjAGkUUOHPvSbNWkGKtNuGm+C6r3aI3JBCFGPqX3ITgZYV76CF7JJJ5hPGaG8YH/XdmxVIeFfD3M39FQCncMyyq7xJJvwKKP1mzS5s1vNQie8hbQ9owRjtqvoWcmM9GEYCUHNcMPLjZc+CBrj8sfBbNTYgIFI4UtirOaRJvYymxXjmXuzeVYxQujMjAjgobxQ8QFv0zlYsHk+gS5EYyafpJG9zmfCFSoF9+ZwqKNADmuADbISD9LZIH/bmkPoNXhxaeFPqYog==:
signature-input: sig1=("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key"
</code></pre>

<p>The <code>signature-input</code> tells you how to construct a "Signature Base". You have to build a text string which places the various components in the order specified and separated with a newline:</p>

<pre><code class="language-_">"@method": POST
"@target-uri": https://example.viii.fi/inbox
"content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=:
"@signature-params": ("@method" "@target-uri" "content-digest");created=1787780262;keyid="https://mastodon.social/users/Edent#main-key"
</code></pre>

<p>Where <code>@method</code> is the HTTP method used to send data to your server (usually <code>GET</code> or <code>POST</code>), and <code>@target-uri</code> is the URl the message was sent to (usually your inbox).</p>

<p>The <code>publicKey</code> is slightly trickier. As you can see, the <code>signature-input</code> ends with <code>keyid="https://mastodon.social/users/Edent#main-key</code></p>

<p>If you make a signed request to that URl, you'll get back an ActivityPub Actor document. It will look something like this:</p>

<pre><code class="language-json">{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
  ],
  "id": "https://mastodon.social/users/Edent",
  "webfinger": "Edent@mastodon.social",
  "type": "Person",
  "name": "Terence Eden",
  "publicKey": {
    "id": "https://mastodon.social/users/Edent#main-key",
    "owner": "https://mastodon.social/users/Edent",
    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsYMEs4waqk/6gaS+xn1T\nYygElTtNIFNkBcEdEBMaeoGVhyZiVKtSjJCS4z+X+394PKvcfSTcFILIt2GI2jOB\nHD0M2fFgxc8mmdSdCQkgEh9jF3bFI3kopDvzYf726iioYKlHXKpfPKvFt7EJgKH7\naCtS25NQkek3YUd6y3VBcT3R6Xhze9P3QNoZMIsFXklgXDKj+EllfbUqLf1vxt3s\nmD9ETxy2bJi9FheE0uY2WhARn49XAvwczM5Wzt+zqxVEtgpi5v2+ZZAVKhDnJkiC\nCCuI6hrSnKNIx/5mSlX0a0S5h5d03djrCkYsqmwelu01rhOXP2grsz4BXp0y2wrO\n3QIDAQAB\n-----END PUBLIC KEY-----\n"
  },
</code></pre>

<p>The <code>publicKeyPem</code> is the string you need. There's no need to convert the <code>\n</code> to literal newlines.</p>

<h2 id="is-that-it"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#is-that-it">Is that it?</a></h2>

<p>Not quite! All we've done so far is verify the headers. It is possible that these are genuine headers but attached to a fraudulent body.</p>

<p>This takes us back to the header <code>"content-digest": sha-256=:tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=:</code></p>

<p>That says that the body of the message sent has a Base64 encoded SHA256 hash of <code>tFdB/ENGczHMlZMDb66pXoUi2d0OqH2iBHdnN/WV1mc=</code>.</p>

<p>To calculate your own content digest in PHP:</p>

<pre><code class="language-php">$input = file_get_contents( "php://input" );
$digestCalculated = base64_encode(
    hash(
        algo: "sha256",
        data: $input,
        binary: true
    )
);
</code></pre>

<p>Does your digest match the one sent along with the headers? If not, something dodgy is going on.</p>

<h2 id="putting-it-all-together"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#putting-it-all-together">Putting it all together</a></h2>

<p>The steps are:</p>

<ol>
<li>Get the headers.</li>
<li>Get the body.</li>
<li>From the headers' <code>content-digest</code> extract the algorithm and hash.</li>
<li>Using the body, calculate your own hash using the algorithm from <code>content-digest</code>.</li>
<li>Does your hash match the sent hash? If not, stop. If so, proceed.</li>
<li>From the headers' <code>signature</code> extract the base64 encoded signature.</li>
<li>From the headers' <code>signature-input</code> extract the signature-input string.</li>
<li>From the signature-input string extract the order of the Signature Base.</li>
<li>Construct the Signature Base.</li>
<li>From the signature-input string extract the keyid.</li>
<li>Get the Public Key from the keyid.</li>
<li>Use <code>openssl_verify()</code> to verify the Signature Base and the base64 decoded signature, against the Public Key using SHA256.</li>
</ol>

<p>Note, <a href="https://docs.joinmastodon.org/spec/security/#http-message-signatures">Mastodon <em>only</em> uses SHA256</a>.  I think it should explicitly say which algorithm it is using <a href="https://github.com/mastodon/mastodon/issues/29905#issuecomment-5440336919">and have raised the issue</a>.</p>

<h3 id="in-code-form"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#in-code-form">In Code Form</a></h3>

<p>This is how you do it in PHP. Please read this carefully as there are some hard-coded assumptions.</p>

<pre><code class="language-php">&lt;?php

//  Validate the Digest.
//  It is the hash of the raw input string, in binary, encoded as base64.

//  The format is content-digest =&gt; &lt;algorithm&gt;=:&lt;base64 encoded hash&gt;:
$digestString = $headers["content-digest"];
//  The Base64 encoding may have multiple `=` at the end. So split this at the first `=`.
$digestData = explode( separator: "=", string: $digestString, limit: 2 );

//  Hashes are in lowercase, but have a `-` in their name.
//  This is not what hash_algos() expects.
$digestAlgorithm = str_replace( search: "-", replace: "", subject: $digestData[0] );

//  The hash is surrounded by `:` characters.
$digestHash = str_replace( search: ":", replace: "", subject: $digestData[1] );

//  Check if the hash algorithm is one known about to PHP.
//  If not, reject and record an error.
if ( !in_array( needle:$digestAlgorithm, haystack: hash_algos() ) ) {
    return false;
}

//  Manually calculate the digest based on the data sent.
$digestCalculated = base64_encode( hash( algo: $digestAlgorithm, data: $input, binary: true ) );

//  Does our calculation match what was sent?
if ( !( $digestCalculated == $digestHash ) ) {
    return false;
}

//  The signature format is signature =&gt; &lt;signature name&gt;=:&lt;base64 encoded hash&gt;:
$signatureString = $headers["signature"];
//  The Base64 encoding may have multiple `=` at the end. So split this at the first `=`.
$signatureData = explode( separator: "=", string: $signatureString, limit: 2 );
$signatureName = $signatureData[0];

//  The signature is surrounded by `:` characters.
$signatureB64 = str_replace( search: ":", replace: "", subject: $signatureData[1] );

//  The signature-input format is complicated!
$signatureInputString = $headers["signature-input"];

//  Get the parameters. Assume there is only one signature.
$signatureParamsString = explode( separator: "=", string: $signatureInputString, limit: 2 )[1];

//  Get the different elements of the signature.
$signatureInputData = explode( separator: ";", string: $signatureInputString );

//  Construct the data.
$signatureInput = [];
foreach( $signatureInputData as $signatureInputParts ) {
    $partsData = explode( separator: "=", string: $signatureInputParts );
    //  Strip quotes from keyid and parentheses from sig1.
    if ( "keyid" == $partsData[0] ) {
        $partsData[1] = str_replace( search: "\"", replace: "", subject: $partsData[1] );
    }

    if ( $signatureName == $partsData[0] ) {
        $partsData[1] = str_replace( search: ["(", ")"], replace: "", subject: $partsData[1] );
    }

    $signatureInput[ $partsData[0] ] = $partsData[1] ;
}

$signatureStructure = $signatureInput[$signatureName];
$signatureKeyID     = $signatureInput["keyid"];

//  Remove quotes.
$signatureStructure = str_replace( search: "\"", replace: "", subject: $signatureStructure );
$signatureStructureData = explode( separator: " ", string: $signatureStructure );

//  https://www.rfc-editor.org/info/rfc9421/#section-2.5
$signatureBase = "";
foreach ( $signatureStructureData as $signatureStructureParts ) {
    if ( "@method" == $signatureStructureParts ) {
        //  https://www.rfc-editor.org/info/rfc9421/#name-method
        $signatureBase .= "\"@method\": " . $_SERVER["REQUEST_METHOD"] . "\n";
    }
    if ( "@target-uri" == $signatureStructureParts ) {
        //  https://www.rfc-editor.org/info/rfc9421/#section-2.2.2
        //  Change the domain name to your own.
        $signatureBase .= "\"@target-uri\": https://EXAMPLE.COM" . $_SERVER["REQUEST_URI"] . "\n";
    }
    if ( "content-digest" == $signatureStructureParts ) {
        $signatureBase .= "\"content-digest\": $digestString\n";
    }
}

//  https://victoronsoftware.com/posts/http-message-signatures/#how-the-signature-is-created
$signatureBase .= "\"@signature-params\": $signatureParamsString";

//  Get the signing user's public key.
//  This is usually in the form `https://example.com/user/username#main-key`
//  This is to differentiate if the user has multiple keys.
//  This may need to be a signed request. You will need to write your own getDataFromURl() function to get the sending user's key.
$userData  = getDataFromURl( $signatureKeyID );
$publicKey = $userData["publicKey"]["publicKeyPem"];

//  Verify the request
$verified = openssl_verify(
    data:       $signatureBase,
    signature:  base64_decode( $signatureB64 ),
    public_key: $publicKey,
    algorithm:  $digestAlgorithm
);

//  Convert the result to boolean.
if ( $verified === 1 ) {
    $verified = true;
} elseif ( $verified === 0 ) {
    $verified = false;
} else {
    $verified = null;
}

return $verified;
</code></pre>

<h2 id="further-reading"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#further-reading">Further Reading</a></h2>

<ul>
<li><a href="https://www.rfc-editor.org/info/rfc9421/">RFC 9421 HTTP Message Signatures</a></li>
<li><a href="https://victoronsoftware.com/posts/http-message-signatures/">Understanding HTTP message signatures: A developer's guide</a></li>
<li><a href="https://www.otoroshi.io/docs/tutorials/http-message-signatures-rfc9421/">Sign and verify HTTP messages (RFC 9421)</a></li>
<li><a href="https://darutk.medium.com/verification-of-http-message-signatures-501bbdc7dfec">Verification of HTTP Message Signatures</a></li>
<li><a href="https://github.com/macgirvin/HTTP-Message-Signer">HTTP-Message-Signer in PHP</a></li>
</ul>

<h2 id="thanks-to-nlnet"><a href="https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/#thanks-to-nlnet">Thanks to NLnet</a></h2>

<p>This blog post was funded in part by the work I'm doing for my NLnet NGI0 grant. Thanks!</p>

<p><a href="https://nlnet.nl/project/ActivityBot/"><img src="https://shkspr.mobi/blog/wp-content/uploads/2026/08/NGI-logos.webp" alt="NLnet logo." width="900" height="200" class="aligncenter"></a></p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=74429&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[ActivityBot is the recipient of an NLnet grant!]]></title>
		<link>https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/</link>
					<comments>https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 11:34:55 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityBot]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[NLnet]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=74406</guid>

					<description><![CDATA[Back in February, I applied for NLnet&#039;s Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:  Reclaim the public nature of the internet  Small and medium-sized R&#38;D grants between 5.000 and 50.000 euro, with the possibility to scale up.  I run ActivityBot - it is a single-file ActivityPub server suitable for…]]></description>
										<content:encoded><![CDATA[<p>Back in February, I applied for <a href="https://nlnet.nl/NGI0/">NLnet's Next Generation Zero grant</a>. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:</p>

<blockquote><p>Reclaim the public nature of the internet</p>

<p>Small and medium-sized R&amp;D grants between 5.000 and 50.000 euro, with the possibility to scale up.</p></blockquote>

<p>I run <a href="https://gitlab.com/edent/activity-bot">ActivityBot</a> - it is a single-file ActivityPub server suitable for launching automated accounts and designed as a learning tool for those who want to understand how the protocol works. Several people have told me how useful it is, but I haven't had the time to make it better. So I decided to stick in a last-minute application to the fund.</p>

<p>I really didn't know how much to apply for - or even if my project would be suitable for funding - so I cheekily asked for €10,000. After a few months of back-and-forth, I'm delighted to announce that I was successful!</p>

<p>In the spirit of openness, this blog post details how the NLnet grant process worked for me and what I'll be using the money for.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/08/NGI-logos.webp" alt="NLnet logo." width="900" height="200" class="aligncenter">

<h2 id="the-process"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#the-process">The Process</a></h2>

<p>The application was delightfully simple. Here's what it asked for, along with my answers. If you apply, please don't copy these verbatim; use your own words.</p>

<blockquote>
<ul>
<li>Abstract    : A single file server for ActivityPub. Designed for write-only bots. Allows any project to quickly and easily start publishing automated content to the Fediverse. Uses PHP, no other dependencies.
</li><li>Experience  : I am the sole developer of Single File ActivityPub - https://gitlab.com/edent/activitypub-single-php-file<br>I was formerly the UK Government's representative to the W3C and have contributed to various ActivityPub projects and specifications.
</li><li>Amount      : € 10000
</li><li>Use         : The fund will be used for development, testing, promotional activity (including conference travel).<br>I anticipate this will fund 6 months of development. I have funded all previous development.<br>
</li><li>Comparison  : Most ActivityPub services are complex. They implement a full specification and are designed for multi-user environments. Other projects allow reading and writing. ActivityBot is deliberately designed to be as simple as possible. A single file to upload, one user, publish only.<br>This will enable more projects to be able to instantly start publishing with low development cost and close to zero hosting cost.
</li><li>Challenges  : Formal spec verification and a security audit will be the main technical challenges. The ActivityBot software has been running well for over a year. The funding will allow for better compatibility and security.
</li><li>Ecosystem   : The project has mostly targeted individuals who want to run small bots. After further development, the project will engage with IoT providers, smaller publishers, open source projects who wish to publish updates, and other relevant parties.
</li></ul>
</blockquote>

<p>I was told there was intense competition. After a couple of months, I received word that I'd made it to the 2nd round.</p>

<p>What then followed was a <em>very</em> polite interrogation about my ideas, how I would develop the project, what I would use the money for, and what my AI usage policy was.  They also wanted a breakdown of the main tasks - with the understanding that this would be a provisional document subject to change.</p>

<p>I was on <a href="https://shkspr.mobi/blog/2026/07/another-ridiculous-interrail-holiday-6379km-and-13-countries-over-7-weeks/">a train through Europe</a> when I wrote this. I don't claim it to be a brilliant document - but it got the job done!</p>

<blockquote><p>1. User Research

</p><p>Recruit 2 - 5 potential users. Offer an incentive (approx £20ea) to participate in a user research session. Study design will take 1 - 2 days. Each interview and write up to take 1 day. Consolidation and report 2 days.

</p><p>Total effort 3 - 4 weeks.

</p><p>2. Standards Research

</p><p>Participate in ActivityPub user communities and standardisation groups. Attend virtual conferences (or any local to the UK). Approx 1 day per week for 6 months.

</p><p>3. Test Driven Development

</p><p>Create modern test harness, write test suite, iterate design based on tests. Anticipated effort 2 days per week for approx 3 months.

</p><p>4. Security Testing

</p><p>Work with the community and security professionals to test the resultant code. This will use human testers and normal fuzzers - this will not use AI tools. Anticipated effort 2 days per week for approx 2 months.

</p><p>5. User Acceptance Testing

</p><p>Recruit 2 - 5 potential users (ideally different to the research participants). Offer an incentive (approx £20ea) to participate in a user acceptance session. Study design will take 1 - 2 days. Each interview and write up to take 1 day. Consolidation and report 2 days.

</p><p>Total effort 3 - 4 weeks.

</p><p>6. Updates Based on Research, Testing, and Security

</p><p>While it would be lovely to anticipate getting everything right first time, the reality is that changes will need to be made based on the findings of the above. This will take up the remainder of the allocated time.</p></blockquote>

<p>Again, there was a little more back and forth. But a few weeks later I was informed that I was at the final stage, pending review. And, a few weeks after that, I was told my project had been given the green light.</p>

<p>I was invited to a group call where the very friendly team discussed the practicalities of the grant, what it could and couldn't fund. I also met a bunch of other people who'd also won.</p>

<p>The final stage was writing a proper Memorandum of Understanding. With the help of one of the team (thanks Victoria!) I was able to turn my scrappy plan into something a bit more formal. The project tool NLnet uses made it easy to build up a plan and put € amounts by each task.</p>

<p>The idea is that I will invoice against the grant whenever I have completed a task or sub-task. Obviously I don't want to leave invoicing until the end of the project, but I also need to be mindful of the foreign exchange fees charged by my bank for receiving Euro payments.</p>

<h2 id="final-project-plan"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#final-project-plan">Final Project Plan</a></h2>

<p>This is the plan I submitted. It represents what I hope to accomplish and how I'll draw down on the grant. I suspect this will change as the months go on.</p>

<hr>

<p>ActivityBot is an Open Source project which aims to develop, maintain, and improve a minimum viable ActivityPub server in a single PHP file.</p>

<p>The project is run by Terence Eden (trading as @edent); a developer residing in England.</p>

<p>This project is expected to run for approximately 6 months. All of the deliverables will be openly licenced using either an OSI approved software licence or a Creative Commons licence.</p>

<p>The high-level aims of the project are for ActivityBot to be:</p>

<ol>
<li><p>A fully compliant ActivityPub server, running in a single PHP file.</p></li>
<li><p>A teaching tool to help developers understand the practical aspects of creating an ActivityPub server.</p></li>
<li><p>A practical method of publishing automated messages to the Fediverse.</p></li>
<li><p>A promotional tool to show how simple and easy ActivityPub development can be.</p></li>
<li><p>A secure and usable tool written in modern PHP.</p></li>
<li><p>Written by humans, with no AI/LLM generated code.</p></li>
</ol>

<p>In light of NLnet's non-profit status, costs assume a discounted rate of €330 per day (£280). Incidentals such as hardware, software, travel, or sundries will be charged at cost with receipts provided.</p>

<h2 id="prepare-for-initial-release"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#prepare-for-initial-release">Prepare for initial release</a></h2>

<p>Ensure that the project is in a suitable state for initial release and future development.</p>

<p>Deliverable: Updates published to GitLab.</p>

<ul>
<li><p>€495 Prepare initial release. Clarify licencing, solicit community engagement, include example usage.</p></li>
<li><p>€495 Standards Research. Collation of standards websites. Ensure code comments refer to specific standards. Publish blog post(s) about findings for others to reference.</p></li>
</ul>

<h2 id="user-research"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#user-research">User Research</a></h2>

<p>Recruit up to 10 participants for a user-research study. Participants should represent the diversity of the Fediverse.</p>

<p>Investigate what participants want from a tool like ActivityPub. The project plan may be adapted following the results of this study.</p>

<p>Deliverable: Study plan and results will be published and given a Creative Commons licence. Changes based on the results will be pushed to GitLab.</p>

<ul>
<li><p>€660 Study Design and recruitment of participants (blog post published as deliverable).</p></li>
<li><p>€990 Two days of user interviews, write up and publish results as blog post.</p></li>
</ul>

<h2 id="test-driven-development"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#test-driven-development">Test Driven Development</a></h2>

<p>Create a modern test harness, write test suite, iterate design based on tests.</p>

<p>Deliverable: Tests published to GitLab. Blog posts published about the process and results.</p>

<ul>
<li><p>€330 Set up test suite</p></li>
<li><p>€330 Write tests</p></li>
<li><p>€330 Fixes based on test results</p></li>
</ul>

<h2 id="security-testing"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#security-testing">Security Testing</a></h2>

<p>Working with NLnet's security offering, ensure that the project meets modern security requirements.</p>

<ul>
<li>€720 Work with security team to assess security risks and possible mitigations. Fixes based on security team feedback</li>
</ul>

<p>Deliverable: Updates published to GitLab. Blogs published about the process and results.</p>

<h2 id="user-acceptance-testing"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#user-acceptance-testing">User Acceptance Testing</a></h2>

<p>Recruit up to 10 participants for a user-acceptance study. Participants should represent the diversity of the Fediverse.</p>

<p>Investigate whether participants are able to use ActivityBot. See which aspects need improvement. The project plan may be adapted following the results of this study.</p>

<p>Deliverable: Study plan and results will be published and given a Creative Commons licence. Changes based on feedback will be published to GitLab.</p>

<ul>
<li><p>€660 Study Design and recruitment of participants (blog post published as deliverable).</p></li>
<li><p>€990 Two days of user interviews, write up and publish results as blog post.</p></li>
</ul>

<h2 id="conferences-and-standards-work"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#conferences-and-standards-work">Conferences and Standards Work</a></h2>

<p>Open Source participation often depends on attending conferences, either in person or virtually. Getting involved in the standardisation process ensures that future versions of ActivityPub and associated standards will be suitable for the community.</p>

<p>Deliverables: Presentations material (slideware), speaking at conferences (may be published as video), conference outputs. Where possible, these will be available under a suitable Creative Commons licence.</p>

<ul>
<li><p>€700 Travel and accommodation to one EU conference</p></li>
<li><p>€330 Publishing blog posts about ActivityPub standards work.</p></li>
<li><p>€330 Publishing blog posts about ActivityPub standards work.</p></li>
</ul>

<h2 id="final-release"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#final-release">Final release</a></h2>

<p>Creating a final release for this phase of the ActivityBot project. This will involve incorporating all feedback received so far, improving documentation, and publishing code.</p>

<p>Deliverable: Updates published to GitLab. Blog post written. Release announcements.</p>

<ul>
<li><p>€330 Phase 1: Process and implement feedback from users</p></li>
<li><p>€330 Phase 2: Bug fixes</p></li>
<li><p>€330 Phase 3: Features</p></li>
<li><p>€330 Phase 4: Bug fixes</p></li>
<li><p>€330 Phase 5: Features</p></li>
<li><p>€330 Phase 6: Remedial work</p></li>
<li><p>€330 Process and implement feedback from accessibility scan</p></li>
<li><p>€330 Final release</p></li>
</ul>

<h2 id="next-steps"><a href="https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/#next-steps">Next Steps</a></h2>

<p>I've already begun work on updating the code. If you'd like to get involved, or have suggestions or bug reports - please <a href="https://gitlab.com/edent/activity-bot">take a look at ActivityBot on GitLab</a>.</p>

<p>I'll be putting out a call for user-research participants once I've had a chance to catch my breath 😆</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=74406&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[[RSS Club] I got an NLnet grant!]]></title>
		<link>https://shkspr.mobi/blog/2026/08/rss-club-i-got-an-nlnet-grant/</link>
					<comments>https://shkspr.mobi/blog/2026/08/rss-club-i-got-an-nlnet-grant/#respond</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sat, 08 Aug 2026 11:34:15 +0000</pubDate>
				<category><![CDATA[[RSS Club]]]></category>
		<category><![CDATA[ActivityBot]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[RSS Club]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=73757</guid>

					<description><![CDATA[Pssst! This post is only available to RSS/Atom subscribers. Tell your friends to subscribe  😊.  Just a quick one today, a more detailed blog coming soon - but I thought I&#039;d give RSS Club Members (you!) a sneak peek.  A few months ago I saw a post on Mastodon from NLnet - they were looking for Open Source projects to apply for grant funding. The deadline was less than 24 hours away, so I rushed in …]]></description>
										<content:encoded><![CDATA[<p><mark>Pssst! This post is only available to RSS/Atom subscribers. Tell your friends to subscribe </mark> 😊.</p>

<p>Just a quick one today, a more detailed blog coming soon - but I thought I'd give RSS Club Members (you!) a sneak peek.</p>

<p>A few months ago I saw a post on Mastodon from <a href="https://social.nlnet.nl/@nlnet">NLnet</a> - they were looking for Open Source projects to apply for grant funding. The deadline was less than 24 hours away, so I rushed in an application.</p>

<p>There was a ping-pong of emails where I detailed more about what I planned to do, and where they kept me abreast of their timescales.</p>

<p>Finally, after about 6 months, I received the most delightful email confirming my place in the programme!</p>

<p>I'm officially part of the <a href="https://nlnet.nl/thema/NGI0CommonsFund.html">NGI0 Commons Fund</a>.</p>

<p>So, what's the project? Regular readers will remember that I created <a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/">ActivityBot</a> as a lightweight, single-file, fully featured ActivityPub server. The grant will enable me to improve the code, test the usability, have a security audit, do some accessibility work, and make it more robust.</p>

<p>I'm also going to need a logo 🤖</p>

<p>You can <a href="https://nlnet.nl/project/ActivityBot/">read more about the project on the NLnet site</a>.</p>

<hr>

<p>Because this is an RSS-only post, it doesn't support comments. You can <a href="https://edent.tel/">get in touch with me</a> if you'd like to say something nice.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=73757&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/08/rss-club-i-got-an-nlnet-grant/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Some updates to ActivityBot]]></title>
		<link>https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/</link>
					<comments>https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 16 Mar 2026 12:34:57 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityBot]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=68592</guid>

					<description><![CDATA[I couple of years ago, I developed ActivityBot - the simplest way to build Mastodon Bots. It is a single PHP file which can run an entire ActivityPub server and it is less than 80KB.  It works! You can follow @openbenches@bot.openbenches.org to see the latest entries on OpenBenches.org, and @colours@colours.bots.edent.tel for a slice of colour in your day, and @solar@solar.bots.edent.tel to see…]]></description>
										<content:encoded><![CDATA[<p>I couple of years ago, I developed <a href="https://shkspr.mobi/blog/2024/11/introducing-activitybot-the-simplest-way-to-build-mastodon-bots/">ActivityBot - the simplest way to build Mastodon Bots</a>. It is a <em>single</em> PHP file which can run an entire ActivityPub server and it is less than 80KB.</p>

<p>It works! You can follow <code>@openbenches@bot.openbenches.org</code> to see the latest entries on OpenBenches.org, and <code>@colours@colours.bots.edent.tel</code> for a slice of colour in your day, and <code>@solar@solar.bots.edent.tel</code> to see what my solar panels are up to.</p>

<p>This is <em>so</em> easy to use. Copy the PHP file (and a <code>.env</code> and <code>.htaccess</code>) to literally any web host running PHP 8.5 and you have a fully-fledged bot which can post to Mastodon.</p>

<p><a href="https://gitlab.com/edent/activity-bot/">Grab the code and start today</a>!</p>

<h2 id="features"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#features">Features</a></h2>

<p>Over the years I've added a few more features to it, so I thought I'd run through what they are. Note, this is all hand-written. No sycophantic plagiarism machines were involved in this code or blog post. I just really like emoji, OK⁉️</p>

<h3 id="%f0%9f%94%8d-be-discovered-on-the-fediverse"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%94%8d-be-discovered-on-the-fediverse">🔍 Be discovered on the Fediverse</a></h3>

<p>This is the big one, you can find <code>@example@example.viii.fi</code> on your favourite Fediverse client.  This is thanks to its WebFinger support.</p>

<h3 id="%f0%9f%91%89-be-followed-by-other-accounts"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%91%89-be-followed-by-other-accounts">👉 Be followed by other accounts</a></h3>

<p>No point being discovered if you can't be followed. This accepts follow requests and sends back a signed accept.</p>

<h3 id="%f0%9f%9a%ab-be-unfollowed-by-accounts"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%9a%ab-be-unfollowed-by-accounts">🚫 Be unfollowed by accounts</a></h3>

<p>Sometimes people want to unfollow. Too bad, so sad. Again, this will accept the undo request and delete the unfollowing user's information.</p>

<h3 id="%f0%9f%93%a9-send-messages-to-the-fediverse"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%93%a9-send-messages-to-the-fediverse">📩 Send messages to the Fediverse</a></h3>

<p>If a bot can be followed, but never posts, does it make a sound? This sends a post to all of your followers' (shared) inboxes. Includes some HTML formatting.</p>

<h3 id="%f0%9f%92%8c-send-direct-messages-to-users"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%92%8c-send-direct-messages-to-users">💌 Send direct messages to users</a></h3>

<p>Not every message is for the wider public. If you want a bot which sends you a private message, this'll set the visibility correctly.</p>

<h3 id="%f0%9f%93%b7-attach-images-alt-text-to-a-message-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%93%b7-attach-images-alt-text-to-a-message-%f0%9f%86%95%f0%9f%86%95">📷 Attach images &amp; alt text to a message 🆕🆕</a></h3>

<p>A picture is worth a thousand words. But those pictures are meaningless without alt text. Attach as many images as you like. Note, most Mastodon services only accept a maximum of four.</p>

<h3 id="%f0%9f%8d%bf-video-upload-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%8d%bf-video-upload-%f0%9f%86%95%f0%9f%86%95">🍿 Video Upload 🆕🆕</a></h3>

<p>No transcoding or anything fancy. Upload a video and it'll be sent to your followers.</p>

<h3 id="%f0%9f%94%8a-audio-upload-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%94%8a-audio-upload-%f0%9f%86%95%f0%9f%86%95">🔊 Audio Upload 🆕🆕</a></h3>

<p>Same as video. Raw audio posted to your followers' feeds.</p>

<h3 id="%f0%9f%95%b8%ef%b8%8f-autolink-urls-hashtags-and-mentions"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%95%b8%ef%b8%8f-autolink-urls-hashtags-and-mentions">🕸️ Autolink URls, hashtags, and @ mentions</a></h3>

<p>Including URls, tags, and mentions are <em>mostly</em> autolinked correctly. There's a lot of fuzziness in how it works.</p>

<h3 id="%f0%9f%a7%b5-threads"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%a7%b5-threads">🧵 Threads</a></h3>

<p>You can reply to specific messages in order to create a thread.</p>

<h3 id="%f0%9f%91%88-follow-unfollow-block-and-unblock-other-accounts"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%91%88-follow-unfollow-block-and-unblock-other-accounts">👈 Follow, Unfollow, Block, and Unblock other accounts</a></h3>

<p>It might be useful for you to remove followers or follow specific accounts.</p>

<h3 id="%f0%9f%97%91%ef%b8%8f-delete-posted-messages-and-their-attachments-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%97%91%ef%b8%8f-delete-posted-messages-and-their-attachments-%f0%9f%86%95%f0%9f%86%95">🗑️ Delete posted messages and their attachments 🆕🆕</a></h3>

<p>We all make mistakes. This will delete your post along with any attachments and send that delete message to everyone. Note, because of the federated nature of the Fediverse, you cannot guarantee that a remote server will delete anything.</p>

<h3 id="%e2%9c%8f%ef%b8%8f-edit-posts-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%e2%9c%8f%ef%b8%8f-edit-posts-%f0%9f%86%95%f0%9f%86%95">✏️ Edit Posts 🆕🆕</a></h3>

<p>If you don't want to delete and re-post, you can edit your existing posts.</p>

<h3 id="%f0%9f%a6%8b-bridge-to-bluesky-with-your-domain-name-via-bridgy-fed"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%a6%8b-bridge-to-bluesky-with-your-domain-name-via-bridgy-fed">🦋 Bridge to BlueSky with your domain name via Bridgy Fed</a></h3>

<p>Not everyone is on the Fediverse. If you want to bridge to BlueSky, you can use the <a href="https://fed.brid.gy/">Bridgy Fed service</a>.</p>

<h3 id="%f0%9f%9a%9a-move-followers-from-an-old-account-and-to-a-new-account-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%9a%9a-move-followers-from-an-old-account-and-to-a-new-account-%f0%9f%86%95%f0%9f%86%95">🚚 Move followers from an old account and to a new account 🆕🆕</a></h3>

<p>Perhaps you started as <code>@electric@sex.pants</code> but now you want to become <code>@chaste@nunslife.biz</code> - no worries! You can tell followers you've moved and what your new name is.</p>

<p>Similarly, if ActivityBot is no longer right for you, it's simple to tell your existing follower to move to your new account.</p>

<h3 id="%f0%9f%97%a8%ef%b8%8f-allow-quote-posts-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%97%a8%ef%b8%8f-allow-quote-posts-%f0%9f%86%95%f0%9f%86%95">🗨️ Allow quote posts 🆕🆕</a></h3>

<p>Rather than just reposting your message, this sets the quote policy to allow people to share your message and attach some commentary of your own.</p>

<h3 id="%f0%9f%91%80-show-followers"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%91%80-show-followers">👀 Show followers</a></h3>

<p>Your follower count isn't just a number, it is a living list of <em>who</em> chooses to follow you.</p>

<h3 id="%e2%9a%a0%ef%b8%8f-content-warnings-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%e2%9a%a0%ef%b8%8f-content-warnings-%f0%9f%86%95%f0%9f%86%95">⚠️ Content Warnings 🆕🆕</a></h3>

<p>Perhaps you want to hide a bit of what you're saying. Add a content warning to hide part of your message.</p>

<h3 id="%f0%9f%94%8f-verify-cryptographic-signatures"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%94%8f-verify-cryptographic-signatures">🔏 Verify cryptographic signatures</a></h3>

<p><a href="https://shkspr.mobi/blog/2024/03/i-made-a-mistake-in-verifying-http-message-signatures/">HTTP Message Signatures is <em>hard</em></a>. I think I've mostly got it sorted.</p>

<h3 id="%f0%9f%aa%b5-log-sent-messages-and-errors"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%aa%b5-log-sent-messages-and-errors">🪵 Log sent messages and errors</a></h3>

<p>This is primarily a learning aide, so have a rummage through the logs and see what's going on.</p>

<h3 id="%f0%9f%9a%ae-clear-logs-when-there-are-too-many"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%9a%ae-clear-logs-when-there-are-too-many">🚮 Clear logs when there are too many</a></h3>

<p>ActivityPub is a <em>chatty</em> protocol. Your server can easily fill up with hundreds of thousands of messages from others. This regularly prunes down to something more manageable.</p>

<h3 id="%ef%b8%8f%e2%83%a3-hashed-passwords-for-posting-%f0%9f%86%95%f0%9f%86%95"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%ef%b8%8f%e2%83%a3-hashed-passwords-for-posting-%f0%9f%86%95%f0%9f%86%95">#️⃣ Hashed passwords for posting 🆕🆕</a></h3>

<p>Bit of a guilty moment here. I was originally storing the password in plaintext. Naughty! Passwords are now salted and hashed.</p>

<h3 id="%f0%9f%92%bb-basic-website-for-showing-posts"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%92%bb-basic-website-for-showing-posts">💻 Basic website for showing posts</a></h3>

<p>A nice-enough looking front end if people want to view the posts directly on your domain.</p>

<h2 id="some-deficiencies"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#some-deficiencies">Some Deficiencies</a></h2>

<p>Not every piece of software is perfect. ActivityBot is less perfect than most things. Here are some of the things it can't do and, perhaps, will never do.  If you'd like to help tackle any of these, <a href="https://gitlab.com/edent/activity-bot/">fork the code from my git repo</a>!</p>

<h3 id="%e2%8f%b3-retry-failed-messages"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%e2%8f%b3-retry-failed-messages">⏳ Retry Failed Messages</a></h3>

<p>A <em>proper</em> Mastodon server will keep trying to send messages to unresponsive hosts. ActivityBot is one-and-done. If a remote server didn't respond in time, or was offline, or something else went wrong - it may not get the message.</p>

<h3 id="%f0%9f%94%84-reposts-announce-quote"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%94%84-reposts-announce-quote">🔄 Reposts / Announce / Quote</a></h3>

<p>You cannot boost other posts, or even your own. Nor can you send quote posts.</p>

<h3 id="%f0%9f%a4%96-act-on-instructions"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%a4%96-act-on-instructions">🤖 Act On Instructions</a></h3>

<p>This is a basic bot. It contains no logic. If you send it a message asking it to take action, it will not. You will need to build something else to make it truly interactive.</p>

<h3 id="%f0%9f%93%a5-receive-messages"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%93%a5-receive-messages">📥 Receive Messages</a></h3>

<p>In fact, other than the follow / unfollow stuff, the bot can't receive any messages from the Fediverse. It doesn't know when a post has been replied to, liked, or reposted.</p>

<h3 id="%f0%9f%98%8e-set-post-visibility"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%98%8e-set-post-visibility">😎 Set Post Visibility</a></h3>

<p>Your posts are either public or a DM. There's no support for things like quiet followers.</p>

<h3 id="%f0%9f%93%8a-create-polls"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%93%8a-create-polls">📊 Create Polls</a></h3>

<p>Everyone loves to vote on meaningless polls - but this is quite a hard problem for ActivityBot. It would need to keep track of votes, prevent double voting, and probably some other difficult stuff.</p>

<h3 id="%f0%9f%97%a8%ef%b8%8f-change-quote-post-visibility"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%97%a8%ef%b8%8f-change-quote-post-visibility">🗨️ Change Quote Post Visibility</a></h3>

<p>As quote posts are still quite new to Mastodon, I'm not sure how best to implement this.</p>

<h3 id="%f0%9f%94%97-proper-html-markdown-support"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%94%97-proper-html-markdown-support">🔗 Proper HTML / Markdown Support</a></h3>

<p>Autolinking names, hashtags, and links just about works - but not very reliably. In theory the bot <em>could</em> parse Markdown and create richly formatted HTML from it. But that may require an external library which would bloat the size. Perhaps posting raw HTML could work?</p>

<h3 id="%f0%9f%96%bc%ef%b8%8f-focus-points-for-images"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%f0%9f%96%bc%ef%b8%8f-focus-points-for-images">🖼️ Focus Points for Images</a></h3>

<p>Perhaps of less use now, but still of interest to people?</p>

<h3 id="%e2%9d%93-other-stuff"><a href="https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/#%e2%9d%93-other-stuff">❓ Other Stuff</a></h3>

<p>I don't know what I don't know. Maybe some stuff is total broken? Maybe it is wildly out of spec? If you spot something dodgy, please let me know or <a href="https://gitlab.com/edent/activity-bot/">raise a Pull Request</a>.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=68592&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>
