<?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>mastodon &#8211; Terence Eden’s Blog</title>
	<atom:link href="https://shkspr.mobi/blog/tag/mastodon/feed/" rel="self" type="application/rss+xml" />
	<link>https://shkspr.mobi/blog</link>
	<description>Regular nonsense about tech and its effects 🙃</description>
	<lastBuildDate>Sun, 15 Mar 2026 13:04:02 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://shkspr.mobi/blog/wp-content/uploads/2023/07/cropped-avatar-32x32.jpeg</url>
	<title>mastodon &#8211; Terence Eden’s Blog</title>
	<link>https://shkspr.mobi/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<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>
		<item>
		<title><![CDATA[Adding "Log In With Mastodon" to Auth0]]></title>
		<link>https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/</link>
					<comments>https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 02 Mar 2026 12:34:48 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[Auth0]]></category>
		<category><![CDATA[HowTo]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[Social Media]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=67308</guid>

					<description><![CDATA[I use Auth0 to provide social logins for the OpenBenches website. I don&#039;t want to deal with creating user accounts, managing passwords, or anything like that, so Auth0 is perfect for my needs.  There are a wide range of social media logins provided by Auth0 - including the usual suspects like Facebook, Twitter, WordPress, Discord, etc. Sadly, there&#039;s no support for Mastodon.  All is not lost…]]></description>
										<content:encoded><![CDATA[<p>I use <a href="https://auth0.com/">Auth0</a> to provide social logins for the <a href="https://openbenches.org">OpenBenches</a> website. I don't want to deal with creating user accounts, managing passwords, or anything like that, so Auth0 is perfect for my needs.</p>

<p>There are a wide range of <a href="https://auth0.com/learn/social-login">social media logins provided by Auth0</a> - including the usual suspects like Facebook, Twitter, WordPress, Discord, etc. Sadly, there's <a href="https://community.auth0.com/t/custom-social-for-mastodon/103356">no support for Mastodon</a><sup id="fnref:blog"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#fn:blog" class="footnote-ref" title="Auth0 did blog about Mastodon a few years ago but never bothered implementing it!" role="doc-noteref">0</a></sup>.</p>

<p>All is not lost though. The Auth0 documentation says:</p>

<blockquote><p>However, you can use Auth0’s Connections API to add any OAuth2 Authorization Server as an identity provider.</p></blockquote>

<p>You can manually add a <em>single</em> Mastodon instance, but that doesn't work with the decentralised nature of the Fediverse. Instead, I've come up with a manual solution which works with <em>any</em> Mastodon server!</p>

<h2 id="background"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#background">Background</a></h2>

<p>Every Mastodon<sup id="fnref:masto"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#fn:masto" class="footnote-ref" title="I do mean Mastodon; not the wider Fediverse. This only works with sites which have implemented Mastodon's APIs." role="doc-noteref">1</a></sup> server is independent. I have an account on <code>mastodon.social</code> you have an account on <code>whatever.chaos</code>. They are separate servers, albeit running similar software. A generic authenticator needs to work with <em>all</em> these servers. There's no point only allowing log ins from a single server.</p>

<p>Fortuitously, Mastodon allows app developers to automatically create new apps. A few simple lines of code and you will have an API key suitable for <em>read-only</em> access to that server. You can <a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/">read how to instantly create Mastodon API keys</a> or you can <a href="https://github.com/openbenches/openbenches.org/blob/343e4c0169a2af8e567f9444c9cbf5d43d03011a/www/src/Controller/UserController.php#L26">steal my PHP code</a>.</p>

<h2 id="user-experience"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#user-experience">User Experience</a></h2>

<p>The user clicks the sign-in button on OpenBenches. They're taken to the Auth0 social login screen:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/01/Auth0-Mastodon.webp" alt="Login screen with several social login buttons." width="1677" height="1258" class="aligncenter size-full wp-image-67317">

<p>The user clicks on Mastodon. This is where Auth0's involvement ends!</p>

<p>The user is asked to provide the URl of their instance:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/01/Enter-server.webp" alt="Screenshot. The site asks for a Mastodon server URl." width="941" height="414" class="aligncenter size-full wp-image-67318">

<p>In the background, my server contacts the Mastodon instance and creates a read-only API key.</p>

<p>The user is asked to sign in to Mastodon.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/01/Masto-login.webp" alt="Screenshot of a login page." width="800" height="900" class="aligncenter size-full wp-image-67319">

<p>The user is asked to authorise read-only access.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/01/Authorisation.webp" alt="Screenshot. Page asks whether the user wants to authorise OpenBenches for read only access." width="720" height="656" class="aligncenter size-full wp-image-67320">

<p>The user is now signed in and OpenBenches can retrieve their name, avatar image, and other useful information. Hurrah!</p>

<h2 id="auth0"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#auth0">Auth0</a></h2>

<p>Once you have  <a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/">created a service to generate API keys</a>, it will need to run on a publicly accessible web server. For example <code>https://example.com/mastodon_login</code>.</p>

<p>Here's what you need to do within your Auth0 tennant:</p>

<ul>
<li>Authentication → Social → Create Connection</li>
<li>At the bottom, choose "Create Custom".</li>
<li>Choose "Authentication" only.</li>
<li>Give your connection a name. This will be visible to users.</li>
<li>"Authorization URL" and "Token URL" have the same value - the URl of your service.</li>
<li>"Client ID" is only visible to you.</li>
<li>"Client Secret" any random password; it won't be used for anything.</li>
<li>Leave everything else in the default state.</li>
</ul>

<p>It should look something like this:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/01/Example-Auth0.webp" alt="Screenshot of a form with all the settings filled in." width="891" height="1239" class="aligncenter size-full wp-image-67321">

<p>Click the "Create" button and you're (almost) done.</p>

<h2 id="auth0-icon"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#auth0-icon">Auth0 Icon</a></h2>

<p>You will need to <a href="https://shkspr.mobi/blog/2024/12/add-a-custom-icon-to-auth0s-custom-social-integrations/">add a custom icon to the social integration</a>. Annoyingly, there's no way to do it through the web interface, so follow that guide to use the command line.</p>

<h2 id="done"><a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#done">Done!</a></h2>

<p>I'll admit, this isn't the most straightforward thing to implement. Auth0 could make this easier - but it would still rely on users knowing the URl of their home instance.</p>

<p>That said, the Mastodon API is a delight to work with and the read-only permissions reduce risk for all parties.</p>

<div id="footnotes" role="doc-endnotes">
<hr>
<ol start="0">

<li id="fn:blog">
<p>Auth0 <a href="https://auth0.com/blog/mastdon-for-developers/">did blog about Mastodon a few years ago</a> but never bothered implementing it!&nbsp;<a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#fnref:blog" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:masto">
<p>I <em>do</em> mean Mastodon; not the wider Fediverse. This only works with sites which have implemented Mastodon's APIs.&nbsp;<a href="https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/#fnref:masto" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

</ol>
</div>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=67308&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/03/adding-log-in-with-mastodon-to-auth0/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Social Media Payments and Perverse Incentives]]></title>
		<link>https://shkspr.mobi/blog/2026/02/social-media-payments-and-perverse-incentives/</link>
					<comments>https://shkspr.mobi/blog/2026/02/social-media-payments-and-perverse-incentives/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 15 Feb 2026 12:34:23 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[BSky]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[money]]></category>
		<category><![CDATA[Social Media]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=67669</guid>

					<description><![CDATA[At the recent &#34;Protocols for Publishers&#34; event, a group of us were talking about news paywalls, social media promotion, and the embarrassment of having to ask for money.  What if, we said, you could tip a journalist directly on social media? Or reward your favourite creator without leaving the platform? Or just say thanks by buying someone a pint?  Here&#039;s a trivial mock-up:    Of course, this…]]></description>
										<content:encoded><![CDATA[<p>At the recent "<a href="https://protocolsforpublishers.com/">Protocols for Publishers</a>" event, a group of us were talking about news paywalls, social media promotion, and the embarrassment of having to ask for money.</p>

<p>What if, we said, you could tip a journalist directly on social media? Or reward your favourite creator without leaving the platform? Or just say thanks by buying someone a pint?</p>

<p>Here's a trivial mock-up:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2026/02/Tip-page.webp" alt="Mock up of a Mastodon post. There's a a £ button next to boost. It offers the options to tip the suggested amount £0.15, or to tip a custom amount." width="998" height="541" class="aligncenter size-full wp-image-67671">

<p>Of course, this hides a ton of complexity. Does it show your local currency symbol? Does the platform take a cut or does it just pass you to the poster's preferred platform? Do users want to be able to tip as well as / instead of reposting and favouriting?</p>

<p>But I think the real problem is the perverse incentives it creates. We already know that relentless A|B testing of monetisation strategies leads to homogeneity and outrage farming. Every YouTuber has the same style of promotional thumbnail. Rage-baiters on Twitter know what drives the algorithm and pump out unending slurry.</p>

<p>Even if we ignore those who want to burn the world, content stealers like @CUTE_PUPP1E5 grab all the content they can and rip-off original creators. At the moment that's merely annoying, but monetisation means a strong incentive to steal content.</p>

<p>When people inevitably get scammed, would that damage the social media platform? Would promoting a payment link lead to liability? Now that money is involved, does that make hacking more attractive?</p>

<p>And yet… Accounts add payment links to their profiles all the time. Lots of accounts regularly ask for donor and sponsors. GitHub sponsors exist and I don't see evidence of people impersonating big projects and snaffling funds.</p>

<p>It is somewhat common for platforms to pay for publishers to be on their site. If you're starting up a new service then you need to give people an incentive to be there. That might be as a payer or receiver.</p>

<p>Personally, I'd love a frictionless way to throw a quid to a helpful blog post, or effortlessly donate to a poster who has made me laugh. Selfishly, I'd like it if people paid me for my Open Source or (micro)blogging.</p>

<p>I don't know whether Mastodon or BlueSky will ever have a payments button - and I have no influence on their decision-making process - but I'd sure like to see them experiement.</p>

<p>You can <a href="https://mastodon.social/@Edent/116023582888695517">read more discussion on Mastodon</a>.</p>

<p>Or, feel free to send me a tip!</p>

<ul id="review-list">
<li><a href="https://www.amazon.co.uk/hz/wishlist/ls/13GFCFR2B2IX4?type=wishlist&amp;linkCode=sl2&amp;tag=shksprblogwish-21"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=amazon.co.uk" width="20">Buy me a gift from my Amazon wishlist</a></li>
<li><a href="https://github.com/sponsors/edent"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=github.com" width="20">Sponsor me on GitHub</a></li>
<li><a href="https://paypal.me/edent/gbp1"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=paypal.com" width="20">Send me money via PayPal</a></li>
<li><a href="https://ko-fi.com/edent"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=ko-fi.com" width="20">Support me on Ko-Fi</a></li>
<li><a href="https://patreon.com/edent"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=patreon.com" width="20">Become a Patreon</a></li>
<li><a href="https://opencollective.com/edent"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=opencollective.com" width="20">Join my Open Collective</a></li>
<li><a href="https://liberapay.com/edent"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=liberapay.com" width="20">Donate using LiberaPay</a></li>
<li><a href="https://wise.com/pay/me/terencee51"><img alt="" loading="lazy" src="https://shkspr.mobi/favicons/?domain=wise.com" width="20">Pay with Wise</a></li>
</ul>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=67669&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2026/02/social-media-payments-and-perverse-incentives/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Now witness the power of this fully operational Fediverse!]]></title>
		<link>https://shkspr.mobi/blog/2025/11/now-witness-the-power-of-this-fully-operational-fediverse/</link>
					<comments>https://shkspr.mobi/blog/2025/11/now-witness-the-power-of-this-fully-operational-fediverse/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 23 Nov 2025 12:34:35 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[BlueSky]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[statistics]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=63716</guid>

					<description><![CDATA[How can you measure the popularity of a social network site? Perhaps by counting the number of active accounts, or the quality of the discourse, or even how many people reply to your witty memes.  Me? I prefer to look at how many people visit my blog from each site. It is an imperfect measure - and a vain one - but lets me know where I should be spending my time. No point posting on a network…]]></description>
										<content:encoded><![CDATA[<p>How can you measure the popularity of a social network site? Perhaps by counting the number of active accounts, or the quality of the discourse, or even how many people reply to your witty memes.</p>

<p>Me? I prefer to look at how many people visit my blog from each site. It is an imperfect measure - and a vain one - but lets me know where I should be spending my time. No point posting on a network which is just bots talking to each other, right?</p>

<p>Earlier this year <a href="https://shkspr.mobi/blog/2025/09/reasonably-accurate-privacy-conscious-cookieless-visitor-tracking-for-wordpress/">I built a stats-counter for my blog</a>. Every time someone clicks from a website which links to my blog, it records that visit in a database. I get to see which blog posts are doing numbers, and where those numbers came from.</p>

<p>Until fairly recently, the Mastodon social network didn't send referer details. I thought that reduced the visibility of the network and <a href="https://shkspr.mobi/blog/2024/12/mastodon-now-sends-referer-headers-hurrah/">lobbied for it to change</a>. As various Mastodon servers upgrade, and admins opt-in, it is becoming more apparent just how much traffic originates from the Fediverse.</p>

<p>Over the last few weeks, here's how many people have clicked <em>from</em> BlueSky and Mastodon <em>to</em> one of my blog posts.</p>

<table class="edent_stats_column"><thead><tr><th class="totals">Total</th><th>Source</th></tr></thead><tbody>
<tr><td class="stats-count">1,607</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=bsky.app"><a href="https://bsky.app">bsky.app</a></td></tr>
<tr><td class="stats-count">752</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mastodon.social"><a href="https://mastodon.social">mastodon.social</a></td></tr>
</tbody></table>

<p>At first glance, it doesn't look good for our elephantine friends, does it? The butterfly sends over twice the traffic. Game over!</p>

<p>But, of course, while Mastodon.social is the biggest instance - it is far from the only one. What happens if we slide down the long tail? Here's all the Mastodon-ish instances which sent me over 10 clicks.</p>

<table class="edent_stats_column"><thead><tr><th class="totals">Total</th><th>Source</th></tr></thead><tbody>
<tr><td class="stats-count">193</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=phanpy.social"><a href="https://phanpy.social">phanpy.social</a></td></tr>
<tr><td class="stats-count">120</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=joinmastodon.org"> android-app://org.joinmastodon.android/</td></tr>
<tr><td class="stats-count">106</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=infosec.exchange"><a href="https://infosec.exchange">infosec.exchange</a></td></tr>
<tr><td class="stats-count">62</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mas.to"><a href="https://mas.to">mas.to</a></td></tr>
<tr><td class="stats-count">59</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mstdn.social"><a href="https://mstdn.social">mstdn.social</a></td></tr>
<tr><td class="stats-count">55</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=social.vivaldi.net"><a href="https://social.vivaldi.net">social.vivaldi.net</a></td></tr>
<tr><td class="stats-count">49</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=wandering.shop"><a href="https://wandering.shop">wandering.shop</a></td></tr>
<tr><td class="stats-count">48</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=fosstodon.org"><a href="https://fosstodon.org">fosstodon.org</a></td></tr>
<tr><td class="stats-count">33</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mathstodon.xyz"><a href="https://mathstodon.xyz">mathstodon.xyz</a></td></tr>
<tr><td class="stats-count">27</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mastodon.online"><a href="https://mastodon.online">mastodon.online</a></td></tr>
<tr><td class="stats-count">26</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mastodon.scot"><a href="https://mastodon.scot">mastodon.scot</a></td></tr>
<tr><td class="stats-count">24</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=app.wafrn.net"><a href="https://app.wafrn.net">app.wafrn.net</a></td></tr>
<tr><td class="stats-count">19</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=indieweb.social"><a href="https://indieweb.social">indieweb.social</a></td></tr>
<tr><td class="stats-count">18</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=social.lol"><a href="https://social.lol">social.lol</a></td></tr>
<tr><td class="stats-count">17</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=tech.lgbt"><a href="https://tech.lgbt">tech.lgbt</a></td></tr>
<tr><td class="stats-count">17</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=toot.wales"><a href="https://toot.wales">toot.wales</a></td></tr>
<tr><td class="stats-count">16</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=en.osm.town"><a href="https://en.osm.town">en.osm.town</a></td></tr>
<tr><td class="stats-count">16</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=feditrends.com"><a href="https://feditrends.com">feditrends.com</a></td></tr>
<tr><td class="stats-count">14</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mstdn.ca"><a href="https://mstdn.ca">mstdn.ca</a></td></tr>
<tr><td class="stats-count">14</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=piefed.social"><a href="https://piefed.social">piefed.social</a></td></tr>
<tr><td class="stats-count">12</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=wetdry.world"><a href="https://wetdry.world">wetdry.world</a></td></tr>
<tr><td class="stats-count">11</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=c.im"><a href="https://c.im">c.im</a></td></tr>
<tr><td class="stats-count">11</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mastodon.nl"><a href="https://mastodon.nl">mastodon.nl</a></td></tr>
<tr><td class="stats-count">51</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=mastodon.social"> Sites sending &lt; 10 clicks</td></tr>
</tbody></table>

<p>Ah! Add them all up and you get a grand total of <strong>1,773 visitors from Mastodon-powered sites</strong>.  That's <em>more</em> than BlueSky.</p>

<p>Now, there are some obvious caveats to the data:</p>

<ul>
<li>I have a smaller follower count on BlueSky than I do on Mastodon.</li>
<li>My posts may appeal more to one demographic than another.</li>
<li>People may have strict privacy controls which suppress the true volume of visitors.</li>
<li>There's no way to measure how long someone spends reading my posts.</li>
<li>RSS and newsletter visitors aren't counted.</li>
<li>Clicks from apps may not always show a referer.</li>
<li>Some people may be on multiple services.</li>
<li>Fediverse users can follow the post directly, so don't need to visit the site to read it.</li>
</ul>

<p>And yet… no matter how you slice it, Fediverse servers are sending as much traffic as BlueSky!</p>

<p>I think this is brilliant. Web services should be able to scale from small to big - and each ActivityPub-powered site helps power the open Internet.</p>

<p>Just for completeness, this is how Reddit, Facebook, LinkedIn, Twitter, and Lemmy do over the same period:</p>

<table class="edent_stats_column"><thead><tr><th class="totals">Total</th><th>Source</th></tr></thead><tbody>
<tr><td class="stats-count">1,158</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=reddit.com"><a href="https://reddit.com">reddit.com</a></td></tr>
<tr><td class="stats-count">585</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=reddit.com"> android-app://com.reddit.frontpage/</td></tr>
<tr><td class="stats-count">76</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=facebook.com"><a href="https://facebook.com">facebook.com</a></td></tr>
<tr><td class="stats-count">76</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=old.reddit.com"><a href="https://old.reddit.com/r/programming/">https://old.reddit.com/r/programming/</a></td></tr>
<tr><td class="stats-count">56</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=www.reddit.com"><a href="https://www.reddit.com/r/programming/">https://www.reddit.com/r/programming/</a></td></tr>
<tr><td class="stats-count">52</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=youtube.com"><a href="https://youtube.com">youtube.com</a></td></tr>
<tr><td class="stats-count">41</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=t.co"><a href="https://t.co">t.co</a></td></tr>
<tr><td class="stats-count">38</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=old.reddit.com"><a href="https://old.reddit.com/r/todayilearned/comments/1nsw7f4/til_in_mongolia_instead_of_a_street_address_a/">https://old.reddit.com/r/todayilearned/comments/1nsw7f4/til_in_mongolia_instead_of_a_street_address_a/</a></td></tr>
<tr><td class="stats-count">31</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=linkedin.com"><a href="https://linkedin.com">linkedin.com</a></td></tr>
<tr><td class="stats-count">27</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=lemmy.world"> android-app://io.syncapps.lemmy_sync/</td></tr>
<tr><td class="stats-count">27</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=www.reddit.com"><a href="https://www.reddit.com/r/todayilearned/comments/1nsw7f4/til_in_mongolia_instead_of_a_street_address_a/">https://www.reddit.com/r/todayilearned/comments/1nsw7f4/til_in_mongolia_instead_of_a_street_address_a/</a></td></tr>
<tr><td class="stats-count">22</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=old.reddit.com"><a href="https://old.reddit.com/r/programming/comments/1n96ftn/40_years_later_are_bentleys_programming_pearls/">https://old.reddit.com/r/programming/comments/1n96ftn/40_years_later_are_bentleys_programming_pearls/</a></td></tr>
<tr><td class="stats-count">22</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=lemmy.ca"><a href="https://lemmy.ca">lemmy.ca</a></td></tr>
<tr><td class="stats-count">17</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=linkedin.com"> android-app://com.linkedin.android/</td></tr>
<tr><td class="stats-count">16</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=lemmy.dbzer0.com"><a href="https://lemmy.dbzer0.com">lemmy.dbzer0.com</a></td></tr>
<tr><td class="stats-count">14</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=feddit.org"><a href="https://feddit.org">feddit.org</a></td></tr>
<tr><td class="stats-count">11</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=www.reddit.com"><a href="https://www.reddit.com/r/programming/comments/1n96ftn/40_years_later_are_bentleys_programming_pearls/">https://www.reddit.com/r/programming/comments/1n96ftn/40_years_later_are_bentleys_programming_pearls/</a></td></tr>
<tr><td class="stats-count">10</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=discuss.tchncs.de"><a href="https://discuss.tchncs.de">discuss.tchncs.de</a></td></tr>
<tr><td class="stats-count">10</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=l.instagram.com"><a href="https://l.instagram.com">l.instagram.com</a></td></tr>
<tr><td class="stats-count">8</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=lemmy.blahaj.zone"><a href="https://lemmy.blahaj.zone">lemmy.blahaj.zone</a></td></tr>
<tr><td class="stats-count">6</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=www.reddit.com"><a href="https://www.reddit.com/r/GrapheneOS/comments/1m2l84b/considering_making_the_switch_does_google_pay/">https://www.reddit.com/r/GrapheneOS/comments/1m2l84b/considering_making_the_switch_does_google_pay/</a></td></tr>
<tr><td class="stats-count">6</td><td><img class="pingback-favicon" src="https://shkspr.mobi/favicons/?domain=reddthat.com"><a href="https://reddthat.com">reddthat.com</a></td></tr>
</tbody></table>

<p>If you add up all the Lemmy instances, they send about as much traffic as Facebook and LinkedIn combined. That's not a huge surprise - those platforms hate anyone clicking away to the wider web.</p>

<p>Twitter is basically <a href="https://en.wikipedia.org/wiki/Dead_Internet_theory">the Dead Internet</a>. I'm no longer on there, but I do occasionally search it to see who is sharing my posts. The popular posts I write get shared a <em>lot</em> - sometimes by accounts with huge followers - yet there are no comments or retweets and barely and clicks.</p>

<p>I don't do Instagram or Threads, and that might be reflected in their low numbers. But I'm not active on YouTube either - yet people there occasionally link back to me.</p>

<h2 id="final-thoughts"><a href="https://shkspr.mobi/blog/2025/11/now-witness-the-power-of-this-fully-operational-fediverse/#final-thoughts">Final Thoughts</a></h2>

<p>Firstly, my stats only represent my site. Your site might be very different.</p>

<p>Secondly, I've ignored search engine traffic, big blogs, newsletters, and other sources.</p>

<p>Thirdly, and most importantly, this <em>isn't</em> a competition! The desire for a "winner-takes-all" service is dangerous and disturbing. An ecosystem is at its most vibrant when there are multiple participants each thriving in their own niche.</p>

<p>I want a thousand sites, running a hundred different software stacks, some of which only serve a dozen people, or even a lone participant.</p>

<p>Diversity is strength.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=63716&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2025/11/now-witness-the-power-of-this-fully-operational-fediverse/feed/</wfw:commentRss>
			<slash:comments>10</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[The Peaceful Transfer of Power in Open Source Projects]]></title>
		<link>https://shkspr.mobi/blog/2025/11/the-peaceful-transfer-of-power-in-open-source-projects/</link>
					<comments>https://shkspr.mobi/blog/2025/11/the-peaceful-transfer-of-power-in-open-source-projects/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Wed, 19 Nov 2025 12:34:27 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[BDFL]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[oss]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=65112</guid>

					<description><![CDATA[Most of the people who run Open Source projects are mortal.  Recent history shows us that they will all eventually die, or get bored, or win the lottery, or get sick, or be conscripted, or lose their mind.  If you&#039;ve ever visited a foreign country&#039;s national history museum, I guarantee you&#039;ve read this little snippet:  King Whatshisface was a wise and noble ruler who bought peace and prosperity…]]></description>
										<content:encoded><![CDATA[<p>Most of the people who run Open Source projects are mortal.  Recent history shows us that they will all eventually die, or get bored, or win the lottery, or get sick, or be conscripted, or lose their mind.</p>

<p>If you've ever visited a foreign country's national history museum, I guarantee you've read this little snippet:</p>

<blockquote><p>King Whatshisface was a wise and noble ruler who bought peace and prosperity to all the land.</p>

<p>Upon his death, his heirs waged bloody war over rightful succession which plunged the country into a hundred years of hardship.</p></blockquote>

<p>The great selling point of democracy is that it allows for the peaceful transition of power. Most modern democracies have rendered civil war almost unthinkable. Sure, you might not like the guy currently in charge, but there are well established mechanisms to limit their power and kick them out if they misbehave. If they die in office, there's an obvious and understood hierarchy for who follows them.</p>

<p>Most Open Source projects start small - just someone in their spare room tinkering for fun. Unexpectedly, they grow into a behemoth which now powers half the world. These mini-empires are <em>fragile</em>. The most popular method of governance is the Benevolent Dictator For Life model. The founder of the project controls <em>everything</em>.  But, as I've said before, BDFL only works if the D is genuinely B. Otherwise the FL becomes FML.</p>

<p>The last year has seen several BDFLs act like Mad Kings. They become tyrannical despots, lashing out at their own volunteers. They execute takeovers of community projects. They demand fealty and tithes.  Like dragons, they become quick to anger when their brittle egos are tested. Spineless courtiers carry out deluded orders while pilfering the coffers.</p>

<p>Which is why I am <em>delighted</em> that the Mastodon project has shown a better way to behave.</p>

<p>In "<a href="https://blog.joinmastodon.org/2025/11/the-future-is-ours-to-build-together/">The Future is Ours to Build - Together</a>" they describe <em>perfectly</em> how to gracefully and peacefully transfer power. There are no VCs bringing in their MBA-brained lackeys to extract maximum value while leaving a rotting husk.  No one is seizing community assets and jealously hoarding them. Opaque financial structures and convoluted agreements are prominent in their absence.</p>

<p>Eugen Rochko, the outgoing CEO, has <a href="https://blog.joinmastodon.org/2025/11/my-next-chapter-with-mastodon/">a remarkably honest blog post about the transition</a>. I wouldn't wish success on my worst enemy. He talks plainly about the reality of dealing with the pressure and how he might have been a limiting factor on Mastodon's growth.  That's a far step removed from the ego-centric members of The Cult of The Founder with their passionate belief in the Divine Right of Kings.</p>

<p>Does your tiny OSS script need a succession plan? Probably not. Do you have several thousand NPM installs per day? It might be worth working out who you can share responsibility with if you are unexpectedly raptured. Do you think that your project is going to last for a thousand years? Build an organisation which won't crumble the moment its founder is arrested for their predatory behaviour on tropical islands.</p>

<p>I'm begging project leaders everywhere - please read up on the social contract and the consent of the governed. Or, if reading is too woke, just behave like grown-ups rather than squabbling tweenagers.</p>

<p>It is a sad inevitability that, eventually, we will all be nothing but memories. The bugs that we create live after us, the patches are oft interrèd with our code. Let it be so with all Open Source projects.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=65112&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2025/11/the-peaceful-transfer-of-power-in-open-source-projects/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Getting started with Mastodon's Quote Posts - technical implementation details for servers]]></title>
		<link>https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/</link>
					<comments>https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Fri, 03 Oct 2025 11:34:27 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=63527</guid>

					<description><![CDATA[Quoting posts on Mastodon is slightly complex. Because of the privacy conscious nature of the platform and its users, reposting isn&#039;t merely a case of sharing a URl.  A user writes a status. The user can choose to make their statuses quotable or not. What happens when a quoter quotes that post?  I&#039;ve read through the specification and tried to simplify it.  Quoting is a multi-step process:   The…]]></description>
										<content:encoded><![CDATA[<p>Quoting posts on Mastodon is <em>slightly</em> complex. Because of the privacy conscious nature of the platform and its users, reposting isn't merely a case of sharing a URl.</p>

<p>A user writes a status. The user can choose to make their statuses quotable or not. What happens when a quoter quotes that post?</p>

<p>I've <a href="https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md">read through the specification</a> and tried to simplify it.  Quoting is a multi-step process:</p>

<ol>
<li>The status <em>must</em> opt-in to being shared.</li>
<li>The quoter quotes the status.</li>
<li>The quoter's server sends a request to the status's server.</li>
<li>The status's server sends an accept message back to the quoter's server.</li>
<li>When other servers see the quote, they check with the status's server to see if it is allowed.</li>
</ol>

<p>I'm going to walk you through each stage as best as I understand them.</p>

<h2 id="opting-in"><a href="https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#opting-in">Opting In</a></h2>

<p>An ActivityPub status message is JSON. In order to opt-in, it needs this additional field.</p>

<pre><code class="language-JSON">"interactionPolicy": {
  "canQuote": {
    "automaticApproval": "https://www.w3.org/ns/activitystreams#Public"
  }
}
</code></pre>

<p>That tells ActivityPub clients that anyone is allowed to quote this post. It is also possible to say that only specific users, or only followers, or no-one is allowed.</p>

<h2 id="the-quoterequest"><a href="https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#the-quoterequest">The QuoteRequest</a></h2>

<p>Someone has hit the quote post button, typed their own message, and shared their wisdom. Their server sends the following message to the server which hosts the quoted status. This has been edited for brevity.</p>

<pre><code class="language-JSON">{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "QuoteRequest":   "https://w3id.org/fep/044f#QuoteRequest"
    }
  ],
  "type": "QuoteRequest",
  "id":     "https://mastodon.test/users/Edent/quote_requests/1234-5678-9101",
  "actor":  "https://mastodon.test/users/Edent",
  "object": "https://example.com/posts/987654321.json",
  "instrument": {
    "id":           "https://mastodon.test/users/Edent/statuses/123456789",
    "url":          "https://mastodon.test/@Edent/123456789",
    "attributedTo": "https://mastodon.test/users/Edent",
    "quote":          "https://example.com/posts/987654321.json",
    "_misskey_quote": "https://example.com/posts/987654321.json",
    "quoteUri":       "https://example.com/posts/987654321.json"
  }
}
</code></pre>

<p>All this says is "I would like permission to quote you."</p>

<h2 id="the-stamp"><a href="https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#the-stamp">The Stamp</a></h2>

<p>The quoted server needs to approve this quote. First, it generates a "stamp".</p>

<p>This is a file which lives on the quoted server. It is proof that the quote is allowed. If it is deleted, the quote permission is revoked. When the <a href="https://socialhub.activitypub.rocks/t/quote-post-implementation-issues/8032/2?u=eden_t">stamp's ID is requested the stamp <em>must</em> be returned</a>.</p>

<pre><code class="language-JSON">{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "gts": "https://gotosocial.org/ns#",
      "QuoteAuthorization": {
        "@id": "https://w3id.org/fep/044f#QuoteAuthorization",
        "@type": "@id"
      },
      "interactingObject": {
        "@id": "gts:interactingObject"
      },
      "interactionTarget": {
        "@id": "gts:interactionTarget"
      }
    }
  ],
  "type": "QuoteAuthorization",
  "id":                "https://example.com/quote-987654321.json",
  "attributedTo":      "https://example.com/users/username",
  "interactionTarget": "https://example.com/posts/987654321.json",
  "interactingObject": "https://mastodon.test/users/Edent/statuses/123456789"
}
</code></pre>

<p>If the quoted status is viewed from a different server, that server will query the stamp to make sure the share is allowed.</p>

<h2 id="the-accept"><a href="https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#the-accept">The Accept</a></h2>

<p>This is the message that the quoted server sends to the quoting server. It references the request and the stamp.</p>

<pre><code class="language-JSON">{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    {
      "QuoteRequest": "https://w3id.org/fep/044f#QuoteRequest"
    }
  ],
  "type": "Accept",
  "to":    "https://mastodon.test/users/Edent",
  "id":    "https://example.com/posts/987654321.json",
  "actor": "https://example.com/account",
  "object": {
    "type": "QuoteRequest",
    "id":         "https://mastodon.test/users/Edent/quote_requests/1234-5678-9101",
    "actor":      "https://mastodon.test/users/Edent",
    "instrument": "https://mastodon.test/users/Edent/statuses/123456789",
    "object":     "https://example.com/posts/987654321.json"
  },
  "result": "https://example.com/quote-987654321.json"
}
</code></pre>

<p>The "result" <em>must</em> be the same as the stamp's URl.</p>

<h2 id="and-then"><a href="https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/#and-then">And then?</a></h2>

<p>You can follow and quote <a href="https://colours.bots.edent.tel/">@colours@colours.bots.edent.tel</a> on your favourite Fediverse platform.</p>

<p>I've written an ActivityPub server in a single file which is designed to teach you have the protocol works. Have a play with <a href="https://gitlab.com/edent/activity-bot">ActivityBot</a>.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=63527&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2025/10/getting-started-with-mastodons-quote-posts-technical-implementation-details-for-servers/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Mastodon Now Sends Referer Headers! Hurrah!]]></title>
		<link>https://shkspr.mobi/blog/2024/12/mastodon-now-sends-referer-headers-hurrah/</link>
					<comments>https://shkspr.mobi/blog/2024/12/mastodon-now-sends-referer-headers-hurrah/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sat, 14 Dec 2024 12:34:25 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[mastodon]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=54457</guid>

					<description><![CDATA[Back in 2022, I wrote this rather grumpy post on Mastodon, the federated social media platform.  @Edent@mastodon.socialTerence EdenMastodon enforces a &#34;noreferrer&#34; on all external links.I have mixed feelings about that.As a blogger, I want to see *where* visitors are coming from. I also like to see (and sometimes join in) with the conversations they&#039;re having.But, I get that people want privacy…]]></description>
										<content:encoded><![CDATA[<p>Back in 2022, I wrote this rather grumpy post on Mastodon, the federated social media platform.</p>

<blockquote class="social-embed" id="social-embed-109323917419768019" lang="en" itemscope="" itemtype="https://schema.org/SocialMediaPosting"><header class="social-embed-header" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><a href="https://mastodon.social/@Edent" class="social-embed-user" itemprop="url"><img class="social-embed-avatar" src="https://files.mastodon.social/accounts/avatars/000/007/112/original/37df032a5951b96c.jpg" alt="" itemprop="image"><div class="social-embed-user-names"><p class="social-embed-user-names-name" itemprop="name">@Edent@mastodon.social</p>Terence Eden</div></a><img class="social-embed-logo" alt="Mastodon" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-label='Mastodon' role='img' viewBox='0 0 512 512' fill='%23fff'%3E%3Cpath d='m0 0H512V512H0'/%3E%3ClinearGradient id='a' y2='1'%3E%3Cstop offset='0' stop-color='%236364ff'/%3E%3Cstop offset='1' stop-color='%23563acc'/%3E%3C/linearGradient%3E%3Cpath fill='url(%23a)' d='M317 381q-124 28-123-39 69 15 149 2 67-13 72-80 3-101-3-116-19-49-72-58-98-10-162 0-56 10-75 58-12 31-3 147 3 32 9 53 13 46 70 69 83 23 138-9'/%3E%3Cpath d='M360 293h-36v-93q-1-26-29-23-20 3-20 34v47h-36v-47q0-31-20-34-30-3-30 28v88h-36v-91q1-51 44-60 33-5 51 21l9 15 9-15q16-26 51-21 43 9 43 60'/%3E%3C/svg%3E"></header><section class="social-embed-text" itemprop="articleBody"><p>Mastodon enforces a "noreferrer" on all external links.</p><p>I have mixed feelings about that.</p><p>As a blogger, I want to see *where* visitors are coming from. I also like to see (and sometimes join in) with the conversations they're having.</p><p>But, I get that people want privacy and don't want to "leak" where they're visiting from.</p><p>Is it such a bad thing to tell a website "I was referred from this specific server"?</p><div class="social-embed-media-grid"></div></section><hr class="social-embed-hr"><footer class="social-embed-footer"><a href="https://mastodon.social/@Edent/109323917419768019"><span aria-label="61 likes" class="social-embed-meta">❤️ 61</span><span aria-label="16 replies" class="social-embed-meta">💬 16</span><span aria-label="29 reposts" class="social-embed-meta">🔁 29</span><time datetime="2022-11-11T07:09:55.396Z" itemprop="datePublished">07:09 - Fri 11 November 2022</time></a></footer></blockquote>

<p>When you click on this link - <a href="https://www.bbc.co.uk/news">https://www.bbc.co.uk/news</a> - your browser says "Hey! BBC! Please can I have your <code>/news</code> page? BTW, I was referred here by <code>shkspr.mobi</code>. THANKS!"  This is called the "<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer">Referer</a>" and, yes, it is <a href="https://en.wikipedia.org/wiki/HTTP_referer#Etymology">mispelt</a>.</p>

<p>One the one hand, sending the referer is good; it lets the linked-to server know who is linking to it. That allows them to see where traffic is coming from. On the other hand, this <em>could</em> be bad for much the same reason.</p>

<p>If you run a server <code>anarcho_terrorists.biz</code>, you probably don't want the FBI knowing that your members are sharing links to their pages. If you run a small personal server, you may not want anyone knowing that you personally linked to them. If you run a server for a marginalised community, you may not want a hate-site to know your members are linking to you.</p>

<p>But if you're a large-ish, general purpose, non-private site - like Mastodon.social - where's the harm in allowing referer headers?</p>

<p>Anyway, for historic reasons, Mastodon blocked the referer header. This, I believe, was sensible for smaller servers but a miss-step for larger servers.  As I pointed out last week:</p>

<blockquote class="social-embed" id="social-embed-113611619218784737" lang="en" itemscope="" itemtype="https://schema.org/SocialMediaPosting"><header class="social-embed-header" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><a href="https://mastodon.social/@Edent" class="social-embed-user" itemprop="url"><img class="social-embed-avatar" src="https://files.mastodon.social/accounts/avatars/000/007/112/original/37df032a5951b96c.jpg" alt="" itemprop="image"><div class="social-embed-user-names"><p class="social-embed-user-names-name" itemprop="name">@Edent@mastodon.social</p>Terence Eden</div></a><img class="social-embed-logo" alt="Mastodon" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-label='Mastodon' role='img' viewBox='0 0 512 512' fill='%23fff'%3E%3Cpath d='m0 0H512V512H0'/%3E%3ClinearGradient id='a' y2='1'%3E%3Cstop offset='0' stop-color='%236364ff'/%3E%3Cstop offset='1' stop-color='%23563acc'/%3E%3C/linearGradient%3E%3Cpath fill='url(%23a)' d='M317 381q-124 28-123-39 69 15 149 2 67-13 72-80 3-101-3-116-19-49-72-58-98-10-162 0-56 10-75 58-12 31-3 147 3 32 9 53 13 46 70 69 83 23 138-9'/%3E%3Cpath d='M360 293h-36v-93q-1-26-29-23-20 3-20 34v47h-36v-47q0-31-20-34-30-3-30 28v88h-36v-91q1-51 44-60 33-5 51 21l9 15 9-15q16-26 51-21 43 9 43 60'/%3E%3C/svg%3E"></header><section class="social-embed-text" itemprop="articleBody"><p>Two years later.</p><p>Want to know one of the major reasons Mastodon didn't catch on with journalists and large website owners?</p><p>It is *invisible* in referrer statistics.</p><p>Here's my blog from the last month.</p><p>BlueSky now sends me more traffic than Bing.</p><p>How much traffic does Mastodon send? It is impossible to know due to the "noreferrer" header in all links.</p><p>(I'm not saying your privacy isn't important. But you can't grow a community if no-one knows you exist.)</p><div class="social-embed-media-grid"><a href="https://files.mastodon.social/media_attachments/files/113/611/599/519/383/213/original/f32f26cb4a0f015a.png" class="social-embed-media-link"><img class="social-embed-media" alt="1 google.com 10,957 12,1112 news.ycombinator.com 1,681 1,7633 duckduckgo.com 415 4584 css-tricks.com 353 3875 reddit.com 317 3736 yandex.ru 352 3567 google.co.uk 280 3058 bsky.app 252 2969 bing.com 254 282" src="data:image/webp;base64,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"></a></div></section><hr class="social-embed-hr"><footer class="social-embed-footer"><a href="https://mastodon.social/@Edent/113611619218784737"><span aria-label="305 likes" class="social-embed-meta">❤️ 305</span><span aria-label="57 replies" class="social-embed-meta">💬 57</span><span aria-label="248 reposts" class="social-embed-meta">🔁 248</span><time datetime="2024-12-07T12:48:52.705Z" itemprop="datePublished">12:48 - Sat 07 December 2024</time></a></footer></blockquote>

<p>I'm not the only one to make this point - it has been a popular complaint for some time.</p>

<p>A few days ago, <a href="https://github.com/mastodon/mastodon/pull/33214">Mastodon changed to allow this to be configurable</a>.</p>

<p>This is <em>excellent</em> news. Website owners will be able to (somewhat) accurately see how much traffic Mastodon sends them.  That way they can determine if there is a suitably large audience to engage with on the Fediverse.</p>

<p>It is, of course, slightly more complicated than that!</p>

<ul>
<li>Instance owners can opt-in to allowing Referer headers (it is off by default).</li>
<li>The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#directives">policy</a> means that only the domain name is sent; not the full page.</li>
<li>Mastodon is federated and there are thousands of sites. Even if they all opted-in, their statistics will be fragmented.</li>
<li>Apps can set their own Referer header - leading to more fragmentation.</li>
<li>Even if they do opt-in, users can set their browsers not to send Referer headers.</li>
</ul>

<p>Nevertheless, I'm delighted with this change. Hopefully it will allow the Fediverse to grow and attract more users.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=54457&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/12/mastodon-now-sends-referer-headers-hurrah/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Hashtag Standards (part deux)]]></title>
		<link>https://shkspr.mobi/blog/2024/12/hashtag-standards-part-deux/</link>
					<comments>https://shkspr.mobi/blog/2024/12/hashtag-standards-part-deux/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Wed, 04 Dec 2024 12:34:34 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[BlueSky]]></category>
		<category><![CDATA[hashtags]]></category>
		<category><![CDATA[mastodon]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=54180</guid>

					<description><![CDATA[What is a hashtag?  Fifteen years ago (fuck, I&#039;m old) I started documenting what Twitter&#039;s nascent hashtags could and couldn&#039;t do.  Back in 2010, this is how the official Twitter site linked hashtags.    Notably, punctuation symbols didn&#039;t &#34;count&#34; as part of a tag.  How does modern social media handle something like #Fish&#38;Chips?   Mastodon links directly to #Fish&#38;Chips BlueSky links directly to…]]></description>
										<content:encoded><![CDATA[<p>What is a hashtag?</p>

<p>Fifteen years ago (fuck, I'm old) I started documenting <a href="https://shkspr.mobi/blog/2010/02/hashtag-standards/">what Twitter's nascent hashtags could and couldn't do</a>.</p>

<p>Back in 2010, this is how the official Twitter site linked hashtags.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2010/02/Twitter-Website-Hashtag.png" alt="Screenshot from the Twitter website showing hashtags being linked." width="407" height="214" class="aligncenter size-full wp-image-25902">

<p>Notably, punctuation symbols didn't "count" as part of a tag.</p>

<p>How does modern social media handle something like #Fish&amp;Chips?</p>

<ul>
<li>Mastodon links directly to <a href="https://mastodon.social/tags/Fish&amp;Chips">#Fish&amp;Chips</a></li>
<li>BlueSky links directly to <a href="https://bsky.app/hashtag/Fish%26Chips">#Fish&amp;Chips</a></li>
<li>Threads links to a <em>search</em> for <a href="https://www.threads.net/search?q=Fish%26Chips&amp;serp_type=tags&amp;tag_id=18413420497040775">Fish &amp; Chips</a></li>
</ul>

<p>What about normalisation?</p>

<p>Should #Romeo link to #ROMEO and #rOMeO?</p>

<p>On all three of the major social networks, case is insensitive.</p>

<p>But what about the vagueries of <a href="https://www.unicode.org/reports/tr15/">Unicode normalisation</a>?</p>

<p>Is #Ŕöméø&amp;Jülíèt the same as #Romeo&amp;Juliet?</p>

<p>Both <a href="https://www.threads.net/search?q=%C5%94%C3%B6m%C3%A9%C3%B8%26J%C3%BCl%C3%AD%C3%A8t&amp;serp_type=tags&amp;tag_id=18475178065037431">Threads</a> and <a href="https://mastodon.social/tags/%C5%94%C3%B6m%C3%A9%C3%B8&amp;J%C3%BCl%C3%AD%C3%A8t">Mastodon</a> do some form of decomposition - turning the various accents into their accentless versions.</p>

<p>But <a href="https://bsky.app/hashtag/%C5%94%C3%B6m%C3%A9%C3%B8%26J%C3%BCl%C3%AD%C3%A8t">BlueSky links to the <em>literal</em> version</a>.</p>

<p>Is that the right thing to do? I don't know.</p>

<p>This literal interpretation of the text in hashtags <a href="https://shkspr.mobi/blog/2019/12/hashtag-steganography/">allows for some interesting steganography</a> - which can be fun, but I wonder if it is what users expect?</p>

<p>And that's what it comes down to. What is <em>technically</em> correct isn't always the same as what users <em>need</em>.</p>

<p>Perhaps most users prefer #ROMEO to link to the same posts as #romeo.  Perhaps they think #Romeó should link there too.  But no social network, as far as I am aware, has done any user research into the behaviour that users want when interacting with hashtags.</p>

<p>I'd love someone to do some actual research on how people expect a <a href="https://www.vanderwal.net/folksonomy.html">folksonomy</a> to work.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=54180&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/12/hashtag-standards-part-deux/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[A few thoughts on domain verification for social media]]></title>
		<link>https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/</link>
					<comments>https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 01 Dec 2024 12:34:24 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[BlueSky]]></category>
		<category><![CDATA[domains]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[Social Media]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=54056</guid>

					<description><![CDATA[Both Mastodon and BlueSky have the concept of &#34;self-verification&#34;. Rather than trust a central authority to assess your notability and then bless your account (as Twitter used to do), they let anyone self-attest using Domain Verification.  What does that mean?   You tell the service what your website is. The service gives you a secret code. You upload that secret code onto your website. The…]]></description>
										<content:encoded><![CDATA[<p>Both Mastodon and BlueSky have the concept of "self-verification". Rather than trust a central authority to assess your notability and then bless your account (as Twitter used to do), they let anyone self-attest using Domain Verification<sup id="fnref:complicated"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fn:complicated" class="footnote-ref" title="It is a lot more complicated than that - as per this essay by Christine Lemmer-Webber." role="doc-noteref">0</a></sup>.</p>

<p>What does that mean?</p>

<ul>
<li>You tell the service what your website is.</li>
<li>The service gives you a secret code<sup id="fnref:secret"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fn:secret" class="footnote-ref" title="Secret in the sense that they only generate it for you. It isn't private. Nothing bad will happen if other people see it." role="doc-noteref">1</a></sup>.</li>
<li>You upload that secret code onto your website.</li>
<li>The service checks the secret code is on the website.</li>
<li>If it is, the service says your domain is verified.</li>
</ul>

<p>On Mastodon, that gives you a green tick next to your link. On BlueSky, it gives you the ability to change your username to your website's name.</p>

<p>This is <em>reasonably</em> strong proof that you are the owner of that website. I don't have the ability to add the secret file I've been given to <code>bbc.co.uk</code>, so I cannot impersonate them.</p>

<p>But it isn't all sunshine and roses. There are some important issues with this process.</p>

<h2 id="revocation-and-revalidation"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#revocation-and-revalidation">Revocation and Revalidation</a></h2>

<p>Let's say an employee has validated <code>alice.big_company.com</code> - what happens when Alice leaves<sup id="fnref:alice"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fn:alice" class="footnote-ref" title="Let's assume she's naughty and doesn't remove the validation herself from her profile." role="doc-noteref">2</a></sup>?</p>

<p>Well, you just delete the secret code from your website, right?</p>

<p>In <em>theory</em> yes. But in practice, no.</p>

<p>From <a href="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial#:~:text=revalidate">BlueSky</a>:</p>

<blockquote><p>We're working on adding the ability to revalidate these handles periodically.</p></blockquote>

<p>And <a href="https://github.com/mastodon/mastodon/issues/27847">Mastodon</a>:</p>

<blockquote><p>Verified links are currently verified at each time the profile is updated, but they will only be verified once, when initially entered.</p></blockquote>

<p>So, at the moment, there is a risk that revalidation isn't completed and revocation never happens<sup id="fnref:rev"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fn:rev" class="footnote-ref" title="It appears that it takes BlueSky around 2 hour to detect and revoke verification." role="doc-noteref">3</a></sup>.  Accounts which were once trusted may stay trusted, even when they're no longer trustworthy.</p>

<h2 id="copy-cat-domains"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#copy-cat-domains">Copy Cat Domains</a></h2>

<p>You're chatting with your credit card company's social media account. You see that they've verified the domain.</p>

<p>Wait?! Are they <em>really</em> <code>mastercrrd.info ✅</code>?</p>

<p>There are several practical attacks against humans trying to validate a domain name. A simple misspelling is easy to overlook. There are thousands of top level domains, and you may not be sure if your bank uses .com, .uk, .tech, or something else.  It only costs a few quid for an attacker to buy a domain which contains a politician's name.</p>

<p>International domain names mean that <a href="https://www.malwarebytes.com/blog/news/2017/10/out-of-character-homograph-attacks-explained">homograph attacks</a> are possible.</p>

<h2 id="humans-arent-very-clever"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#humans-arent-very-clever">Humans aren't very clever</a></h2>

<p>Recently, several prominent journalists on BlueSky embarrassed themselves by pronouncing fake accounts to be real. The journalists - with all their resources and contacts - didn't bother to actually verify if the person who registered <code>@KemiBadenoch</code> was really the Leader of the Opposition.</p>

<p>They could have checked her website to see if it linked to the new account. They could have rung up the Tory press office. They could have checked to see if she have verified her account. Or they could have done a dozen other things to verify the facts before posting.  They didn't.</p>

<p>These aren't random users blindly reposting. These are highly educated, thoroughly trained fact-finders. Their mission is accuracy and their livelihood depends on being able to report the truth. And yet they just <em>assumed</em> that no one would lie on the Internet.</p>

<p>Would a journalist be able to spot that <code>tailer-swift.fartotron.xyz</code> was an impersonator? I highly doubt it<sup id="fnref:wrong"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fn:wrong" class="footnote-ref" title="Prove me wrong. Seriously. So many journalists seem utterly credulous." role="doc-noteref">4</a></sup>.</p>

<h2 id="hacks-happen"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#hacks-happen">Hacks Happen</a></h2>

<p>Even when Twitter was validating celebrities correctly, it didn't stop <a href="https://www.bbc.co.uk/news/technology-65540901">the accounts getting hacked</a>.</p>

<p>An attacker might compromise your social media account <em>or</em> your domain name registrar.</p>

<p>Just because an account and domain appear verified, it doesn't mean they're legitimate. Is that politician you follow <em>really</em> posting about dietary supplements?</p>

<h2 id="it-might-be-too-difficult-for-large-organisation"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#it-might-be-too-difficult-for-large-organisation">It might be too difficult for large organisation</a></h2>

<p>I've written <a href="https://shkspr.mobi/blog/2024/11/an-easy-guide-to-bluesky-verification/">An Easy Guide To BlueSky Verification</a>. It can be as simple as uploading a single file to your website. Although I have some sympathy for claims that managing the process for hundreds of employees might be difficult.</p>

<p>Based on <a href="https://bsky.app/profile/edent.tel/post/3lbwpu7zmuc2r">my calculations</a> around 5% of active BlueSky users have verified their domain.</p>

<h2 id="the-alternative-isnt-much-better"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#the-alternative-isnt-much-better">The alternative isn't much better</a></h2>

<p>Verification is <em>hard</em>. Can an over-worked verification team spot that I've photoshopped a passport so that it looks like someone else's?</p>

<p>There are hundred of famous people called <a href="https://en.wikipedia.org/wiki/John_Williams_(disambiguation)">John Williams</a> - which one do you verify?</p>

<p>Also, <em>what</em> are you verifying? In my post on <a href="https://shkspr.mobi/blog/2021/08/rethinking-twitter-verification/">Rethinking Twitter Verification</a>, I pointed out that the ambiguity of verification leads to some weird and non-obvious outcomes.</p>

<h2 id="final-thoughts"><a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#final-thoughts">Final thoughts</a></h2>

<p>There are no simple technological fixes to complex social issues.</p>

<p>But I'm naïve enough to believe that, with time, we can train people to be better at assessing the information they are given.</p>

<div id="footnotes" role="doc-endnotes">
<hr>
<ol start="0">

<li id="fn:complicated">
<p>It is a <em>lot</em> more complicated than that - <a href="https://dustycloud.org/blog/how-decentralized-is-bluesky/">as per this essay by Christine Lemmer-Webber</a>.&nbsp;<a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fnref:complicated" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:secret">
<p>Secret in the sense that they only generate it for you. It isn't private. Nothing bad will happen if other people see it.&nbsp;<a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fnref:secret" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:alice">
<p>Let's assume she's naughty and doesn't remove the validation herself from her profile.&nbsp;<a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fnref:alice" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:rev">
<p>It appears that <a href="https://bsky.app/profile/edent.tel/post/3lbcbpad5m42p">it takes BlueSky around 2 hour to detect and revoke verification</a>.&nbsp;<a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fnref:rev" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:wrong">
<p>Prove me wrong. Seriously. <a href="https://bsky.app/profile/edent.tel/post/3lb6glt5d7k2m">So many journalists seem utterly credulous</a>.&nbsp;<a href="https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/#fnref:wrong" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

</ol>
</div>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=54056&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/12/a-few-thoughts-on-domain-verification-for-social-media/feed/</wfw:commentRss>
			<slash:comments>9</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Set your domain name as your handle for a BlueSky bot which is bridged from the Fediverse]]></title>
		<link>https://shkspr.mobi/blog/2024/11/set-your-domain-name-as-your-handle-for-a-bluesky-bot-which-is-bridged-from-the-fediverse/</link>
					<comments>https://shkspr.mobi/blog/2024/11/set-your-domain-name-as-your-handle-for-a-bluesky-bot-which-is-bridged-from-the-fediverse/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Thu, 14 Nov 2024 12:34:32 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[BlueSky]]></category>
		<category><![CDATA[bot]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=53849</guid>

					<description><![CDATA[If you&#039;ve found this page, it&#039;s because you are me in the future and want to remember these instructions!   Create an account on the Fediverse using a domain you control   For example @user@bots.example.com  Follow the Fediverse-ATProto bridge @bsky.brid.gy@bsky.brid.gy   Your account will need to be over 2 weeks old and have a name, profile picture, etc.  You now have an account on BSky! Its…]]></description>
										<content:encoded><![CDATA[<p>If you've found this page, it's because you are me in the future and want to remember these instructions!</p>

<ol start="0">
<li>Create an account on the Fediverse using a domain you control

<ul>
<li>For example <code>@user@bots.example.com</code></li>
</ul></li>
<li><a href="https://fed.brid.gy/docs#fediverse-get-started">Follow the Fediverse-ATProto bridge</a> <code>@bsky.brid.gy@bsky.brid.gy</code>

<ul>
<li><a href="https://fed.brid.gy/docs#troubleshooting">Your account will need to be over 2 weeks old and have a name, profile picture, etc</a>.</li>
</ul></li>
<li>You now have an account on BSky! Its name will be something like <code>user.bots.example.com.ap.brid.gy</code></li>
<li>Get the DID of your account

<ul>
<li><code>https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=user.bots.example.com.ap.brid.gy</code></li>
<li>Or <code>https://fed.brid.gy/ap/@user@bots.example.com</code></li>
</ul></li>
<li><a href="https://atproto.com/specs/handle#handle-resolution">Add the DID to your domain</a>

<ul>
<li>I think the easiest way is sticking it in a plain text file at <code>bots.example.com/.well-known/atproto-did</code></li>
</ul></li>
<li>Use the <a href="https://bsky-debug.app/handle?handle=user.bots.example.com.ap.brid.gy">BSky Debugger</a> to make sure it was successful.</li>
<li>Send a Direct Message from the Fediverse to <code>@bsky.brid.gy@bsky.brid.gy</code>. The message must only contain <code>username bots.example.com</code>.

<ul>
<li>That's <em>literally</em> the word <code>username</code>. It isn't your account's username.</li>
</ul></li>
<li>Wait a few moments.</li>
<li>Your bot will now be on BSky as <code>https://bsky.app/profile/bots.example.com</code>!</li>
</ol>

<p>You can see that <a href="https://bot.viii.fi/bot">https://bot.viii.fi/bot</a> is also available at <a href="https://bsky.app/profile/bot.viii.fi">https://bsky.app/profile/bot.viii.fi</a></p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=53849&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/11/set-your-domain-name-as-your-handle-for-a-bluesky-bot-which-is-bridged-from-the-fediverse/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Social Media Blocking Has Always Been A Lie]]></title>
		<link>https://shkspr.mobi/blog/2024/09/social-media-blocking-has-always-been-a-lie/</link>
					<comments>https://shkspr.mobi/blog/2024/09/social-media-blocking-has-always-been-a-lie/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 24 Sep 2024 11:34:48 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[BlueSky]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[Social Media]]></category>
		<category><![CDATA[twitter]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=53274</guid>

					<description><![CDATA[What does it mean to block someone on a social media site?  Way back in the mists of time, we dealt with trolls on Usenet with the almighty PLONK - PLaced On Newsgroup Killfile.  It meant your newsreader never downloaded their posts. They could rant at you all day long, and you&#039;d never hear from them.  It&#039;s what we would nowadays call &#34;Mute&#34;.  But, whether you&#039;re on Usenet or a modern social…]]></description>
										<content:encoded><![CDATA[<p>What does it mean to block someone on a social media site?</p>

<p>Way back in the mists of time, we dealt with trolls on Usenet with the almighty PLONK - <a href="https://members.newsdemon.com/what-is-plonk.php">PLaced On Newsgroup Killfile</a>.  It meant your newsreader never downloaded their posts. They could rant at you all day long, and you'd never hear from them.  It's what we would nowadays call "Mute".</p>

<p>But, whether you're on Usenet or a modern social network, muting someone doesn't actually stop them replying to you. The miscreant can still see your posts, interact with them, quote them. And everyone on that service can see their abuse. Perhaps they will also join in?</p>

<p>Most modern social networks now have the concept of "Block". When Alice blocks Bob, it means Bob cannot see Alice's posts.  The service doesn't deliver her content to him. If he goes looking, he can't find it. She is invisible to him.</p>

<p>Except, of course, that's a lie. If Bob logs out of his account, he can see Alice's public content. If he logs into an alternative account, he isn't blocked.</p>

<p>The block is a <em>social signal</em> backed up with mild technical restrictions.</p>

<p>What do I mean by that? Ordinarily, you will have no idea that you have been blocked by someone. They will simply vanish from your screens.  You do not receive an alert that you've been blocked. Technical restrictions mean you won't see their posts, nor replies to them.  The only way you might know is if you deliberately look for the person blocking you.</p>

<p>Seeing that you have been blocked is a "social signal". It lets you know that your behaviour was unwanted, or that your contributions weren't valued, or that someone just doesn't like you.  For most people, that sort of chastisement probably induces a little shame or grief.  For others, it is enraging.</p>

<p>Again, it isn't impossible for a blocked user to see content - but technical restrictions means it takes <em>effort</em>.  And, it turns out, for all but the most obsessive abusers - a mild bit of UI friction is all that it takes for them to stop.</p>

<p>On a centralised social media platform, like Twitter and Facebook, your blocks are private. The only people who know you have blocked Taylor Swift are you, the platform, and T-Swizzle herself.</p>

<p>On decentralised social media platforms, it is more complicated.</p>

<p>Mastodon / ActivityPub lets you block a user. In doing so, you have to tell that user's server that you don't want them seeing your messages. That means your server knows about the block, their server know, and the user knows. But, crucially, there's nothing to stop a malicious server ignoring your wishes.  While your server can mute all the interactions from them, there are only <a href="https://fedi.tips/authorized-fetch/">weak technological restrictions on their behaviour</a>.</p>

<p>BlueSky / AT Protocol takes a different (and more worrying) approach. BlueSky tells <em>everyone</em> about your blocks. If Alice blocks Bob - the system lets everyone know. This means that if Bob starts replying to your posts, other clients will know to ignore his interactions with you. I've written more <a href="https://bsky.app/profile/edent.tel/post/3l4rjxx32br2j">about the dangers of public blocklists over on BSky</a>.</p>

<p>But, crucially, <strong>none of these systems actually block users</strong>.  This isn't like that <a href="https://black-mirror.fandom.com/wiki/White_Christmas">Black Mirror episode</a> where people are literally blurred out from your eyeballs.</p>

<p>In <em>all</em> cases, a user can log out and see your public posts. They can sign in with an alternative account. And, in the case of decentralised social media, they can choose to ignore the technological restrictions you impose.</p>

<p>Social networks have a responsibility to keep their users safe. That means having enough friction to prevent casual abuse.</p>

<p>But blocking is <em>only</em> a social signal.  That's all it ever has been. It is a boop on the nose with a rolled up newspaper. It is a message to tell someone that they might want to adjust their attitude.</p>

<p>You should block - and block often. You should feel empowered to curate an environment that is safe for you. But you should also understand the limitations of the technical controls which underpin these social signals.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=53274&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/09/social-media-blocking-has-always-been-a-lie/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[No, ActivityPub votes aren't anonymous]]></title>
		<link>https://shkspr.mobi/blog/2024/09/no-activitypub-isnt-anonymous/</link>
					<comments>https://shkspr.mobi/blog/2024/09/no-activitypub-isnt-anonymous/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 09 Sep 2024 11:34:07 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[privacy]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=52999</guid>

					<description><![CDATA[Several years ago, I posted this poll on Twitter.  Terence Eden is on Mastodon@edentIf the recent Twitter hack had exposed they way you voted on every Twitter poll, how would you feel?(There is no suggestion that this has happened, I&#039;m just curious about people&#039;s relationships to voting and privacy.)Meh. So what?: (167)167Hmph. That&#039;s annoying.: (68)68Umm… This could be bad!: (32)32Delete account …]]></description>
										<content:encoded><![CDATA[<p>Several years ago, I posted this poll on Twitter.</p>

<blockquote class="social-embed" id="social-embed-1286178187937042432" lang="en" itemscope="" itemtype="https://schema.org/SocialMediaPosting"><header class="social-embed-header" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><a href="https://twitter.com/edent" class="social-embed-user" itemprop="url"><img class="social-embed-avatar social-embed-avatar-circle" src="data:image/webp;base64,UklGRkgBAABXRUJQVlA4IDwBAACQCACdASowADAAPrVQn0ynJCKiJyto4BaJaQAIIsx4Au9dhDqVA1i1RoRTO7nbdyy03nM5FhvV62goUj37tuxqpfpPeTBZvrJ78w0qAAD+/hVyFHvYXIrMCjny0z7wqsB9/QE08xls/AQdXJFX0adG9lISsm6kV96J5FINBFXzHwfzMCr4N6r3z5/Aa/wfEoVGX3H976she3jyS8RqJv7Jw7bOxoTSPlu4gNbfXYZ9TnbdQ0MNnMObyaRQLIu556jIj03zfJrVgqRM8GPwRoWb1M9AfzFe6Mtg13uEIqrTHmiuBpH+bTVB5EEQ3uby0C//XOAPJOFv4QV8RZDPQd517Khyba8Jlr97j2kIBJD9K3mbOHSHiQDasj6Y3forATbIg4QZHxWnCeqqMkVYfUAivuL0L/68mMnagAAA" alt="" itemprop="image"><div class="social-embed-user-names"><p class="social-embed-user-names-name" itemprop="name">Terence Eden is on Mastodon</p>@edent</div></a><img class="social-embed-logo" alt="Twitter" src="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0Aaria-label%3D%22Twitter%22%20role%3D%22img%22%0AviewBox%3D%220%200%20512%20512%22%3E%3Cpath%0Ad%3D%22m0%200H512V512H0%22%0Afill%3D%22%23fff%22%2F%3E%3Cpath%20fill%3D%22%231d9bf0%22%20d%3D%22m458%20140q-23%2010-45%2012%2025-15%2034-43-24%2014-50%2019a79%2079%200%2000-135%2072q-101-7-163-83a80%2080%200%200024%20106q-17%200-36-10s-3%2062%2064%2079q-19%205-36%201s15%2053%2074%2055q-50%2040-117%2033a224%20224%200%2000346-200q23-16%2040-41%22%2F%3E%3C%2Fsvg%3E"></header><section class="social-embed-text" itemprop="articleBody">If the recent Twitter hack had exposed they way you voted on every Twitter poll, how would you feel?<br><br>(There is no suggestion that this has happened, I'm just curious about people's relationships to voting and privacy.)<hr class="social-embed-hr"><label for="poll_1_count">Meh. So what?: (167)</label><br><meter class="social-embed-meter" id="poll_1_count" min="0" max="100" low="33" high="66" value="60.7">167</meter><br><label for="poll_2_count">Hmph. That's annoying.: (68)</label><br><meter class="social-embed-meter" id="poll_2_count" min="0" max="100" low="33" high="66" value="24.7">68</meter><br><label for="poll_3_count">Umm… This could be bad!: (32)</label><br><meter class="social-embed-meter" id="poll_3_count" min="0" max="100" low="33" high="66" value="11.6">32</meter><br><label for="poll_4_count">Delete account &amp; run away: (8)</label><br><meter class="social-embed-meter" id="poll_4_count" min="0" max="100" low="33" high="66" value="2.9">8</meter></section><hr class="social-embed-hr"><footer class="social-embed-footer"><a href="https://twitter.com/edent/status/1286178187937042432"><span aria-label="0 likes" class="social-embed-meta">❤️ 0</span><span aria-label="8 replies" class="social-embed-meta">💬 8</span><span aria-label="0 reposts" class="social-embed-meta">🔁 0</span><time datetime="2020-07-23T05:55:50.000Z" itemprop="datePublished">05:55 - Thu 23 July 2020</time></a></footer></blockquote>

<p>Most of the tech world that I interact with has moved to Mastodon and other ActivityPub-based social networks.  Decentralised social media is <em>great</em>. It allows you to be fully in control of what you post, what you see, and how you interact with others.</p>

<p>Of course, there are downsides. No centralised authorities means verification is difficult. Abuse (of all sorts) can only be dealt with in a piecemeal fashion. And anonymity takes a bit of a nosedive.</p>

<p>When you block or mute someone, that information <a href="https://shkspr.mobi/blog/2023/07/fediverse-account-portability-and-blocking/">might leak to the offending user</a>. By its nature, you need to send a message to someone else's server in order to interact with them.</p>

<p>So what about polls on the Fediverse?  This poll, for example, is gathering sensitive personal information.</p>

<blockquote class="social-embed" id="social-embed-113079948257450773" lang="en" itemscope="" itemtype="https://schema.org/SocialMediaPosting"><header class="social-embed-header" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><a href="https://blackrock.city/@farooqkz" class="social-embed-user" itemprop="url"><img class="social-embed-avatar" src="https://files.mastodon.social/cache/accounts/avatars/110/487/620/471/518/769/original/8e8c3e9b9b7b122e.png" alt="" itemprop="image"><div class="social-embed-user-names"><p class="social-embed-user-names-name" itemprop="name">@farooqkz@blackrock.city</p>Farooq Karimi Zadeh</div></a><img class="social-embed-logo" alt="Mastodon" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-label='Mastodon' role='img' viewBox='0 0 512 512' fill='%23fff'%3E%3Cpath d='m0 0H512V512H0'/%3E%3ClinearGradient id='a' y2='1'%3E%3Cstop offset='0' stop-color='%236364ff'/%3E%3Cstop offset='1' stop-color='%23563acc'/%3E%3C/linearGradient%3E%3Cpath fill='url(%23a)' d='M317 381q-124 28-123-39 69 15 149 2 67-13 72-80 3-101-3-116-19-49-72-58-98-10-162 0-56 10-75 58-12 31-3 147 3 32 9 53 13 46 70 69 83 23 138-9'/%3E%3Cpath d='M360 293h-36v-93q-1-26-29-23-20 3-20 34v47h-36v-47q0-31-20-34-30-3-30 28v88h-36v-91q1-51 44-60 33-5 51 21l9 15 9-15q16-26 51-21 43 9 43 60'/%3E%3C/svg%3E"></header><section class="social-embed-text" itemprop="articleBody"><p>Let's see how many Muslims are out there on Fediverse. Are you a <a href="https://blackrock.city/tags/muslim" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>muslim</span></a>? </p><p><a href="https://blackrock.city/tags/Islam" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>Islam</span></a> <a href="https://blackrock.city/tags/Religion" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>Religion</span></a> <a href="https://blackrock.city/tags/God" class="mention hashtag" rel="nofollow noopener noreferrer" target="_blank">#<span>God</span></a> </p><p>Please boost it so we can have more accurate statistics.</p><div class="social-embed-media-grid"></div><hr class="social-embed-hr"><label for="poll_0">I am a Muslim: (62)</label><br><meter class="social-embed-meter" id="poll_0" min="0" max="100" low="33" high="66" value="1.6">62</meter><br><label for="poll_1">Not a Muslim: (3,696)</label><br><meter class="social-embed-meter" id="poll_1" min="0" max="100" low="33" high="66" value="98.4">3696</meter><br></section><hr class="social-embed-hr"><footer class="social-embed-footer"><a href="https://blackrock.city/@farooqkz/113079948160094406"><span aria-label="11 likes" class="social-embed-meta">❤️ 11</span><span aria-label="30 replies" class="social-embed-meta">💬 30</span><span aria-label="874 reposts" class="social-embed-meta">🔁 874</span><time datetime="2024-09-04T15:17:56.000Z" itemprop="datePublished">15:17 - Wed 04 September 2024</time></a></footer></blockquote>

<p>In order to vote on the poll, your server sends a message to the poll's server saying "I am user @someone@example.com. I wish to vote for option X. Here is an HTTP signature confirming my message."</p>

<p>Does the receiving server abide by GDPR? Who knows!</p>

<p>The <a href="https://www.w3.org/TR/activitystreams-vocabulary/#questions">specification around questions</a> is a little ill-defined and the <a href="https://docs.joinmastodon.org/methods/polls/#vote">Mastodon documentation</a> is also a bit vague. Neither of them discuss privacy.</p>

<p>There is an <a href="https://humberto.io/blog/mastodon_poll_in_activitypub/">excellent blog post by Humberto Rocha looking at Mastodon Poll in ActivityPub</a>. It shows quite clearly that a vote is just a normal message which is passed onto the receiving server.</p>

<p>Services like Mastodon won't let the poll's author see who voted for which option. But that's by convention. There's nothing technical to stop them. Indeed, I understand that <a href="https://outerheaven.club/notice/Aln6q1bVGpyToIx7J2">the Akkoma social network <em>does</em> show users how users voted</a>.</p>

<p>Of course, on a centralised service like Facebook or Twitter your vote is still recorded somewhere. It can be subpoenaed or looked at by unscrupulous engineers.</p>

<p>Privacy is, of course, a social construct. In some communities it might be sensible to have all votes on the public record. In others, it could be deadly.  Some countries have laws mandating strong privacy protections, others less so.</p>

<p>Conduct yourself with that in mind!</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=52999&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/09/no-activitypub-isnt-anonymous/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[The Limits of Organic Growth for Startups and Social Networks]]></title>
		<link>https://shkspr.mobi/blog/2024/08/the-limits-of-organic-growth-for-startups/</link>
					<comments>https://shkspr.mobi/blog/2024/08/the-limits-of-organic-growth-for-startups/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 05 Aug 2024 11:34:01 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[capitalism]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[startups]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=51194</guid>

					<description><![CDATA[Many years ago, when I was younger and more foolish, I worked for an advertising startup. Things seemed to be going pretty well! The office was expanding, the sales team was screaming into phones, the budget for servers was rising. Growth had been healthy, but now looked to be plateauing.  One day we were summoned into a large conference room. Our CEO was on the speakerphone (I told you this was…]]></description>
										<content:encoded><![CDATA[<p>Many years ago, when I was younger and more foolish, I worked for an advertising startup. Things seemed to be going pretty well! The office was expanding, the sales team was screaming into phones, the budget for servers was rising. Growth had been healthy, but now looked to be plateauing.</p>

<p>One day we were summoned into a large conference room. Our CEO was on the speakerphone (I told you this was a long time ago) with an important update on our financial situation.</p>

<p>I think every arsehole in the room puckered. Were we about to be made redundant? No! Far from it. The business had just taken on a <em>massive</em> amount of investment from a prominent Venture Capital fund.  The champagne flowed! With this money we could turbo-charge our hiring, build new products, and accelerate our growth.</p>

<p>At least, that's what I thought. In my naïveté, I congratulated our head of sales on the growth of his empire. More money meant more sales staff, yes?</p>

<p>He was glum. "That's not how it works," he explained.</p>

<p>Our large institutional investors had a vast stable of other companies. Each covered a different bit of the tech ecosystem and our advertising business was about to become part of an incestuous web.  Every app that the VCs had invested in would now be "encouraged" to use our advertising solution. You don't need a sales team when your customers are your cousins in an arranged marriage.</p>

<p>Similarly, our approved corporate chat tool would be replaced by one from another stable-mate.  Who, in turn, would buy advertising from us.</p>

<p>Over the next few months our growth rocketed and almost none of it was organic.</p>

<p>Eventually I left for pastures marginally more ethical than advertising. But I continue to see the same pattern repeat itself.</p>

<p>When you wonder why one social network grows and another doesn't - look at the investors.</p>

<p>For example, I'm sure a lot of Twitter's early growth was organic. But once rich and powerful companies can direct their investments to sign up and route all their customer service / product announcements through there, it exploded.  When you have a financial investment at stake, finding ways to boost growth is a priority - and a little mutually reinforced reciprocation goes a long way.</p>

<p>I don't know if Mastodon (and other ActivityPub services) have reached their maximum organic growth. I suspect they have. To be clear, I'm not calling on Mastodon to take on a billion dollars of VC funding. But without sales teams and without a bunch of associated organisations forced to use it, I worry that the Fediverse will hit a natural limit.</p>

<p>And, again, I don't know if that's a bad thing <i lang="la">per se</i> - but if you're expecting big companies, governments, and your favourite VC-adjacent celebrities to join, you're likely to be disappointed.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=51194&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/08/the-limits-of-organic-growth-for-startups/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Who can reply?]]></title>
		<link>https://shkspr.mobi/blog/2024/06/who-can-reply/</link>
					<comments>https://shkspr.mobi/blog/2024/06/who-can-reply/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 25 Jun 2024 11:34:08 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[Social Media]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=50898</guid>

					<description><![CDATA[Vague thoughts as they enter my brainbox.  The BlueSky social network has introduced &#34;Reply Gating&#34; - it looks like this:   You can write your hot take on Taylor Swift and not be inundated by weirdos replying to you. Nifty!  This is nothing new. Twitter has it. Facebook has the concept of &#34;audiences&#34; to restrict who your post is visible to.    And, of course, blogging has this! There is a comment …]]></description>
										<content:encoded><![CDATA[<p>Vague thoughts as they enter my brainbox.</p>

<p>The <a href="https://bsky.app/">BlueSky social network</a> has introduced "Reply Gating" - it looks like this:
<img src="https://shkspr.mobi/blog/wp-content/uploads/2024/06/Bluesky-fs8.png" alt="Who can reply?
Choose &quot;Everybody&quot; or &quot;Nobody&quot; Or combine these options: Mentioned users, Followed users." width="720" height="472" class="aligncenter size-full wp-image-50899"></p>

<p>You can write your hot take on Taylor Swift and <em>not</em> be inundated by weirdos replying to you. Nifty!</p>

<p>This is nothing new. Twitter has it. Facebook has the concept of "audiences" to restrict who your post is visible to.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2024/06/Facebook-fs8.png" alt="Facebook's audience page with options to share to select groups." width="720" height="869" class="aligncenter size-full wp-image-50900">

<p>And, of course, blogging has this! There is a comment form at the bottom of this page - and I moderate it. If you post something stupid, I don't have to subject my audience to your inanities. I can (and do) block users from commenting.</p>

<p>ActivityPub doesn't have this (yet). It's much more like a public mailing list.  I can block or mute you - which stops me from seeing your abuse - but doesn't stop anyone else from seeing it.</p>

<p>Should ActivityPub have something similar? Yeah, I reckon so. I'd like to be able to say "Anyone I know want to go to the pub tonight" and only have mutuals reply. I want to prune away spam or repetitive replies.  It would be helpful to have a conversation in public that other people can't interrupt.</p>

<p>The UI would be complex. And the social model needs a bit of work. And there are some technical challenges around syndicating <em>which</em> replies should be included.</p>

<p>But, ultimately, social media should respond to the needs of its users.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=50898&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/06/who-can-reply/feed/</wfw:commentRss>
			<slash:comments>16</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[How updates work in ActivityPub / Mastodon]]></title>
		<link>https://shkspr.mobi/blog/2024/03/how-updates-work-in-activitypub-mastodon/</link>
					<comments>https://shkspr.mobi/blog/2024/03/how-updates-work-in-activitypub-mastodon/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Fri, 15 Mar 2024 12:34:28 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=49894</guid>

					<description><![CDATA[I didn&#039;t realise this, so I&#039;m documenting it to stop other people making the same silly mistake that I did.  Messages in ActivityPub have two distinct ID strings.  Here&#039;s a (truncated) view of what happens when I send a new message on Mastodon:    &#38;quot;id&#38;quot;: &#38;quot;https://mastodon.social/users/Edent/statuses/1234567890/activity&#38;quot;,   &#38;quot;type&#38;quot;: &#38;quot;Create&#38;quot;,  …]]></description>
										<content:encoded><![CDATA[<p>I didn't realise this, so I'm documenting it to stop other people making the same silly mistake that I did.</p>

<p>Messages in ActivityPub have <em>two distinct</em> ID strings.  Here's a (truncated) view of what happens when I send a new message on Mastodon:</p>

<pre><code class="language-json">  &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890/activity&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;Create&amp;quot;,
  &amp;quot;actor&amp;quot;: &amp;quot;https://mastodon.social/users/Edent&amp;quot;,
  &amp;quot;published&amp;quot;: &amp;quot;2024-03-10T16:13:49Z&amp;quot;,
  &amp;quot;object&amp;quot;: {
    &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;Note&amp;quot;,
    &amp;quot;content&amp;quot;: &amp;quot;Hello&amp;quot;
...
</code></pre>

<p>The "Note" has some human-readable content, some metadata, and an ID - in this case a URl ending with <code>1234567890</code></p>

<p>But that Note is wrapped in an <em>Activity</em>. In this case, it is a "Create" message, with some metadata, and it's <em>own</em> ID - in this case ending <code>1234567890/activity</code></p>

<p>What happens if I edit the post? Here's a truncated view of what the server sends:</p>

<pre><code class="language-json">  &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890#updates/1710087334&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;Update&amp;quot;,
  &amp;quot;actor&amp;quot;: &amp;quot;https://mastodon.social/users/Edent&amp;quot;,
  &amp;quot;published&amp;quot;: &amp;quot;2024-03-10T16:15:34Z&amp;quot;,
  &amp;quot;object&amp;quot;: {
    &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;Note&amp;quot;,
    &amp;quot;content&amp;quot;: &amp;quot;I meant Goodbye!&amp;quot;
...
</code></pre>

<p>The "Note" has the <em>same</em> ID as before - but the activity has a <em>different</em> ID.</p>

<p>Further updates follow the same pattern:</p>

<pre><code class="language-JSON">  &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890#updates/1984651324&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;Update&amp;quot;,
  &amp;quot;object&amp;quot;: {
    &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890&amp;quot;,
...
</code></pre>

<p>So what happens when I <em>Delete</em> a previously updated post?</p>

<p>Here's what's sent:</p>

<pre><code class="language-JSON">  &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890#delete&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;Delete&amp;quot;,
  &amp;quot;actor&amp;quot;: &amp;quot;https://mastodon.social/users/Edent&amp;quot;,
  &amp;quot;object&amp;quot;: {
    &amp;quot;id&amp;quot;: &amp;quot;https://mastodon.social/users/Edent/statuses/1234567890&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;Tombstone&amp;quot;,
...
</code></pre>

<p>Again, the Activity has its own, unique, ID. But it is <em>the original ID</em> of the Note which is to be deleted!</p>

<p>The <a href="https://www.w3.org/TR/activitypub/#obj-id">spec says that all IDs must be URls</a> - but it doesn't say what format they should be in. Mastodon helpfully makes the Activity's ID somewhat related to the object's ID - but not all software will do that.</p>

<p>So, if you're doing something like saving messages to disk or a database, use the <em>object</em> ID as the canonical reference. The ID of the Activity isn't particularly important when it comes to receiving updates, deletes, replies, or anything else.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=49894&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/03/how-updates-work-in-activitypub-mastodon/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[A simple(ish) guide to verifying HTTP Message Signatures in PHP]]></title>
		<link>https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/</link>
					<comments>https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 27 Feb 2024 12:34:04 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[cryptography]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[security]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=49733</guid>

					<description><![CDATA[Mastodon makes heavy use of HTTP Message Signatures. They&#039;re a newish almost-standard which allows a server to verify that a request made to it came from the person who sent it.  This is a quick example to show how to verify these signatures using PHP. I don&#039;t claim that it covers every use-case, and it is no-doubt missing some weird edge cases. But it successfully verifies messages sent by…]]></description>
										<content:encoded><![CDATA[<p>Mastodon makes heavy use of <a href="https://datatracker.ietf.org/doc/rfc9421/">HTTP Message Signatures</a>. They're a newish almost-standard which allows a server to verify that a request made to it came from the person who sent it.</p>

<p>This is a quick example to show how to verify these signatures using PHP. I don't claim that it covers every use-case, and it is no-doubt missing some weird edge cases. But it successfully verifies messages sent by multiple Fediverse servers.</p>

<p>Let's step through it with an example of a message sent from Mastodon to my server.</p>

<h2 id="headers"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#headers">Headers</a></h2>

<p>The HTTP request starts with these headers:</p>

<pre><code class="language-_">User-Agent:  http.rb/5.1.1 (Mastodon/4.3.0-nightly.2024-02-23; +https://mastodon.social/)
Host:  example.com
Date:  Sun, 25 Feb 2024 10:48:22 GMT
Accept-Encoding:  gzip
Digest:  SHA-256=Hqu/6MR2imi8DTzbNp5PNEAFSyk0poN7+x5F+Z4vZMg=
Content-Type:  application/activity+json
Signature:  keyId="https://mastodon.social/users/Edent#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="P07V5I2zflR8FRsDMHshHmhgOwSkjWevujEbOyKMwjycrdVXjTD0ACiLuc5lTqDEXZ/...4eg=="
Connection:  Keep-Alive
Content-Length:  2857
</code></pre>

<p>Some of those you may be familiar with, some not.  The first thing we'll do is a sanity check; was this message sent recently? Because clocks drift in and out of synchronisation, we'll check if the message was within ±30 seconds.</p>

<pre><code class="language-php">$headers = getallheaders();
if ( !isset( $headers["Date"] ) ) { return null; }  //  No date set
$dateHeader = $headers["Date"];
$headerDatetime  = DateTime::createFromFormat('D, d M Y H:i:s T', $dateHeader);
$currentDatetime = new DateTime();

// Calculate the time difference in seconds
$timeDifference = abs( $currentDatetime-&gt;getTimestamp() - $headerDatetime-&gt;getTimestamp() );
return ( $timeDifference &lt; 30 );
</code></pre>

<p>That was easy! On to the next bit.</p>

<h2 id="digest"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#digest">Digest</a></h2>

<p>A message posted to the server usually has a body. In this case it is a long string of JSON data.  To ensure the message hasn't been altered in transit, one of the headers is:</p>

<pre><code class="language-_">Digest:  SHA-256=Hqu/6MR2imi8DTzbNp5PNEAFSyk0poN7+x5F+Z4vZMg=
</code></pre>

<p>That says, if you do a SHA-256 hash of the JSON you received, and convert that hash to Base64, it should match the digest in the header.</p>

<pre><code class="language-php">$digestString = $headers["Digest"];
//  Usually in the form `SHA-256=Hqu/6MR2imi8DTzbNp5PNEAFSyk0poN7+x5F+Z4vZMg=`
//  The Base64 encoding may have multiple `=` at the end. So split this at the first `=`
$digestData = explode( "=", $digestString, 2 );
$digestAlgorithm = $digestData[0];
$digestHash = $digestData[1];

//  There might be many different hashing algorithms
//  TODO: Find a way to transform these automatically
if ( "SHA-256" == $digestAlgorithm ) {
    $digestAlgorithm = "sha256";
} else if ( "SHA-512" == $digestAlgorithm ) {
    $digestAlgorithm = "sha512";
}

$json = file_get_contents( "php://input" );

//  Manually calculate the digest based on the data sent
$digestCalculated = base64_encode( hash( $digestAlgorithm, $json, true ) );

return ( $digestCalculated == $digestHash );
</code></pre>

<p>But, of course, if someone has manipulated the JSON, they may also have manipulated the digest. So it is time to look at the signature.</p>

<h2 id="the-signature"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#the-signature">The Signature</a></h2>

<p>Let's take a look at the signature header:</p>

<pre><code class="language-_">Signature:
  keyId="https://mastodon.social/users/Edent#main-key",
  algorithm="rsa-sha256",
  headers="(request-target) host date digest content-type",
  signature="P07V5I2zflR8FRsDMHshHmhgOwSkjWevujEbOyKMwjycrdVXjTD0ACiLuc5lTqDEXZ/...4eg=="
</code></pre>

<p>This contains 4 pieces of information.</p>

<ol>
<li><code>keyID</code> - a link to the user's public key.</li>
<li><code>algorithm</code> - the algorithm used by this signature.</li>
<li><code>headers</code> - the headers which make up the string to be signed.</li>
<li><code>signature</code> - the signature string.</li>
</ol>

<p>Let's split them up so they can be used:</p>

<pre><code class="language-php">//  Examine the signature
$signatureHeader = $headers["Signature"];

// Extract key information from the Signature header
$signatureParts = [];
//  Converts 'a=b,c=d e f' into ["a"=&gt;"b", "c"=&gt;"d e f"]
               // word="text"
preg_match_all('/(\w+)="([^"]+)"/', $signatureHeader, $matches);
foreach ($matches[1] as $index =&gt; $key) {
    $signatureParts[$key] = $matches[2][$index];
}
</code></pre>

<p>Let's tackle each part in order.</p>

<h3 id="get-the-users-public-key"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#get-the-users-public-key">Get the user's public key</a></h3>

<p>You might think you can just get <code>https://mastodon.social/users/Edent#main-key</code> - but you would be wrong.</p>

<p>Firstly, you need to tell the key server that you want the JSON representation of the URl - otherwise you'll end up with HTML.</p>

<pre><code class="language-php">$publicKeyURL = $signatureParts["keyId"];
$context   = stream_context_create(
    [ "http" =&gt; [ "header" =&gt; "Accept: application/activity+json" ] ] 
);
$userJSON  = file_get_contents( $publicKeyURL, false, $context );
</code></pre>

<p>That gets you the JSON representation of the user.  On Mastodon, the key can be found at:
<img src="https://shkspr.mobi/blog/wp-content/uploads/2024/02/jsonkey.pnh-fs8.png" alt="Screenshot of JSON. As described in text." width="938" height="355" class="aligncenter size-full wp-image-49734"></p>

<p>I don't know how to automatically find the key, so I've hard-coded its location.</p>

<pre><code class="language-php">$userData  = json_decode( $userJSON, true );
$publicKey = $userData["publicKey"]["publicKeyPem"];
</code></pre>

<h3 id="get-the-algorithm"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#get-the-algorithm">Get the algorithm</a></h3>

<p>This is rather straightforward. It's just the text in the signature header:</p>

<pre><code class="language-php">$algorithm = $signatureParts["algorithm"];
</code></pre>

<h3 id="reconstruct-the-signing-header"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#reconstruct-the-signing-header">Reconstruct the signing header</a></h3>

<p>Let's take a look at the third piece of the puzzle:</p>

<p><code>headers="(request-target) host date digest content-type"</code></p>

<p>This says "The signature is based on the following parts in order". So we only care about the headers which make up the request, the host, the date, the digest, and the content type. Other servers may require different parts of the headers.</p>

<p>Again, let's tackle them in order.</p>

<h4 id="request-target"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#request-target"><code>(request-target)</code></a></h4>

<p>This means the method of the request and the target it was sent to.  In our example, this is a <code>POST</code> sent to the path <code>/inbox</code>.</p>

<h4 id="host"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#host"><code>host</code></a></h4>

<p>This is the HTTP host the message was sent to.  This should be retrieved from the server, not taken from the sent headers.</p>

<h4 id="date-digest-content-type"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#date-digest-content-type"><code>date digest content-type</code></a></h4>

<p>These are the values from the headers which were sent with the request.</p>

<h4 id="putting-it-all-together"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#putting-it-all-together">Putting it all together</a></h4>

<p>Annoyingly, the HTTP headers are written in Title-Case whereas the headers in the Signature are in lower-case. So some conversion is necessary:</p>

<pre><code class="language-php">//  Manually reconstruct the header string
$signatureHeaders = explode(" ", $signatureParts["headers"] );
$signatureString = "";
foreach ($signatureHeaders as $signatureHeader) {
    if ( "(request-target)" == $signatureHeader ) {
        $method = strtolower( $_SERVER["REQUEST_METHOD"] );
        $target = strtolower( $_SERVER["REQUEST_URI"] );
        $signatureString .= "(request-target): {$method} {$target}\n";
    } else if ( "host" == $signatureHeader ) {
        $host = strtolower( $_SERVER["HTTP_HOST"] );    
        $signatureString .= "host: {$host}\n";
    } else {
        //  In the HTTP header, the keys use Title Case
        $signatureString .= "{$signatureHeader}: " . $headers[ ucwords( $signatureHeader, "-" ) ] . "\n";
    }
}

//  Remove trailing newline
$signatureString = trim( $signatureString );
</code></pre>

<p>This results in a string like this:</p>

<pre><code class="language-_">(request-target): post /inbox
host: example.com
date: Sun, 25 Feb 2024 10:48:22 GMT
digest: SHA-256=Hqu/6MR2imi8DTzbNp5PNEAFSyk0poN7+x5F+Z4vZMg=
content-type: application/activity+json
</code></pre>

<h2 id="get-the-signature"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#get-the-signature">Get the signature</a></h2>

<p>The signature that we are sent is in Base64.</p>

<p><code>signature="P07V5I2zflR8FRsDMHshHmhgOwSkjWevujEbOyKMwjycrdVXjTD0ACiLuc5lTqDEXZ/...4eg=="</code></p>

<p>It needs to be decoded before we can use it.</p>

<pre><code class="language-php">$signature = base64_decode( $signatureParts["signature"] );
</code></pre>

<h2 id="verify-the-signature"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#verify-the-signature">Verify the signature</a></h2>

<p>We're nearly there!  Luckily, we don't have to do any crazy cryptography by hand. We use PHP's <a href="https://www.php.net/manual/en/function.openssl-verify"><code>openssl_verify()</code></a>:</p>

<pre><code class="language-php">//  Finally! Calculate whether the signature is valid
$verified = openssl_verify(
    $signatureString, 
    $signature, 
    $publicKey, 
    $algorithm
);
</code></pre>

<p>That takes the reconstructed string based on the headers, the signature which was sent, the public key we retrieved, and the algorithm.</p>

<p>If it all matches, it will return <code>true</code>.  If not... time for some debugging!</p>

<h2 id="but-what-about"><a href="https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/#but-what-about">But what about...?</a></h2>

<p>This is <em>not</em> a complete solution. My code almost certainly contains bugs, unforeseen edge-cases, memory leaks, black holes, and poisonous frogs.  This is intended to step you through the practical process of verifying an HTTP Message Signature.</p>

<p>Then you should get a properly tested and validated library and use that instead.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=49733&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/02/a-simpleish-guide-to-verifying-http-message-signatures-in-php/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[ActivityPub Server in a Single PHP File]]></title>
		<link>https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/</link>
					<comments>https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 18 Feb 2024 12:34:48 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=49641</guid>

					<description><![CDATA[Any computer program can be designed to run from a single file if you architect it wrong enough!  I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.  The design goals were:   Upload a single PHP file to the server. No databases or separate config files. Single Actor (i.e. not multi-user). Allow the Actor to…]]></description>
										<content:encoded><![CDATA[<p>Any computer program can be designed to run from a single file if you architect it wrong enough!</p>

<p>I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.</p>

<p>The design goals were:</p>

<ul>
<li>Upload a single PHP file to the server.</li>
<li>No databases or separate config files.</li>
<li>Single Actor (i.e. not multi-user).</li>
<li>Allow the Actor to be followed.</li>
<li>Post plain-text messages to followers.</li>
<li>Be <em>roughly</em> standards compliant.</li>
</ul>

<p>And those goals have all been met! <a href="https://gitlab.com/edent/activitypub-single-php-file/">Check it out on GitLab</a>. I warn you though, it is the <em>nadir</em> of bad coding. There are no tests, bugger-all security, scalability isn't considered, and it is a mess. But it <em>works</em>.</p>

<p>You can follow the test user <code>@example@example.viii.fi</code></p>

<h2 id="architecture"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#architecture">Architecture</a></h2>

<p>Firstly, I've slightly cheated on my "single file" stipulation. There's an <code>.htaccess</code> file which turns <code>example.com/whatever</code> into <code>example.com/index.php?path=whatever</code></p>

<p>The <code>index.php</code> file then takes that path and does <em>stuff</em>. It also contains all the configuration variables which is <strong>very bad</strong> practice.</p>

<p>Rather than using a database, it saves files to disk.</p>

<p>Again, this is <strong>not suitable</strong> for any real world use. This is an educational tool to help explain the basics of posting messages to the Fediverse.  It requires absolutely no dependencies. You do not need to spin up a dockerised hypervisor to manage your node bundles and re-compile everything to WASM. Just FTP the file up to prod and you're done.</p>

<h2 id="walkthrough"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#walkthrough">Walkthrough</a></h2>

<p>This is a quick ramble through the code. It is reasonably well documented, I hope.</p>

<h3 id="preamble"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#preamble">Preamble</a></h3>

<p>This is where you set up your account's name and bio. You also need to provide a public/private keypair. The posting page is protected with a password that also needs to be set here.</p>

<pre><code class="language-php">    //  Set up the Actor's information
    $username = rawurlencode("example");    //  Encoded as it is often used as part of a URl
    $realName = "E. Xample. Jr.";
    $summary  = "Some text about the user.";
    $server   = $_SERVER["SERVER_NAME"];    //  Domain name this is hosted on

    //  Generate locally or from https://cryptotools.net/rsagen
    //  Newlines must be replaced with "\n"
    $key_private = "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----";
    $key_public  = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----";

    //  Password for sending messages
    $password = "P4ssW0rd";
</code></pre>

<h3 id="logging"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#logging">Logging</a></h3>

<p>ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in <code>/logs/</code> as a datestamped text file.</p>

<pre><code class="language-php">    // Get all headers and requests sent to this server
    $headers     = print_r( getallheaders(), true );
    $postData    = print_r( $_POST,    true );
    $getData     = print_r( $_GET,     true );
    $filesData   = print_r( $_FILES,   true );
    $body        = json_decode( file_get_contents( "php://input" ), true );
    $bodyData    = print_r( $body,    true );
    $requestData = print_r( $_REQUEST, true );
    $serverData  = print_r( $_SERVER,  true );

    //  Get the type of request - used in the log filename
    if ( isset( $body["type"] ) ) {
        $type = " " . $body["type"];
    } else {
        $type = "";
    }

    //  Create a timestamp in ISO 8601 format for the filename
    $timestamp = date( "c" );
    //  Filename for the log
    $filename  = "{$timestamp}{$type}.txt";

    //  Save headers and request data to the timestamped file in the logs directory
    if( ! is_dir( "logs" ) ) { mkdir( "logs"); }

    file_put_contents( "logs/{$filename}", 
        "Headers:     \n$headers    \n\n" .
        "Body Data:   \n$bodyData   \n\n" .
        "POST Data:   \n$postData   \n\n" .
        "GET Data:    \n$getData    \n\n" .
        "Files Data:  \n$filesData  \n\n" .
        "Request Data:\n$requestData\n\n" .
        "Server Data: \n$serverData \n\n"
    );
</code></pre>

<h3 id="routing"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#routing">Routing</a></h3>

<p>The <code>.htaccess</code> changes <code>/whatever</code> to <code>/?path=whatever</code>
This runs the function of the path requested.</p>

<pre><code class="language-php">    !empty( $_GET["path"] )  ? $path = $_GET["path"] : die();
    switch ($path) {
        case ".well-known/webfinger":
            webfinger();
        case rawurldecode( $username ):
            username();
        case "following":
            following();
        case "followers":
            followers();
        case "inbox":
            inbox();
        case "write":
            write();
        case "send":
            send();
        default:
            die();
    }
</code></pre>

<h3 id="webfinger"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#webfinger">WebFinger</a></h3>

<p>The <a href="https://docs.joinmastodon.org/spec/webfinger/">WebFinger Protocol</a> is used to identify accounts.
It is requested with <code>example.com/.well-known/webfinger?resource=acct:username@example.com</code>
This server only has one user, so it ignores the query string and always returns the same details.</p>

<pre><code class="language-php">    function webfinger() {
        global $username, $server;

        $webfinger = array(
            "subject" =&gt; "acct:{$username}@{$server}",
              "links" =&gt; array(
                array(
                     "rel" =&gt; "self",
                    "type" =&gt; "application/activity+json",
                    "href" =&gt; "https://{$server}/{$username}"
                )
            )
        );
        header( "Content-Type: application/json" );
        echo json_encode( $webfinger );
        die();
    }
</code></pre>

<h3 id="username"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#username">Username</a></h3>

<p>Requesting <code>example.com/username</code> returns a JSON document with the user's information.</p>

<pre><code class="language-php">    function username() {
        global $username, $realName, $summary, $server, $key_public;

        $user = array(
            "@context" =&gt; [
                "https://www.w3.org/ns/activitystreams",
                "https://w3id.org/security/v1"
            ],
                                   "id" =&gt; "https://{$server}/{$username}",
                                 "type" =&gt; "Person",
                            "following" =&gt; "https://{$server}/following",
                            "followers" =&gt; "https://{$server}/followers",
                                "inbox" =&gt; "https://{$server}/inbox",
                    "preferredUsername" =&gt;  rawurldecode($username),
                                 "name" =&gt; "{$realName}",
                              "summary" =&gt; "{$summary}",
                                  "url" =&gt; "https://{$server}",
            "manuallyApprovesFollowers" =&gt;  true,
                         "discoverable" =&gt;  true,
                            "published" =&gt; "2024-02-12T11:51:00Z",
            "icon" =&gt; [
                     "type" =&gt; "Image",
                "mediaType" =&gt; "image/png",
                      "url" =&gt; "https://{$server}/icon.png"
            ],
            "publicKey" =&gt; [
                "id"           =&gt; "https://{$server}/{$username}#main-key",
                "owner"        =&gt; "https://{$server}/{$username}",
                "publicKeyPem" =&gt; $key_public
            ]
        );
        header( "Content-Type: application/activity+json" );
        echo json_encode( $user );
        die();
    }
</code></pre>

<h3 id="following-followers"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#following-followers">Following &amp; Followers</a></h3>

<p>These JSON documents show how many users are following / followers-of this account.
The information here is self-attested. So you can lie and use any number you want.</p>

<pre><code class="language-php">function following() {
        global $server;

        $following = array(
              "@context" =&gt; "https://www.w3.org/ns/activitystreams",
                    "id" =&gt; "https://{$server}/following",
                  "type" =&gt; "Collection",
            "totalItems" =&gt; 0,
                 "items" =&gt; []
        );
        header( "Content-Type: application/activity+json" );
        echo json_encode( $following );
        die();
    }
    function followers() {
        global $server;
        $followers = array(
              "@context" =&gt; "https://www.w3.org/ns/activitystreams",
                    "id" =&gt; "https://{$server}/followers",
                  "type" =&gt; "Collection",
            "totalItems" =&gt; 0,
                 "items" =&gt; []
        );
        header( "Content-Type: application/activity+json" );
        echo json_encode( $followers );
        die();
    }
</code></pre>

<h3 id="inbox"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#inbox">Inbox</a></h3>

<p>The <code>/inbox</code> is the main server. It receives all requests. This server only responds to "Follow" requests.
A remote server sends a follow request which is a JSON file saying who they are.
This code does not cryptographically validate the headers of the received message.
The name of the remote user's server is saved to a file so that future messages can be delivered to it.
An accept request is cryptographically signed and POST'd back to the remote server.</p>

<pre><code class="language-php">    function inbox() {
        global $body, $server, $username, $key_private;

        //  Get the message and type
        $inbox_message = $body;
        $inbox_type = $inbox_message["type"];

        //  This inbox only responds to follow requests
        if ( "Follow" != $inbox_type ) { die(); }

        //  Get the parameters
        $inbox_id    = $inbox_message["id"];
        $inbox_actor = $inbox_message["actor"];
        $inbox_host  = parse_url( $inbox_actor, PHP_URL_HOST );

        //  Does this account have any followers?
        if( file_exists( "followers.json" ) ) {
            $followers_file = file_get_contents( "followers.json" );
            $followers_json = json_decode( $followers_file, true );
        } else {
            $followers_json = array();
        }

        //  Add user to list. Don't care about duplicate users, server is what's important
        $followers_json[$inbox_host]["users"][] = $inbox_actor;

        //  Save the new followers file
        file_put_contents( "followers.json", print_r( json_encode( $followers_json ), true ) );

        //  Response Message ID
        //  This isn't used for anything important so could just be a random number
        $guid = uuid();

        //  Create the Accept message
        $message = [
            "@context" =&gt; "https://www.w3.org/ns/activitystreams",
            "id"       =&gt; "https://{$server}/{$guid}",
            "type"     =&gt; "Accept",
            "actor"    =&gt; "https://{$server}/{$username}",
            "object"   =&gt; [
                "@context" =&gt; "https://www.w3.org/ns/activitystreams",
                "id"       =&gt;  $inbox_id,
                "type"     =&gt;  $inbox_type,
                "actor"    =&gt;  $inbox_actor,
                "object"   =&gt; "https://{$server}/{$username}",
            ]
        ];

        //  The Accept is sent to the server of the user who requested the follow
        //  TODO: The path doesn't *always* end with/inbox
        $host = $inbox_host;
        $path = parse_url( $inbox_actor, PHP_URL_PATH ) . "/inbox";

        //  Get the signed headers
        $headers = generate_signed_headers( $message, $host, $path );

        //  Specify the URL of the remote server's inbox
        //  TODO: The path doesn't *always* end with /inbox
        $remoteServerUrl = $inbox_actor . "/inbox";

        //  POST the message and header to the requester's inbox
        $ch = curl_init( $remoteServerUrl );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
        curl_setopt( $ch, CURLOPT_POSTFIELDS,     json_encode($message) );
        curl_setopt( $ch, CURLOPT_HTTPHEADER,     $headers );
        $response = curl_exec( $ch );

        //  Check for errors
        if( curl_errno( $ch ) ) {
            file_put_contents( "error.txt",  curl_error( $ch ) );
        }
        curl_close($ch);
        die();
    }
</code></pre>

<h3 id="uuid"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#uuid">UUID</a></h3>

<p>Every message sent should have a unique ID. 
This can be anything you like. Some servers use a random number.
I prefer a date-sortable string.</p>

<pre><code class="language-php">    function uuid() {
        return sprintf( "%08x-%04x-%04x-%04x-%012x",
            time(),
            mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffffffffffff)
        );
    }
</code></pre>

<h3 id="signing-headers"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#signing-headers">Signing Headers</a></h3>

<p>Every message that your server sends needs to be cryptographically signed with your Private Key.
This is a complicated process. Please read "<a href="https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/">How to make friends and verify requests</a>" for more information.</p>

<pre><code class="language-php">    function generate_signed_headers( $message, $host, $path ) {
        global $server, $username, $key_private;

        //  Encode the message to JSON
        $message_json = json_encode( $message );

        //  Location of the Public Key
        $keyId = "https://{$server}/{$username}#main-key";

        //  Generate signing variables
        $hash   = hash( "sha256", $message_json, true );
        $digest = base64_encode( $hash );
        $date   = date( "D, d M Y H:i:s \G\M\T" );

        //  Get the Private Key
        $signer = openssl_get_privatekey( $key_private );

        //  Sign the path, host, date, and digest
        $stringToSign = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest";

        //  The signing function returns the variable $signature
        //  https://www.php.net/manual/en/function.openssl-sign.php
        openssl_sign(
            $stringToSign, 
            $signature, 
            $signer, 
            OPENSSL_ALGO_SHA256
        );
        //  Encode the signature
        $signature_b64 = base64_encode( $signature );

        //  Full signature header
        $signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';

        //  Header for POST reply
        $headers = array(
                    "Host: {$host}",
                    "Date: {$date}",
                  "Digest: SHA-256={$digest}",
               "Signature: {$signature_header}",
            "Content-Type: application/activity+json",
                  "Accept: application/activity+json",
        );

        return $headers;
    }
</code></pre>

<h3 id="user-interface-for-writing"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#user-interface-for-writing">User Interface for Writing</a></h3>

<p>This creates a basic HTML form. Type in your message and your password. It then POSTs the data to the <code>/send</code> endpoint.</p>

<pre><code class="language-php">    function write() {
        //  Display an HTML form for the user to enter a message.
echo &lt;&lt;&lt; HTML
&lt;!DOCTYPE html&gt;
&lt;html lang="en-GB"&gt;
    &lt;head&gt;
        &lt;meta charset="UTF-8"&gt;
        &lt;title&gt;Send Message&lt;/title&gt;
        &lt;style&gt;
            *{font-family:sans-serif;font-size:1.1em;}
        &lt;/style&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;form action="/send" method="post" enctype="multipart/form-data"&gt;
            &lt;label   for="content"&gt;Your message:&lt;/label&gt;&lt;br&gt;
            &lt;textarea id="content" name="content" rows="5" cols="32"&gt;&lt;/textarea&gt;&lt;br&gt;
            &lt;label   for="password"&gt;Password&lt;/label&gt;&lt;br&gt;
            &lt;input  type="password" name="password" id="password" size="32"&gt;&lt;br&gt;
            &lt;input  type="submit"  value="Post Message"&gt; 
        &lt;/form&gt;
    &lt;/body&gt;
&lt;/html&gt;
HTML;
        die();
    }
</code></pre>

<h3 id="send-endpoint"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#send-endpoint">Send Endpoint</a></h3>

<p>This takes the submitted message and checks the password is correct.
It reads the <code>followers.json</code> file and sends the message to every server that is following this account.</p>

<pre><code class="language-php">    function send() {
        global $password, $server, $username, $key_private;

        //  Does the posted password match the stored password?
        if( $password != $_POST["password"] ) { die(); }

        //  Get the posted content
        $content = $_POST["content"];

        //  Current time - ISO8601
        $timestamp = date( "c" );

        //  Outgoing Message ID
        $guid = uuid();

        //  Construct the Note
        //  contentMap is used to prevent unnecessary "translate this post" pop ups
        // hardcoded to English
        $note = [
            "@context"     =&gt; array(
                "https://www.w3.org/ns/activitystreams"
            ),
            "id"           =&gt; "https://{$server}/posts/{$guid}.json",
            "type"         =&gt; "Note",
            "published"    =&gt; $timestamp,
            "attributedTo" =&gt; "https://{$server}/{$username}",
            "content"      =&gt; $content,
            "contentMap"   =&gt; ["en" =&gt; $content],
            "to"           =&gt; ["https://www.w3.org/ns/activitystreams#Public"]
        ];

        //  Construct the Message
        $message = [
            "@context" =&gt; "https://www.w3.org/ns/activitystreams",
            "id"       =&gt; "https://{$server}/posts/{$guid}.json",
            "type"     =&gt; "Create",
            "actor"    =&gt; "https://{$server}/{$username}",
            "to"       =&gt; [
                "https://www.w3.org/ns/activitystreams#Public"
            ],
            "cc"       =&gt; [
                "https://{$server}/followers"
            ],
            "object"   =&gt; $note
        ];

        //  Create the context for the permalink
        $note = [ "@context" =&gt; "https://www.w3.org/ns/activitystreams", ...$note ];

        //  Save the permalink
        $note_json = json_encode( $note );
        //  Check for posts/ directory and create it
        if( ! is_dir( "posts" ) ) { mkdir( "posts"); }
        file_put_contents( "posts/{$guid}.json", print_r( $note_json, true ) );

        //  Read existing users and get their hosts
        $followers_file = file_get_contents( "followers.json" );
        $followers_json = json_decode( $followers_file, true );     
        $hosts = array_keys( $followers_json );

        //  Prepare to use the multiple cURL handle
        $mh = curl_multi_init();

        //  Loop through all the severs of the followers
        //  Each server needs its own cURL handle
        //  Each POST to an inbox needs to be signed separately
        foreach ( $hosts as $host ) {
            $path = "/inbox";

            //  Get the signed headers
            $headers = generate_signed_headers( $message, $host, $path );

            // Specify the URL of the remote server
            $remoteServerUrl = "https://{$host}{$path}";

            //  POST the message and header to the requester's inbox
            $ch = curl_init( $remoteServerUrl );

            curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
            curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
            curl_setopt( $ch, CURLOPT_POSTFIELDS,     json_encode($message) );
            curl_setopt( $ch, CURLOPT_HTTPHEADER,     $headers );

            //  Add the handle to the multi-handle
            curl_multi_add_handle( $mh, $ch );
        }

        //  Execute the multi-handle
        do {
            $status = curl_multi_exec( $mh, $active );
            if ( $active ) {
                curl_multi_select( $mh );
            }
        } while ( $active &amp;&amp; $status == CURLM_OK );

        //  Close the multi-handle
        curl_multi_close( $mh );

        //  Render the JSON so the user can see the POST has worked
        header( "Location: https://{$server}/posts/{$guid}.json" );
        die();
    }
</code></pre>

<h2 id="next-steps"><a href="https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/#next-steps">Next Steps</a></h2>

<p>This is <em>not</em> intended to be used in production. <strong>Ever</strong>.  But if you would like to contribute more simple examples of how the protocol works, please <a href="https://gitlab.com/edent/activitypub-single-php-file/">come and play on GitLab</a>.</p>

<p>You can follow the test user <code>@example@example.viii.fi</code></p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=49641&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/feed/</wfw:commentRss>
			<slash:comments>10</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Internationalise The Fediverse]]></title>
		<link>https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/</link>
					<comments>https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sat, 17 Feb 2024 12:34:58 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[unicode]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=49643</guid>

					<description><![CDATA[We live in the future now. It is OK to use Unicode everywhere.  It seems bizarre to me that modern Internet services sometimes &#34;forget&#34; that there&#039;s a world outside the Anglosphere. Some people have the temerity to speak foreign languages! And some of those languages have accents on their letters!! Even worse, some don&#039;t use English letters at all!!!  A decade ago, I was miffed that GitHub only…]]></description>
										<content:encoded><![CDATA[<p>We live in the future now. It is OK to use Unicode everywhere.</p>

<p>It seems bizarre to me that modern Internet services sometimes "forget" that there's a world outside the Anglosphere. Some people have the temerity to speak <em>foreign</em> languages! And some of those languages have accents on their letters!! Even worse, some don't use English letters <em>at all!!!</em></p>

<p>A decade ago, I was miffed that <a href="https://shkspr.mobi/blog/2013/06/is-github-racist/">GitHub only supported some ASCII characters</a> in its project names. There's no <em>technical</em> reason why your repo can't be called "ഹലോ വേൾഡ്".</p>

<p>Similarly, I'm frustrated that Mastodon (the largest ActivityPub service) <a href="https://github.com/mastodon/mastodon/issues/8417">doesn't allow Unicode usernames</a> and has <a href="https://jam.xwx.moe/notice/AdXsJF6Q5oYHJBEAiG">resisted efforts to change</a>.</p>

<p>So I built a small ActivityPub server which publishes content from an Actor called <a href="https://i18n.viii.fi/.well-known/webfinger"><code>@你好@i18n.viii.fi</code></a> - it is only a demo account, but it works!</p>

<p>Some ActivityPub clients report that they are able to follow it and receive messages from it. Others - like Mastodon - simply can't see anything from it.  Take a look <a href="https://mastodon.social/@Edent/111920759100955860">at the replies on Mastodon</a> to see which services work.  You can also <a href="https://fed.xnor.in/users/$Aet3ViWYORXdinGChM">see some of its posts on the Fediverse</a>.</p>

<h2 id="what-does-the-fox-spec-say"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#what-does-the-fox-spec-say">What Does The <del>Fox</del> Spec Say?</a></h2>

<p>The ActivityPub specification says:</p>

<blockquote><p>Building an international base of users is important in a federated network.
<a href="https://www.w3.org/TR/activitypub/#i18n-concerns">Internationalization</a></p></blockquote>

<p>I can't find anything in the specifications which limits what languages a username can be written in. But there are a few clues scattered about.</p>

<p>The user's <code>@</code> name is defined by <code>preferredUsername</code> which is:</p>

<blockquote><p>A short username which may be used to refer to the actor, with no uniqueness guarantees. 
<a href="https://www.w3.org/TR/activitypub/#preferredUsername">4.1 Actor objects</a></p></blockquote>

<p>There's nothing in there about what scripts it can contain. However, later on, the spec says:</p>

<blockquote><p>Properties containing natural language values, such as <code>name</code>, <code>preferredUsername</code>, or <code>summary</code>, make use of <a href="https://www.w3.org/TR/activitystreams-core/#naturalLanguageValues">natural language support defined in ActivityStreams</a>.
<a href="https://www.w3.org/TR/activitypub/#h-note-2">4. Actors</a></p></blockquote>

<p>So it is expected that a preferred username could be written in multiple scripts. Which implies that the default need not be limited to A-Z0-9.</p>

<p>The <a href="https://www.w3.org/TR/activitystreams-core/#marking-up-language">ActivityStreams specification talks about language mapping</a>.</p>

<p>Finally, the <a href="https://www.w3.org/TR/activitypub/#liked-property">ActivityPub specification has some examples on non-Latin text</a> in names.</p>

<p>So, I think that it is acceptable for usernames to be written in a variety of non-Latin scripts.</p>

<h2 id="but-what-about"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#but-what-about">But What About...?</a></h2>

<p>There are usually a few objections to "Unicode Everywhere" zealots like me. I'd like to forestall any arguments.</p>

<h3 id="what-about-homograph-attacks"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#what-about-homograph-attacks">What about homograph attacks?</a></h3>

<p>Well, what about them? ASCII has plenty of similar looking characters. I doubt most people would notice when a capital i is replaced by a lower L - and vice-versa. Similarly the kerning issue of an r and n looking like an m is well known. Are mixed language homographs more dangerous? I don't think so.</p>

<h3 id="what-if-people-make-names-that-cant-be-typed"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#what-if-people-make-names-that-cant-be-typed">What if people make names that can't be typed?</a></h3>

<p>Well, what if they do? Maybe not being found by people who can't type your language is a feature, not a bug.  But, anyway, clients can let users search for other people, or copy and paste their names.</p>

<h3 id="what-about-weird-zalgo-text"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#what-about-weird-zalgo-text">What about weird "Zalgo" text?</a></h3>

<p>It is up to a client to decide how they want to render text input. The "problems" of strange Unicode combinations are well known. This is not a hard computer-science problem.</p>

<h3 id="what-about-bi-directional-text"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#what-about-bi-directional-text">What about bi-directional text?</a></h3>

<p><a href="https://www.w3.org/TR/activitystreams-core/#h-biditext">The spec makes clear this is allowed</a>.</p>

<h3 id="do-people-even-want-a-username-in-their-own-script"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#do-people-even-want-a-username-in-their-own-script">Do people even want a username in their own script?</a></h3>

<p>I have no evidence for this. But I bet you'd get pretty frustrated if you had to switch keyboard just to type your own name, wouldn't you? In any case, why can't I have a username of <code>@😉</code></p>

<h2 id="whats-next"><a href="https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/#whats-next">What's Next?</a></h2>

<p>If you build ActivityPub software, give some thought to the billions of people who don't have names which easily fit into ASCII.</p>

<p>If your software can see <a href="https://i18n.viii.fi/.well-known/webfinger"><code>@你好@i18n.viii.fi</code></a> and its posts, please let me know.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=49643&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/02/internationalise-the-fediverse/feed/</wfw:commentRss>
			<slash:comments>36</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[A (tiny, incomplete, single user, write-only) ActivityPub server in PHP]]></title>
		<link>https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/</link>
					<comments>https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sat, 03 Feb 2024 12:34:51 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[Symfony]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=49490</guid>

					<description><![CDATA[I&#039;ve written an ActivityPub server which only allows you to post messages to your followers.  That&#039;s all it does.  It won&#039;t record favourites or reposts. There&#039;s no support for following other accounts or receiving replies.  It cannot delete or update posts nor can it verify signatures. It doesn&#039;t have a database or any storage beyond flat files.  But it will happily send messages and allow…]]></description>
										<content:encoded><![CDATA[<p>I've written <a href="https://github.com/edent/location-activitypub-symfony/">an ActivityPub server which <em>only</em> allows you to post messages to your followers</a>.  That's all it does.  It won't record favourites or reposts. There's no support for following other accounts or receiving replies.  It cannot delete or update posts nor can it verify signatures. It doesn't have a database or any storage beyond flat files.</p>

<p>But it will happily send messages and allow itself to be followed.</p>

<p>This shows that it is <em>totally</em> possible to broadcast fully-featured ActivityPub messages to the Fediverse with minimal coding skills and modest resources.</p>

<h2 id="why"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#why">Why</a></h2>

<p>I wanted to create <a href="https://shkspr.mobi/blog/2024/01/rebuilding-foursquare-for-activitypub-using-openstreetmap/">a service a bit like FourSquare</a>.  For this, I needed an ActivityPub server which allows posting geotagged locations to the Fediverse.</p>

<p>I didn't want to install a fully-featured server with lots of complex parts. So I (foolishly) decided to write my own. I had a lot of trouble with HTTP Signatures.  Because they are cursed and I cannot read documentation. But mostly the cursed thing.</p>

<h2 id="how"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#how">How</a></h2>

<p>Creating a minimum viable Mastodon instance can be done with <a href="https://justingarrison.com/blog/2022-12-06-mastodon-files-instance/">half a dozen static files</a>. That gets you an account that people can see. They can't follow it or receive any posts though.</p>

<p>I wanted to use PHP to build an interactive server. PHP is supported everywhere and is simple to deploy.  Luckily, <a href="https://rknight.me/blog/building-an-activitypub-server/">Robb Knight has written an excellent tutorial</a>, so I ripped off his code and rewrote it for Symfony.</p>

<p>The structure is relatively straightforward.</p>

<ul>
<li><code>/.well-known/webfinger</code> is a static file which gives information about where to find details of the account.</li>
<li><code>/[username]</code> is a static file which has the user's metadata, public key, and links to avatar images.</li>
<li><code>/following</code> and <code>/followers</code> are also static files which say how many users are being followed / are following.</li>
<li><code>/posts/[GUID]</code> a directory with JSON files saved to disk - each ones contains the published ActivityPub note.</li>
<li><code>/photos/</code> is a directory with any uploaded media in it.</li>
<li><code>/outbox</code> is a list of all the posts which have been published.</li>
<li><code>/inbox</code> is an <em>external</em> API endpoint. An ActivityPub server sends it a follow request, the endpoint then POSTs a cryptographically signed Accept message to the follower's inbox. The follower's inbox address is saved to disk.</li>
<li><code>/logs</code> is a listing of all the messages received by the inbox.</li>
<li><code>/new</code> is a password protected page which lets you write a message. This is then sent to...</li>
<li><code>/send</code> is an <em>internal</em> API endpoint. It constructs an ActivityPub note, with attached location metadata, and POSTs it to each follower's inbox with a cryptographic signature.</li>
</ul>

<p>That's it.</p>

<p>The front-end grabs my phone's geolocation and shows the 25 nearest places within 100 metres. One click and the page posts to the <code>/send</code> endpoint which then publishes a message saying I'm checked in.  It is also possible to attach to the post a short message and a single photo with alt text.</p>

<p>There's no database. Posts are saved as JSON documents. Images are uploaded to a directory. It is single-user, so there is no account management.</p>

<h2 id="what-works"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#what-works">What Works</a></h2>

<ul>
<li style="list-style-type: '✅ ';">Users can find the account.</li>
<li style="list-style-type: '✅ ';">Users can follow the account and receive updates.</li>
<li style="list-style-type: '✅ ';">Posts contain geotag metadata.</li>
<li style="list-style-type: '✅ ';">Posts contain a description of the place.</li>
<li style="list-style-type: '✅ ';">Posts contain an OSM link to the place.</li>
<li style="list-style-type: '✅ ';">Posts contain a custom message.</li>
<li style="list-style-type: '✅ ';">Posts autolink #Hashtags (sort of).</li>
<li style="list-style-type: '✅ ';">Posts can have an image attached to them.</li>
<li style="list-style-type: '✅ ';">Messages to the inbox are recorded (but not yet integrated).</li>
</ul>

<h2 id="todo"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#todo">ToDo</a></h2>

<ul>
<li>My account only has a few dozen followers, some of whom share the same sever. Even with cURL multi handle, it takes time to post to several servers.</li>
<li>It posts plain text. It doesn't autolink websites</li>
<li>Hashtags are linked when viewed remotely, but they don't go anywhere locally.</li>
<li>There's no language selection - it is hard-coded to English.</li>
<li>The outbox isn't paginated.</li>
<li>The UI looks crap - but it is only me using it.</li>
<li>There's only a basic front-page showing a map of all my check-ins.</li>
<li>Replies are logged, but there's no easy way to see them.</li>
<li>Doesn't show any metadata about the place being checked-in to. It could use the item's website (if any) or hashtags for the type of amenity it is.</li>
<li>No way to handle being unfollowed.</li>
<li>No way to remove servers which have died.</li>
<li>Probably lots more.</li>
</ul>

<h2 id="other-resources"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#other-resources">Other Resources</a></h2>

<p>I found these resources helpful while creating this project:</p>

<ul>
<li><a href="https://tinysubversions.com/notes/activitypub-tool/">MVP ActivityPub in Python on Glitch</a></li>
<li><a href="https://seb.jambor.dev/posts/understanding-activitypub/">Understanding ActivityPub Part 1: Protocol Fundamentals</a></li>
<li><a href="https://flak.tedunangst.com/post/activity-notes">Activity Notes</a></li>
<li><a href="https://docs.gotosocial.org/en/latest/federation/">Federating with GoToSocial</a></li>
</ul>

<h2 id="whats-next"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#whats-next">What's Next?</a></h2>

<p>I've <a href="https://github.com/mastodon/mastodon/issues/29002">raised an issue on Mastodon</a> to see if they can support showing locations in posts. Hopefully, one day, they'll allow adding locations and then I can shut this down.</p>

<p>The code needs tidying up - it is very much a scratch-my-own-itch development. Probably riddled with bugs and security holes.</p>

<p>World domination?</p>

<h2 id="where"><a href="https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/#where">Where</a></h2>

<p>You can <a href="https://github.com/edent/location-activitypub-symfony/">laugh at my code on GitHub</a>.</p>

<p>You can <a href="https://location.edent.tel/">look at my check-ins on a map</a>.</p>

<p>You can follow my location on the Fediverse at <code>@edent_location@location.edent.tel</code></p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=49490&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/02/a-tiny-incomplete-single-user-write-only-activitypub-server-in-php/feed/</wfw:commentRss>
			<slash:comments>10</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Seven Years On Mastodon]]></title>
		<link>https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/</link>
					<comments>https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 31 Oct 2023 12:34:32 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[twitter]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=46654</guid>

					<description><![CDATA[I remember seeing the original &#34;A new decentralized microblogging platform&#34; on HackerNews back in October 2016. A few weeks later, I joined - becoming the 7,112th user.  As the years went on, my use of it waxed and waned. I started cross-posting to both Mastodon and Twitter. Gradually, I started spending more time on the Fediverse.  Once Elon shat the bed on Twitter, I moved over completely. And, …]]></description>
										<content:encoded><![CDATA[<p>I remember seeing the original <a href="https://news.ycombinator.com/item?id=12646083">"A new decentralized microblogging platform"</a> on HackerNews back in October 2016. A few weeks later, I joined - becoming the 7,112th user.  As the years went on, my use of it waxed and waned. I started cross-posting to both Mastodon and Twitter. Gradually, I started spending more time on the Fediverse.</p>

<p>Once Elon shat the bed on Twitter, I moved over completely. And, you know what, I don't regret it for a second.</p>

<p>I've found a lovely community of people. I get my parasocial fix without being inundated by cryptogrifters shilling shitcoins, nor by thought-leaders posting inflammatory takes for clout.  There are no disingenuous politicians and remarkably few celebrities trying to sell me their bathwater. There's no advertising. There's a great API for bots. And - for now - people are generous with their time and expertise.</p>

<p>But, just to be contrary, let's list some of the bad points about it.</p>

<h2 id="there-are-fewer-people-about"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#there-are-fewer-people-about">There are fewer people about</a></h2>

<p>That does mean there are fewer arseholes<sup id="fnref:arse"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#fn:arse" class="footnote-ref" title="Not zero, just fewer." role="doc-noteref">0</a></sup>. But it doesn't yet feel as magical as Twitter did - when you could suddenly be in a conversation with a goat farmer from the other side of the planet and a world-famous astrophysicist.</p>

<p>The people who are about tend to be on the techy side of things. Which does mean putting up with some annoying pedantry and plenty of "<a href="https://shkspr.mobi/blog/2020/02/abstinence-isnt-safe-why-quitting-social-media-isnt-the-solution/">jUSt InsTaLl LinUx aNd delETE facEbOoK</a>."</p>

<h2 id="theres-a-bit-more-%e2%9c%a8drama%e2%9c%a8"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#theres-a-bit-more-%e2%9c%a8drama%e2%9c%a8">There's a bit more ✨drama✨</a></h2>

<p>Small, insular communities are <em>fractious</em>. A perceived insult or slight can rapidly descend into childish taunts of "well I'll defederate <em>you</em> first!"</p>

<p>There was drama on Twitter - and even more since Elon's full on conversion to the dark side - but because the community is smaller here, the drama feels bigger.</p>

<h2 id="fewer-official-accounts"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#fewer-official-accounts">Fewer official accounts</a></h2>

<p>This is a mixed bag. Frankly, Twitter should <em>never</em> have been a customer support channel. But businesses wanted to promote their goods and services, and customers took the opportunity to upbraid them in public. That led to all sorts of weird behaviours.</p>

<p>Nevertheless, I'd like to be able to see what's going on in local politics, and transport, and a dozen little services I used Twitter for.</p>

<h2 id="search-is-getting-better"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#search-is-getting-better">Search (is getting better)</a></h2>

<p>I've posted <a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/">some thoughts on Mastodon search</a>. It's now pretty good. But the federated nature of Mastodon means it'll never be as comprehensive as Twitter.</p>

<h2 id="perhaps-momentum-is-slowing-down"><a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#perhaps-momentum-is-slowing-down">Perhaps momentum is slowing down?</a></h2>

<p>I've seen plenty of waves of users over the years. But I think that the majority of people who wanted to leave Twitter have done so.</p>

<p>And... I think that's OK. I still use Facebook, I'm signed into a dozen different forums, I'm not particularly loyal to anything.</p>

<p>The Fediverse is about diversity. It would be nice if Twitter and Threads and BlueSky all federated with each other. But I think that Mastodon now has enough users to be self-sustaining. It doesn't need to become a giant killer. It <em>mustn't</em> become a de-facto monopoly.</p>

<p>I'm looking forward to the next 7 years here.</p>

<div id="footnotes" role="doc-endnotes">
<hr>
<ol start="0">

<li id="fn:arse">
<p>Not zero, just fewer.&nbsp;<a href="https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/#fnref:arse" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

</ol>
</div>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=46654&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/10/seven-years-on-mastodon/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
	</channel>
</rss>
