<?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>MastodonAPI &#8211; Terence Eden’s Blog</title>
	<atom:link href="https://shkspr.mobi/blog/tag/mastodonapi/feed/" rel="self" type="application/rss+xml" />
	<link>https://shkspr.mobi/blog</link>
	<description>Regular nonsense about tech and its effects 🙃</description>
	<lastBuildDate>Fri, 23 Jan 2026 12:13:07 +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>MastodonAPI &#8211; Terence Eden’s Blog</title>
	<link>https://shkspr.mobi/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<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 aria-label="Footnotes">
<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[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[Creating a generic "Log-in with Mastodon" service]]></title>
		<link>https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/</link>
					<comments>https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 08 Dec 2024 12:34:47 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[Auth0]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[oauth]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=54287</guid>

					<description><![CDATA[Let&#039;s say you have a website - your_website.tld - and you want people to log in to it using their Mastodon account.  For a traditional social-media site like Twitter or Facebook, you would create an OAuth app on the service that you want. But there are hundreds of Mastodon servers. So you need to create a new app for each one.  That sounds hard, but it isn&#039;t.  Well… not too hard.  Here&#039;s some c…]]></description>
										<content:encoded><![CDATA[<p>Let's say you have a website - <code>your_website.tld</code> - and you want people to log in to it using their Mastodon account.</p>

<p>For a traditional social-media site like Twitter or Facebook, you would create an OAuth app on the service that you want. But there are <em>hundreds</em> of Mastodon servers. So you need to create a new app for each one.  That sounds hard, but it isn't.  Well… not <em>too</em> hard.</p>

<p>Here's some <a href="https://infosec.press/jerry/how-to-user-mastodons-built-on-oauth-provider-as-the-authentication-provider">code adapted from Infosec.press</a>.  It's all written using cURL on the command line - so you should be able to adapt it to your preferred programming language.</p>

<h2 id="register-an-app-on-the-users-mastodon-instance"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#register-an-app-on-the-users-mastodon-instance">Register an app on the user's Mastodon instance</a></h2>

<p>Let's assume the user has given you the name of their Mastodon server - <code>example.social</code></p>

<p>You then send a request for an app to be created on <code>example.social</code> with your website's details. All it requests is the ability to read a user's details, nothing else.</p>

<pre><code class="language-bash">curl -X POST \
 -F "client_name=Login to your_website.tld" \
 -F "redirect_uris=https://your_website.tld/oauth/mastodon?server=example.social&amp;" \
 -F "scopes=read:accounts" \
 -F "website=https://your_website.tld" \
 -A "user-agent/0.1"
 https://example.social/api/v1/apps
</code></pre>

<p>You can set the User Agent to be anything suitable. Some servers won't work if it is omitted.</p>

<p>If the request was successful, <code>example.social</code> will send you this JSON in response:</p>

<pre><code class="language-json">{
  "id": "12345",
  "name": "Login to your_website.tld",
  "website": "https://your_website.tld",
  "scopes": [
    "read:accounts"
  ],
  "redirect_uris": [
    "https://your_website.tld/oauth/mastodon?server=example.social&amp;"
  ],
  "vapid_key": "qwertyuiop-asdfghjkl-zxcvbnm",
  "redirect_uri": "https://your_website.tld/oauth/mastodon?server=example.social&amp;",
  "client_id": "qw_asdfghjkl_zxcvbnm",
  "client_secret": "qwertyuiop1234567890"
}
</code></pre>

<p>Save the server's address, the <code>client_id</code>, and the <code>client_secret</code>. You will need all three later.</p>

<h2 id="the-user-logs-in-to-their-mastodon-instance"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#the-user-logs-in-to-their-mastodon-instance">The user logs in to their Mastodon instance</a></h2>

<p>You need to redirect the user to their server so they can log in. You need to construct a Mastodon URl using the data you received back. Don't forget to URl encode the <code>redirect_uri</code>.</p>

<p>For example, redirect the user to:</p>

<pre><code class="language-_">https://example.social/oauth/authorize
?client_id=qw_asdfghjkl_zxcvbnm
&amp;scope=read:accounts
&amp;redirect_uri=https://your_website.tld/oauth/mastodon%3Fserver=example.social%26
&amp;response_type=code
</code></pre>

<p>When the user visits that URl they can then log in. If they're successful, they'll be redirected back to your server using your specified redirect URI:</p>

<pre><code class="language-_">https://your_website.tld/oauth/mastodon?server=example.social&amp;code=qazwsxedcrfvtgbyhnujm
</code></pre>

<h2 id="get-a-bearer-token"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#get-a-bearer-token">Get a Bearer token</a></h2>

<p>Your website has received a GET request with the user's server name and an authorisation code. As per <a href="https://docs.joinmastodon.org/client/authorized/#token">the Mastodon documentation</a>, your app uses that code to request a Bearer token:</p>

<pre><code class="language-bash">curl -X POST \
 -F "client_id=qw_asdfghjkl_zxcvbnm" \
 -F "client_secret=qwertyuiop1234567890" \
 -F "redirect_uri=https://your_website.tld/oauth/mastodon?server=example.social&amp;" \
 -F "grant_type=authorization_code" \
 -F "code=qazwsxedcrfvtgbyhnujm" \
 -F "scope=read:accounts" \
 -A "user-agent/0.1"
 https://example.social/oauth/token
</code></pre>

<p>If that's worked, the user's server will return a Bearer token like this:</p>

<pre><code class="language-json">{
    "access_token": "abcdefg_123456",
    "token_type": "Bearer",
    "scope": "read:accounts",
    "created_at": 1732916685
}
</code></pre>

<h2 id="get-the-users-details"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#get-the-users-details">Get the user's details</a></h2>

<p>Finally(!) you can use that token to verify the user's credentials with the server:</p>

<pre><code class="language-bash">curl \
 -H "Authorization: Bearer abcdefg_123456" \
 -A "user-agent/0.1"
 https://example.social/api/v1/accounts/verify_credentials
</code></pre>

<p>If that works, you'll get back all the user's details. Something like this:</p>

<pre><code class="language-json">{
    "id": "7112",
    "username": "Edent",
    "acct": "Edent",
    "display_name": "Terence Eden",
    "url": "https://mastodon.social/@Edent",
    "avatar": "https://files.mastodon.social/accounts/avatars/000/007/112/original/37df032a5951b96c.jpg",
...
}
</code></pre>

<h2 id="putting-it-all-together"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#putting-it-all-together">Putting it all together</a></h2>

<ol>
<li>User providers their Mastodon instance's domain name</li>
<li>Your service looks up the domain name in its database

<ul>
<li>If there are no results, request to create a new app on the Mastodon instance and save the returned <code>client_id</code> and <code>client_secret</code></li>
</ul></li>
<li>Redirect the User to their Mastodon instance, using a URl which contains the <code>client_id</code> &amp; callback URl</li>
<li>User logs in to their Mastodon instance</li>
<li>The User's Mastodon instance redirects the User to your service's callback URl which includes an the instance's domain name and User's authorisation code</li>
<li>Your service reads the User's domain name and authorisation code</li>
<li>Your service exchanges those details for a Bearer token</li>
<li>Your service uses the Bearer token to get the User's account details</li>
</ol>

<h2 id="next-steps"><a href="https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/#next-steps">Next steps?</a></h2>

<p>This basic code works. For my next trick, can I integrate it into Auth0?</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=54287&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/12/creating-a-generic-log-in-with-mastodon-service/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Untappd to Mastodon - Updated!]]></title>
		<link>https://shkspr.mobi/blog/2024/05/untappd-to-mastodon-updated/</link>
					<comments>https://shkspr.mobi/blog/2024/05/untappd-to-mastodon-updated/#respond</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Sun, 12 May 2024 11:34:19 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[untappd]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=50507</guid>

					<description><![CDATA[A few years ago, I wrote some code to post Untappd check-ins to Mastodon.  I&#039;ve recently updated it to also post a photo of the beer you&#039;re enjoying.      First up, you&#039;ll need a file called config.py to hold all your API keys:  instance = &#34;https://mastodon.social&#34; access_token          = &#34;…&#34; write_access_token    = &#34;…&#34; untappd_client_id     = &#34;…&#34; untappd_client_secret = &#34;…&#34;   Then a file called u…]]></description>
										<content:encoded><![CDATA[<p>A few years ago, I wrote some code to <a href="https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/">post Untappd check-ins to Mastodon</a>.  I've recently updated it to also post a photo of the beer you're enjoying.</p>

<iframe src="https://mastodon.social/@Edent/111784153251146118/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" height="600" allowfullscreen="allowfullscreen"></iframe>

<script src="https://mastodon.social/embed.js" async="async"></script>

<p>First up, you'll need a file called <code>config.py</code> to hold all your API keys:</p>

<pre><code class="language-python">instance = "https://mastodon.social"
access_token          = "…"
write_access_token    = "…"
untappd_client_id     = "…"
untappd_client_secret = "…"
</code></pre>

<p>Then a file called <code>untappd2mastodon.py</code> to do the job of grabbing your data, finding your latest check-in, then posting it to the Fediverse:</p>

<pre><code class="language-python">#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mastodon import Mastodon
import json
import requests
import config

#  Set up access
mastodon = Mastodon( api_base_url=config.instance, access_token=config.write_access_token )

#       Untappd API
untappd_api_url = 'https://api.untappd.com/v4/user/checkins/edent?client_id=' + config.untappd_client_id + '&amp;client_secret='+ config.untappd_client_secret

r = requests.get(untappd_api_url)

untappd_data = r.json()

#       Latest checkin object
checkin = untappd_data["response"]["checkins"]["items"][0]
untappd_id = checkin["checkin_id"]

#       Was this ID the last one we saw?
check_file = open("untappd_last", "r")
last_id = int( check_file.read() )
print("Found " + str(last_id) )
check_file.close()

if (last_id != untappd_id ) :
        print("Found new checkin")
        check_file = open("untappd_last", "w")
        check_file.write( str(untappd_id) )
        check_file.close()
        #       Start creating the message
        message = ""

        if "checkin_comment" in checkin :
                message += checkin["checkin_comment"]

        if "beer" in checkin :
                message += "\nDrinking: " + checkin["beer"]["beer_name"]

        if "brewery" in checkin :
                message += "\nBy: "       + checkin["brewery"]["brewery_name"]

        if "venue" in checkin :
                if "venue_name" in checkin["venue"] :
                        message += "\nAt: "       + checkin["venue"]["venue_name"]
        #       Scores etc
        untappd_checkin_url = "https://untappd.com/user/edent/checkin/" + str(untappd_id)
        untappd_rating      = checkin["rating_score"]
        untappd_score       = "🍺" * int(untappd_rating)

        message += "\n" +  untappd_score + "\n" + untappd_checkin_url + "\n" + "#untappd"

        #       Get Image
        if checkin["media"]["count"] &gt; 0 :
                photo_url = checkin["media"]["items"][0]["photo"]["photo_img_lg"]
                download = requests.get(photo_url)
                with open("untappd.tmp", 'wb') as temp_file:
                        temp_file.write(download.content)
                media = mastodon.media_post("untappd.tmp", description="A photo of some beer.")
                mastodon.status_post(status = message, media_ids=media, idempotency_key = str(untappd_id))
        else:   
                #       Post to Mastodon. Use idempotency just in case something went wrong
                mastodon.status_post(status = message, idempotency_key = str(untappd_id))
else :
        print("No new checkin")
</code></pre>

<p>You can treat this code as being MIT licenced if that makes you happy.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=50507&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2024/05/untappd-to-mastodon-updated/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[How far did my post go on the Fediverse?]]></title>
		<link>https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/</link>
					<comments>https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 26 Sep 2023 11:34:28 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=46831</guid>

					<description><![CDATA[I wrote a moderately popular post on Mastodon. Lots of people shared it. Is it possible to find out how many different ActivityPub servers it went to?  Yes!  As we all know, the Fediverse is one big chain mail.  I don&#039;t mean that in a derogatory way.  When I write a post, it appears on my server (called an &#34;instance&#34; in Mastodon-speak).  Everyone on my instance can see my post.  My instance looks …]]></description>
										<content:encoded><![CDATA[<p>I wrote a moderately popular post on Mastodon. Lots of people shared it. Is it possible to find out how many different ActivityPub servers it went to?</p>

<p>Yes!</p>

<p>As we all know, <a href="https://m.fa.gl/@joshbal4/111048499514776098">the Fediverse is one big chain mail</a>.  I don't mean that in a derogatory way.</p>

<p>When I write a post, it appears on my server (called an "instance" in Mastodon-speak).</p>

<p>Everyone on my instance can see my post.</p>

<p>My instance looks at all my followers - some of whom are on completely different instances - and sends my post to their instances.</p>

<p>As an example:</p>

<ul>
<li>I am on <code>mastodon.social</code></li>
<li>John is on <code>eggman_social.com</code></li>
<li>Paul is on <code>penny_lane.co.uk</code></li>
<li>Both John and Paul follow me. So my post gets syndicated to their servers.</li>
</ul>

<p>With me so far?</p>

<p>What happens when someone shares (reposts) my status?</p>

<ul>
<li>John is on <code>eggman_social.com</code></li>
<li>Ringo is on <code>liverpool.drums</code></li>
<li>Ringo follows John</li>
<li>John reposts my status.</li>
<li><code>eggman_social.com</code> syndicates my post to <code>liverpool.drums</code></li>
</ul>

<p>And so my post goes around the Fediverse!  But can I see where it has gone?  Well... sort of! Let's look at how.</p>

<h2 id="a-note-on-privacy"><a href="https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/#a-note-on-privacy">A note on privacy</a></h2>

<p>People on Mastodon and the Fediverse tend to be privacy conscious. So there are limits - both in the API and the culture - as to what is acceptable.</p>

<p>Some people don't share their "social graph". That is, it is impossible to see who follows them or who they follow.</p>

<p>Users can choose to opt-in or -out of publicly sharing their social graph. They remain in control of their privacy.</p>

<p>In the example above, if Ringo were to reshare John's reshare of my status - John doesn't know about it. Only the original poster (me) gets notified. If John doesn't share his social graph, it <em>might</em> be possible to work out where Ringo saw the status - but that's rather unlikely.</p>

<p>Mastodon has an API rate limit which only allows 80 results per request and 1 request per second. That makes it long and tedious to crawl thousands of results.</p>

<p>Similarly, some instances do not share their social data or expose anything of significance. Some servers may no longer exist, or might have changed names. It's impossible to get a comprehensive view of the entire Fediverse network.</p>

<p>And that's OK! People should be able to set limits on what others can do with their data.  The code you're about to see doesn't attempt to breach anyone's privacy. All it does is show me which servers picked up my post. This is information which is already shown to me - but this makes it slightly easier to see.</p>

<h2 id="the-result"><a href="https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/#the-result">The Result</a></h2>

<p>I looked at <a href="https://mastodon.social/@Edent/111040801202691232">this post of mine</a> which was reposted over 100 times.</p>

<p>It eventually found its way to… <strong>2,547</strong> instances!</p>

<p>Ranging from <code>0ab.uk</code> to <code>թութ.հայ</code> via <code>godforsaken.website</code> and many more!</p>

<p>And that's one of the things which makes me hopeful this rebellion will succeed. There are a thousand points of light out there - each a shining beacon to doing things differently. And, the more the social media giants tighten their grip, the more these systems will slip through their fingers.</p>

<h2 id="the-code"><a href="https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/#the-code">The Code</a></h2>

<p>This is not very efficient code - nor well written. It was designed to scratch an itch.  It uses <a href="https://mastodonpy.readthedocs.io">Mastodon.py</a> to interact with the API.</p>

<p>It gets the instance names of all my followers. Then the instance names of everyone who reposted one of <em>my</em> posts.</p>

<p>But it cannot get the instance names of <em>everyone</em> who follows the users who reposted me - because:
<img src="https://shkspr.mobi/blog/wp-content/uploads/2023/09/Screenshot-2023-09-17-at-20-22-10-Mastodon.png" alt="Followers from other servers are not displayed. Browse more on the original profile." width="418" height="82" class="aligncenter size-full wp-image-47143">
The only way to get a list of followers from a user on a different instance is to apply for an API key for that instance. Which seems a bit impractical.</p>

<p>But I can get the instance name of the followers of accounts on my instance who reposted me. Clear?</p>

<p>I can also get a list of everyone who favourited my post. If they aren't on my instance, or one of my reposter's follower's instances, they're probably from a reposter who isn't on my instance.</p>

<p>My head hurts.</p>

<p>Got it? Here we go!</p>

<pre><code class="language-python">import config
from mastodon import Mastodon
from rich.pretty import pprint

#  Set up access
mastodon = Mastodon( api_base_url=config.instance, access_token=config.access_token, ratelimit_method='pace' )

#   Status to check for
status_id = 111040801202691232
print("Looking up status: " + str(status_id))

#   Get my data
me = mastodon.me()
my_id = me["id"]
print("You have User ID: " + str(my_id))

#   Empty sets
instances_all        = set()
instances_followers  = set()
instances_reposters  = set()
instances_reposters_followers  = set()
instances_favourites = set()

#   My Followers
followers = mastodon.account_followers( my_id )
print( "Getting all followers" )
followers_all = mastodon.fetch_remaining( followers )
print("Total followers = " + str( len(followers_all) ) )

#   Get the server names of all my followers
for follower in followers_all:
    if ( "@" in follower["acct"]) :
        f = follower["acct"].split("@")[1]
        instances_all.add( f )
        if ( f not in instances_followers):
            print( "Follower: " + f )
            instances_followers.add( f )
    else :
        instances_all.add( "mastodon.social" )
        instances_followers.add( "mastodon.social" )
total_followers  = len(instances_followers)
print( "Total Unique Followers Instances = " + str(total_followers)  )

#   Reposters
#   Find the accounts which reposted my status
reposters     = mastodon.status_reblogged_by( status_id )
reposters_all = mastodon.fetch_remaining(reposters)

#   Get all the instance names of my reposters
for reposter in reposters_all:
    if ( "@" in reposter["acct"]) :
        r = reposter["acct"].split("@")[1]
        instances_all.add( r )
        if ( r not in instances_followers ) :
            print( "Reposter: " + r )
            instances_reposters.add( r )
total_reposters  = len(instances_reposters)
print( "Total Unique Reposters Instances = " + str(total_reposters)  )

# Followers of reposters     
# This can take a *long* time!   
for reposter in reposters_all:   
    if ( "@" not in reposter["acct"]) :  
        reposter_id = reposter["id"]
        print( "Getting followers of reposter " + reposter["acct"] + " with ID " + str(reposter_id) )
        reposter_followers = mastodon.account_followers( reposter_id )   
        reposter_followers_all = mastodon.fetch_remaining( reposter_followers )  
        for reposter_follower in reposter_followers_all:    
            if ( "@" in reposter_follower["acct"]) : 
                f = reposter_follower["acct"].split("@")[1]
                instances_all.add( f )
                if (f not in instances_reposters_followers) :
                    print( "   Adding " + f + " from " + reposter["acct"] )
                    instances_reposters_followers.add( f )   
total_instances_reposters_followers  = len(instances_reposters_followers)
print( "Total Unique Reposters' Followers Instances = " + str(total_instances_reposters_followers)  )

#   Favourites
#   Find the accounts which favourited my status
favourites     = mastodon.status_favourited_by( status_id )
favourites_all = mastodon.fetch_remaining(favourites)

#   Get all the instance names of my favourites
for favourite in favourites_all:
    if ( "@" in favourite["acct"]) :
        f = favourite["acct"].split("@")[1]
        instances_all.add( f )
        if ( f not in instances_favourites ) :
            print( "Favourite: " + f )
            instances_favourites.add( r )
total_favourites = len(instances_favourites)

print( "Total Unique Favourites Instances  = " + str(total_favourites) )
print( "Total Unique Reposters Instances = " + str(total_reposters)  )
print( "Total Unique Followers Instances = " + str(total_followers)  )
print( "Total Unique Reposters' Followers Instances = " + str( len(instances_reposters_followers) ) )
print( "Total Unique Instances = " + str( len(instances_all) ) )
</code></pre>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=46831&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/09/how-far-did-my-post-go-on-the-fediverse/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[On The Fediverse, No One Knows You're A Liar]]></title>
		<link>https://shkspr.mobi/blog/2023/08/on-the-fediverse-no-one-knows-youre-a-liar/</link>
					<comments>https://shkspr.mobi/blog/2023/08/on-the-fediverse-no-one-knows-youre-a-liar/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Wed, 30 Aug 2023 11:34:02 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[Social Media]]></category>
		<category><![CDATA[Social Networks]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=46656</guid>

					<description><![CDATA[One of the reasons I&#039;m still on the original Mastodon.social instance is that I am vain. I joined shortly after the project was announced and, as a consequence, I have a &#34;joined&#34; date of 2016 and a user ID of under 10,000.  This doesn&#039;t make me an &#34;elder statesman&#34; and is rarely useful beyond bragging rights.  If I moved to a different server, my &#34;birthday&#34; would be irrevocably lost 😢  But… what i…]]></description>
										<content:encoded><![CDATA[<p>One of the reasons I'm still on the original Mastodon.social instance is that I am vain. I joined shortly after the project was announced and, as a consequence, I have a "joined" date of 2016 and a user ID of under 10,000<sup id="fnref:slashdot"><a href="https://shkspr.mobi/blog/2023/08/on-the-fediverse-no-one-knows-youre-a-liar/#fn:slashdot" class="footnote-ref" title="Anyone else remember Slashdot?" role="doc-noteref">0</a></sup>.  This doesn't make me an "elder statesman" and is rarely useful beyond bragging rights.</p>

<p>If I moved to a different server, my "birthday" would be irrevocably lost 😢</p>

<p>But… what if I moved to a <em>self-hosted</em> Mastodon instance? Why! Then the database would be under my complete control and I could put whatever data I wanted in there. <em>I could even <strong>lie</strong> about things!</em></p>

<p>Surely no one would be that silly though?</p>

<p>The other day I was chatting with someone whose follower count was so high that it temporarily broke my client.
<img src="https://shkspr.mobi/blog/wp-content/uploads/2023/08/Screenshot-2023-08-25-at-07-43-49-Mastodon.png" alt="Screenshot of the Mastodon interface. It claims the user has 97 million posts, follows 97 thousand people, and is followed by 97 billion accounts. Its join date is March 1997." width="393" height="207" class="aligncenter size-full wp-image-46658"></p>

<p>To be clear, the account <a href="https://mastodon.adtension.com/@admin">@admin@mastodon.adtension.com</a> is being deliberately provocative here. I don't think that they expect anyone to believe that they have more followers than the entirety of humanity, nor that they started using Mastodon last century.</p>

<p>This isn't a problem limited to Mastodon and ActivityPub. Twitter controls its own database and could, if it wanted to, <a href="https://mashable.com/article/elon-musk-inactive-followers-whole-x-platform">inflate follower numbers for insecure people</a>.</p>

<p>And, before you get too excited, this isn't a usecase for BlockChain! In theory, multiple servers could write statistics to a ledger (1,000 people on example.social follow @edent@whatever.social) but they have no way of verifying each others' statistics. So a determined user could have multiple fake instances writing fake data to the chain.</p>

<p>Chasing status is a mug's game. Anyone can <a href="https://shkspr.mobi/blog/2020/03/2019-%f0%9f%86%9a-2020/">hire a sports car for the afternoon and rent a fancy suit</a> - that doesn't mean they're a celebrity. Similarly, anyone can lie on the Fediverse and make you believe they're a social media superstar.</p>

<p>Don't believe the hype!</p>

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

<li id="fn:slashdot">
<p>Anyone else remember Slashdot?&nbsp;<a href="https://shkspr.mobi/blog/2023/08/on-the-fediverse-no-one-knows-youre-a-liar/#fnref:slashdot" 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=46656&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/08/on-the-fediverse-no-one-knows-youre-a-liar/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Some thoughts on Mastodon search]]></title>
		<link>https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/</link>
					<comments>https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 28 Aug 2023 11:34:04 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[search]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=46682</guid>

					<description><![CDATA[The latest version of Mastodon includes search functionality.  It&#039;s early days, but seems to work pretty well. Here are some of the interesting things I found when using it.  Search is complex - expectations  I don&#039;t mean the act of searching a database - that&#039;s routine - but I mean it is socially complex.  Lots of people left Twitter because it was too easy to search for them. For example, if…]]></description>
										<content:encoded><![CDATA[<p>The <a href="https://github.com/mastodon/mastodon/releases/tag/v4.2.0-beta2">latest version of Mastodon</a> includes search functionality.  It's early days, but seems to work pretty well. Here are some of the interesting things I found when using it.</p>

<h2 id="search-is-complex-expectations"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-expectations">Search is complex - expectations</a></h2>

<p>I don't mean the act of searching a database - that's routine - but I mean it is <em>socially</em> complex.</p>

<p>Lots of people left Twitter because it was <em>too</em> easy to search for them. For example, if you really hate people who support the wrong football team, it's trivial to search Twitter for people posting about that team. Then you can send them abuse. Or you can drag up old arguments. Or you can find something said years ago which sounds silly today.</p>

<p>So Mastodon was a haven. A place where it wasn't easy for twunts to pick a fight with you. Naturally, that means there has been some resistance to the idea of search - and that has informed its design decisions. By default, accounts cannot be searched. You have to make the positive choice to opt-in to search. And you can opt-out at any time.</p>

<p>That strikes me as a reasonable position. It adds a new feature for those that want it and keeps people safe if they'd rather not be bothered.</p>

<h2 id="search-is-complex-federation"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-federation">Search is complex - federation</a></h2>

<p>The Fediverse is a collection of servers which share information with each other.  You can only search the posts which your server knows about.</p>

<p>Here's what I mean.  I post on <code>mastodon.social</code> and you post on <code>whatever.int</code> - because I subscribe to you, all your posts are sent to <code>mastodon.social</code>. That means anyone on my server can search for your posts. No one from <code>fursuits.example</code> follows me, so my posts aren't searchable by their members.  But let's say <code>@jeff@whatever.int</code> is followed by <code>@sue@fursuits.example</code>  - if Jeff reposts me, then that specific post is syndicated to the <code>fursuits</code> instance and that specific post can then be found in search results.</p>

<p>A bit confusing - but necessary. This isn't a monolithic service like Twitter with a single database. It is a loose collection of distributed entities which share data. You cannot search the <em>entire</em> Fediverse.</p>

<h2 id="search-is-complex-images"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-images">Search is complex - images</a></h2>

<p>Six years ago, I noted that <a href="https://shkspr.mobi/blog/2017/02/accessibility-you-cant-search-twitter-for-alt-text/">you couldn't search Twitter for Alt-Text</a>. But, I am pleased to say, Mastodon allows for that! Search results include the description of images.</p>

<p>This can be a little confusing. You might search for a phrase and see dozens of posts which don't contain it in their body. It's only when you hover over the image that you'll find what you're looking for.</p>

<p>I don't know if this is something which could be improved with a better UI? Perhaps some way of highlighting that it is the image which is the result?</p>

<h2 id="search-is-complex-domains"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-domains">Search is complex - domains</a></h2>

<p>Searching text is complicated. Should a search for "mastodon" find posts with the words "mastodon<strong>s</strong>"? Probably. Should a search for the word "the" find "there", "their", or "them"? Probably not.</p>

<p>Should a search for "example.com" find "example.com/blog"? Maybe. <del datetime="2023-08-30T06:44:03+00:00">At the moment, it doesn't. So you can't use search to find who shared your website. Nor can you find people who are talking about a specific page on the web.</del></p>

<p><ins datetime="2023-08-30T06:44:03+00:00">Update! This works! A search for <code>shkspr.mobi -from:edent</code> finds all mentions of my website by people other than me.</ins></p>

<h2 id="search-is-complex-operator-discovery"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-operator-discovery">Search is complex - operator discovery</a></h2>

<p>As well as searching for words and phrases, you can also search <em>metadata</em>. For example, adding <code>has:video</code> to your query will find posts with an attached video. Using <code>is:reply</code> will find replies. And <code>after:2023-08-27</code> will find posts after a specific date.</p>

<p><del datetime="2023-08-30T06:44:03+00:00">At the moment, there's no way to know what those options are. There's <a href="https://github.com/mastodon/mastodon/pull/26344">a discussion on GitHub which enumerates them</a> but nothing for the casual user. Perhaps there needs to be an "Advanced Search" page? That's how most social networks do things.</del></p>

<p><ins datetime="2023-08-30T06:44:03+00:00">Update! There's now a drop-down on the search box which shows you all the options.
<img src="https://shkspr.mobi/blog/wp-content/uploads/2023/08/Screenshot-from-2023-08-30-07-45-46.png" alt="Infobox showing options for searching for languages, from users, date ranges, etc." width="354" height="557" class="aligncenter size-full wp-image-46717"></ins></p>

<h2 id="search-is-complex-whats-missing"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-whats-missing">Search is complex - what's missing?</a></h2>

<p>Twitter let users search by number of likes &amp; retweet - which was helpful for <a href="https://shkspr.mobi/blog/2020/05/finding-your-most-popular-tweets/">finding your most popular Tweets</a>. That's not (yet) available on Mastodon.</p>

<p>Similarly, there's no way to search by geography. Twitter let users tag posts with their geolocation which could then be queried. Neither of those features are on Mastodon's roadmap.</p>

<p>I don't think it is possible to search <em>previous</em> edits of a post. That's probably a sensible decision.</p>

<h2 id="search-is-complex-unknown-unknowns"><a href="https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/#search-is-complex-unknown-unknowns">Search is complex - unknown unknowns</a></h2>

<p>One valid criticism of the Fediverse is that it is hard to search for interesting people, discussions, or posts. Hopefully this opt-in search makes it easier for people to meet new friends and form new ideas.</p>

<p>And, hopefully, the controls around it are strict enough that it will prevent abuse from running rampant.</p>

<p>I think searching is going to be primarily a good thing for the Fediverse. But what second and third order effects will this bring?</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=46682&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/08/some-thoughts-on-mastodon-search/feed/</wfw:commentRss>
			<slash:comments>8</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[How do you decentralise emergency alerts?]]></title>
		<link>https://shkspr.mobi/blog/2023/04/how-do-you-decentralise-emergency-alerts/</link>
					<comments>https://shkspr.mobi/blog/2023/04/how-do-you-decentralise-emergency-alerts/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Fri, 21 Apr 2023 11:34:54 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[ActivityPub]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[ReDeCentralize]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=45624</guid>

					<description><![CDATA[Twitter&#039;s decision to hobble its API has meant that a number of useful alerting bots might no longer function. Your local subway might not be able to Tweet each morning about delays on the line, nor will a tornado warning be displayed as you scroll through photos of brunch, and forget about flood alerts between your memes.  In one sense, this is sad. A set of useful public services are being cut…]]></description>
										<content:encoded><![CDATA[<p>Twitter's decision to hobble its API has meant that a number of useful alerting bots might no longer function. Your local subway might not be able to Tweet each morning about delays on the line, nor will a tornado warning be displayed as you scroll through photos of brunch, and forget about flood alerts between your memes.</p>

<p>In one sense, this is sad. A set of useful public services are being cut off from their audience. My friend, Bill Thompson, described this as "<a href="https://someone.elses.computer/@billt/110201672232209911">unnecessary disruption</a>" I, on the other hand, think that creative destruction is sometimes a necessity.</p>

<iframe src="https://mastodon.social/@Edent/110201678592862401/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" height="300" allowfullscreen="allowfullscreen"></iframe>

<script src="https://mastodon.social/embed.js" async="async"></script>

<p><small>You can listen to more of the discussion on the <a href="http://garethandbillcast.com/the-gareth-and-billcast-stealing-cars-and-closing-the-public-square">Gareth and BillCast</a> about 19 minutes in.</small></p>

<p>Look, first off, it makes sense for organisations to put warning messages where their audience is likely to see it. No one sensible begrudges that. Twitter is a channel just as legitimate as radio, TV, or loudhailers in the street.</p>

<p>The problem comes when that's the <em>only</em> channel.</p>

<p>Twitter, somewhat accidentally, set itself up as a replacement for the public sphere. Now it has made it clear exactly what the cost of "free speech" is - alert bots are abandoning it.</p>

<p>Almost all the bots I mentioned above have websites. You can go check them, if you remember - but that isn't the same as having their warnings mixed in with your daily scrolling.</p>

<p>Some of those bots also have RSS feeds. But the RSS model doesn't lend itself to your peer group reposting urgent content into your feed.</p>

<p>And none of those websites and feeds are easily discoverable if you're in a new city.</p>

<p>So, how could this be solved with ActivityPub / Mastodon / Fediverse?</p>

<p>On the one hand, it's pretty easy for any organisation to set up a decentralised service with their alerts. Visiting <code>https:// mass_transit.ak</code> will show you all of Arstotzka's transport bots. You could subscribe to <code>@flood-warnings@alerts.gov.ak</code> for Arstotzka's official warnings.</p>

<p>On Twitter, it's easy to search "Train Delays London" and see <em>everything</em>. Hopefully including one of the accounts which gives you official alerts.</p>

<p>But, one of the interesting problems with a decentralised service is that search is (deliberately<sup id="fnref:deliberately"><a href="https://shkspr.mobi/blog/2023/04/how-do-you-decentralise-emergency-alerts/#fn:deliberately" class="footnote-ref" title="There is a strong culture of privacy and personal security on Mastodon. This is a good thing. People on a server dedicated to a marginalised community don't want their posts to be found by anyone who…" role="doc-noteref">0</a></sup>) difficult. Unless someone you're already following happens to share an alert, it can be very difficult to find relevant posts and accounts.</p>

<p>There is something of a solution which, I think, could be helpful. <a href="https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location">ActivityPub defines a way to add location to a post</a>.</p>

<pre><code class="language-JSON">{
  &amp;quot;@context&amp;quot;: &amp;quot;https://www.w3.org/ns/activitystreams&amp;quot;,
  &amp;quot;type&amp;quot;: &amp;quot;Note&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;Arstotzka&amp;#039; Flood Service:&amp;quot;,
  &amp;quot;content&amp;quot;: &amp;quot;Heavy rains! Expect flooding and major disruption.&amp;quot;,
    &amp;quot;location&amp;quot;: {
       &amp;quot;name&amp;quot;: &amp;quot;Arstotzka Central District,
       &amp;quot;type&amp;quot;: &amp;quot;Place&amp;quot;,
       &amp;quot;longitude&amp;quot;: 12.34,
       &amp;quot;latitude&amp;quot;: 56.78,
    }
}
</code></pre>

<p>At the moment, there's no way to search for geo-tagged posts - but it doesn't have to be that way. It is relatively simple to use the <a href="https://en.wikipedia.org/wiki/Haversine_formula">Haversine formula</a> to select database entries which are within a geographic area.</p>

<p>But, the key problem is <a href="https://shkspr.mobi/blog/2022/12/what-would-a-decentralised-uber-look-like/">too much decentralisation</a>.  It goes against the credo of redecentralization to have a single server called <code>official-alerts.com</code> - having a single destination to search is convenient, but provides for a single point of failure.</p>

<p>A country might have <code>alerts.ak</code> which has multiple accounts, or might have single-use instances like <code>flood.ak</code> and <code>fire.ak</code> and <code>traffic.ak</code>.  They might be federated to, for example, <code>official-alert-service.eu</code>.</p>

<p>At which point, I get stuck.</p>

<p>How does a user discover these accounts?</p>

<p>There are three options, as I see it:</p>

<ol>
<li>Advertising. The organisation promotes these accounts via other channels.</li>
<li>Serendipity. A user sees that another user has shared one of these accounts.</li>
<li>Better discovery tools. A user's instance could recommend accounts to follow based on geography or other factors.</li>
</ol>

<p>Thoughts?</p>

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

<li id="fn:deliberately">
<p>There is a <em>strong</em> culture of privacy and personal security on Mastodon. This is a good thing. People on a server dedicated to a marginalised community don't want their posts to be found by anyone who wants to do a drive-by trolling. But, of course, this makes it harder to assess the zeitgeist.&nbsp;<a href="https://shkspr.mobi/blog/2023/04/how-do-you-decentralise-emergency-alerts/#fnref:deliberately" 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=45624&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/04/how-do-you-decentralise-emergency-alerts/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Posting Untappd Checkins to Mastodon (and other services)]]></title>
		<link>https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/</link>
					<comments>https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Thu, 02 Mar 2023 12:34:56 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[beer]]></category>
		<category><![CDATA[cider]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[untappd]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=45036</guid>

					<description><![CDATA[I&#039;m a big fan of Untappd. It&#039;s a social drinking app which lets you check in to a beer and rate it. Look, we all need hobbies, mine is drinking cider.  You can see a list of everything I&#039;ve drunk over the 13 last years. Nearly 900 different pints!  After checking in, the app automatically posts to Twitter. But who wants to prop up Alan&#039;s failing empire? Not me! So here&#039;s some quick code to…]]></description>
										<content:encoded><![CDATA[<p>I'm a big fan of <a href="https://untappd.com">Untappd</a>. It's a social drinking app which lets you check in to a beer and rate it. Look, we all need hobbies, mine is drinking cider.  You can <a href="https://untappd.com/user/edent">see a list of everything I've drunk over the 13 last years</a>. Nearly 900 different pints!</p>

<p>After checking in, the app automatically posts to Twitter. But who wants to prop up Alan's failing empire? Not me! So here's some quick code to liberate your data and post it elsewhere.</p>

<p>There are two ways - APIs and Screen Scraping.</p>

<h2 id="api"><a href="https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/#api">API</a></h2>

<p>First up, a big disclaimer. Untappd <em>had</em> an API - but aren't accepting new users:</p>

<blockquote><p>Thank you for your interest in Untappd’s API. At this time, we are no longer accepting new applications for API access as we work to improve our review and support processes. We do not have a planned date to begin accepting new applications, so please check back soon.</p></blockquote>

<p>If you already have an API key (and I do) you can call your own data. This code saves the ID of the most recent checkin to a file. Each time it runs, it checks in the ID in the file is the same as what's returned by the API. If they are different, the post is published and the new ID is saved.</p>

<p>This is rough and ready code:</p>

<pre><code class="language-python">#!/usr/bin/env python
from mastodon import Mastodon
import json
import requests
import config

#  Set up access
mastodon = Mastodon( api_base_url=config.instance, access_token=config.write_access_token )

#   Untappd API - grab the most recent checkin
untappd_api_url = 'https://api.untappd.com/v4/user/checkins/edent?limit=1&amp;client_id=' + config.untappd_client_id + '&amp;client_secret='+ config.untappd_client_secret

r = requests.get(untappd_api_url)

untappd_data = r.json()

#   Latest checkin object
checkin = untappd_data["response"]["checkins"]["items"][0]
untappd_id = checkin["checkin_id"]

#   Was this ID the last one we saw?
check_file = open("untappd_last", "r")
last_id = int( check_file.read() )
print("Found " + str(last_id) )
check_file.close()

if (last_id != untappd_id ) :
    print("Found new checkin")
    check_file = open("untappd_last", "w")
    check_file.write( str(untappd_id) )
    check_file.close()
    #   Start creating the message
    message = ""

    if "checkin_comment" in checkin :
        message += checkin["checkin_comment"]

    if "beer" in checkin :
        message += "\nDrinking: " + checkin["beer"]["beer_name"]

    if "brewery" in checkin :
        message += "\nBy: "       + checkin["brewery"]["brewery_name"]

    if "venue" in checkin :
        if "venue_name" in checkin["venue"] :
            message += "\nAt: "       + checkin["venue"]["venue_name"]
    #   Scores etc
    untappd_checkin_url = "https://untappd.com/user/edent/checkin/" + str(untappd_id)
    untappd_rating      = checkin["rating_score"]
    untappd_score       = "🍺" * int(untappd_rating)

    message += "\n" +  untappd_score + "\n" + untappd_checkin_url + "\n" + "#untappd"
    print( message )
    #   Post to Mastodon. Use idempotency just in case something went wrong
    mastodon.status_post(status = message, idempotency_key = str(untappd_id))
else :
    print("No new checkin")
</code></pre>

<p>This doesn't do media or badges, but it's good enough to start with.</p>

<h2 id="screen-scraping"><a href="https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/#screen-scraping">Screen Scraping</a></h2>

<p>The Untappd HTML is pretty uniform, so using something like <a href="https://beautiful-soup-4.readthedocs.io/en/latest/">Beautiful Soup</a> is possible.</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2023/02/untappd_src.png" alt="HTML source code of the page." width="791" height="367" class="aligncenter size-full wp-image-45037">

<p>Here's some code to get you started</p>

<pre><code class="language-python">from bs4 import BeautifulSoup
import requests

#   Set the URL and headers
url = "https://untappd.com/user/edent"
headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0'}

#   Get the HTML
response = requests.get(url)
page_html = response.text

#   Parse the HTML into soup
soup = BeautifulSoup(page_html, 'html.parser')

#   Grab the data from the first checkin

untappd_id = int( soup.find("div", class_="item")["data-checkin-id"] )

comment = soup.find("p", class_="comment-text").text

#   Steal the beer from the icon's alt text
beer = soup.find("a", class_="label").find("img")["alt"]

#   Rating is a data element
rating = soup.find("div", class_="caps")["data-rating"]

#   Was this ID the last one we saw?
check_file = open("untappd_last", "r")
last_id = int( check_file.read() )
check_file.close()

if (last_id != untappd_id ) :
    check_file = open("untappd_last", "w")
    check_file.write( str(untappd_id) )
    check_file.close()
    message = .....
</code></pre>

<p>And from there you should have enough to start posting your checkins everywhere.  Stick that code in a crontab and have it run periodically.</p>

<p>Cheers!</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=45036&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2023/03/posting-untappd-checkins-to-mastodon-and-other-services/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Snowflake IDs in Mastodon (and Unique IDs in the Fediverse more generally)]]></title>
		<link>https://shkspr.mobi/blog/2022/12/snowflake-ids-in-mastodon-and-unique-ids-in-the-fediverse-more-generally/</link>
					<comments>https://shkspr.mobi/blog/2022/12/snowflake-ids-in-mastodon-and-unique-ids-in-the-fediverse-more-generally/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Mon, 12 Dec 2022 12:34:06 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[fediverse]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=44114</guid>

					<description><![CDATA[Computer Science has two canonical &#34;hard problems&#34;:   cache invalidation naming things off-by-one errors   Let&#039;s talk about how we name unique items in Federated services - for example, posts on a social media service.  If you have only one service, it&#039;s pretty easy.  Every time a new entry is created in a database, give it a sequential number.  This becomes a problem at scale. If you have…]]></description>
										<content:encoded><![CDATA[<p>Computer Science has two canonical "<a href="https://joncalder.co.za/2017-12-04-naming-things-is-hard/">hard problems</a>":</p>

<ol>
<li>cache invalidation</li>
<li>naming things</li>
<li>off-by-one errors</li>
</ol>

<p>Let's talk about how we name unique items in Federated services - for example, posts on a social media service.</p>

<p>If you have only one service, it's pretty easy.  Every time a new entry is created in a database, give it a sequential number.</p>

<p>This becomes a problem at scale. If you have millions of users on hundreds of different shards of a database, eventually you'll get a clash of IDs.  To that end, <a href="https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake">Twitter invented Snowflake IDs</a>.</p>

<p>Snowflakes are pretty clever.  They are a 64 bit ID. The first part of the ID is a timestamp, and the second part is some information about the server which generated the ID.  This means that IDs can be sorted by time, and will globally unique.</p>

<p>So, does the Fediverse use Snowflake? Yes. <strong>AND NO!</strong></p>

<p>The Mastodon service uses <a href="https://github.com/mastodon/mastodon/blob/main/lib/mastodon/snowflake.rb">Snowflake for its IDs</a>.  For example, one of my posts has the ID <a href="https://mastodon.social/@Edent/109347703064222520"><code>109347703064222520</code></a>.  If you bitshift that into a 64 bit number, you'll see it is a UNIX timestamp with a few bits of extra data.</p>

<p>But here comes the problem.  On a Federated service, there's <em>no guarantee</em> of uniqueness. A naughty server could deliberately generate a duplicate Snowflake ID.  You can't trust anyone on the Internet!</p>

<p>So here's how Mastodon - and the wider Fediverse - deals with unique IDs.  It cheats.</p>

<ul>
<li>When I make a post, my server gives it a Snowflake ID which is unique <em>to my server</em> - e.g. <code>123456</code></li>
<li>This generates a URl which is <em>globally</em> unique - e.g. <code>https:// my_server.xyz/@username/123456</code></li>
<li>My post is delivered to your server.</li>
<li>Your server gives it an ID which is unique to <em>your</em> server - e.g. <code>4d7b70c7</code></li>
<li>Your server turns that into a URl - e.g. <code>https:// your_server.lol/incoming/from/@username@my_server.xyz/4d7b70c7</code></li>
</ul>

<p>For example, here's the JSON response I get when I look up the Snowflake ID of a reply someone sent me. My server replies with:</p>

<pre><code class="language-json">{
 'created_at': datetime.datetime(2022, 11, 13, 0, 52, 37, tzinfo=tzutc()),
 'id': 109347716491680514,
 ...
 'uri': 'https://queer.af/@erincandescent/109347716173491502',
}
</code></pre>

<p>The <em>original</em> ID is <code>109347716173491502</code> on the sender's server.  But the unique ID on my server is <code>109347716491680514</code>.</p>

<p>Their unique ID could be a GUID, a series of emoji, or a sequential number. It doesn't matter. To prevent clashes, the receiving server generates its own ID.</p>

<p>This could all be solved a lot more easily if every server minted each post using a Proof of Work transaction against a centralised BlockCha… <em>*sound of gunshot*</em></p>

<p>Ahem.</p>

<p>It is a pretty reasonable solution. A remote server may or may not have a globally unique ID. It doesn't matter. Once you see a post, it is given its own <em>locally</em> unique ID.  And that's good enough.</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=44114&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2022/12/snowflake-ids-in-mastodon-and-unique-ids-in-the-fediverse-more-generally/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[WebMentions, Privacy, and DDoS - Oh My!]]></title>
		<link>https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/</link>
					<comments>https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 29 Nov 2022 12:34:15 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[metadata]]></category>
		<category><![CDATA[NaBloPoMo]]></category>
		<category><![CDATA[ogp]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=44259</guid>

					<description><![CDATA[Mastodon - the distributed social network - has two interesting challenges when it comes to how users share links.  I&#039;d like to discuss those issues and suggest a possible way forward.  When you click on a link on my website which takes you to another website, your browser sends a Referer. This says to the other site &#34;Hey, I came here using a link on shkspr.mobi&#34;.  This is useful because it lets…]]></description>
										<content:encoded><![CDATA[<p>Mastodon - the distributed social network - has two interesting challenges when it comes to how users share links.  I'd like to discuss those issues and suggest a possible way forward.</p>

<p>When you click on a link on my website which takes you to another website, your browser sends a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer">Referer</a><sup id="fnref:splel"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#fn:splel" class="footnote-ref" title="This is a spleling mistake which is part of the specification so cannot be changed." role="doc-noteref">0</a></sup>. This says to the other site "Hey, I came here using a link on <code>shkspr.mobi</code>".  This is useful because it lets a site owner know who is linking to them.  I <em>love</em> seeing which weird and wonderful sites have linked to my content.</p>

<p>It is also something of a privacy nightmare as it lets sites see who is clicking and from where they're clicking. So Mastodon sets a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/noreferrer"><code>noreferrer</code></a><sup id="fnref:spell"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#fn:spell" class="footnote-ref" title="This one is spelled correctly. Which makes life confusing for all involved." role="doc-noteref">1</a></sup> attribute on all links. This tells the browser not to send the Referer.</p>

<p>This means sites no longer know <em>who</em> is sending them traffic.</p>

<iframe src="https://masto.ai/@stavvers/109420849116336339/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" height="650" allowfullscreen="allowfullscreen"></iframe>

<p>That's either a good thing from a privacy perspective or a disaster from a marketing perspective. Or a little bit of both.</p>

<p>Here's a related issue. When a user posts a link to your website on Mastodon, the server checks your page to see if there are any oEmbed tags for a rich link preview. But, at the moment, it doesn't check your website's <a href="https://developers.google.com/search/docs/crawling-indexing/robots/intro"><code>robots.txt</code></a> file - which lets it know whether it is <em>allowed</em> to scrape your content.</p>

<iframe src="https://mastodon.mit.edu/@jefftk/109416209502343043/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" height="400" allowfullscreen="allowfullscreen"></iframe>

<p>In the case of something like Twitter or Facebook, this is fine. If a million users post a link, the centralised social network checks the link <em>once</em> and caches the result.</p>

<p>With - potentially - thousands of distributed Mastodon sites, this presents a problem. If a popular account posts a link, their instance fetches a rich preview. Then <em>every</em> instance which has users following them also requests that URL.  Essentially, this is a DDoS attack.</p>

<h2 id="i-can-fix-you"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#i-can-fix-you">I can fix you</a></h2>

<p>So here's my thoughts on how to fix this.</p>

<p>When a user posts a link to Mastodon, their instance should send a <a href="https://indieweb.org/Webmention">WebMention</a> to the site hosting the link.  This informs the website that someone has shared their content.  Perhaps a user could adjust their privacy settings to allow or deny this.</p>

<p>The instance would check the site's <code>robots.txt</code> and, if allowed, scrape the site to see if there were any <a href="https://shkspr.mobi/blog/2022/11/is-open-graph-protocol-dead/">Open Graph Protocol</a> metadata elements on it.</p>

<p>That metadata should be <em>included</em> in the post as it is shared across the network.</p>

<p>For example, a status could look like this:</p>

<pre><code class="language-json">{
  "id": "123",
  "created_at": "2022-03-16T14:44:31.580Z",
  "in_reply_to_id": null,
  "in_reply_to_account_id": null,
  "visibility": "public",
  "language": "en",
  "uri": "https://mastodon.social/users/Edent/statuses/123",
  "content": "&lt;p&gt;Check out https://example.com/&lt;/p&gt;",
  "ogp_allowed": true,
  "ogp": {
      "og:title": "My amazing site",
      "og:image:url": "https://cdn.mastodon.social/cache/example.com/preview.jpg",
      "og:description": "A long description. Perhaps the first paragraph of the text."
      ...
   }
   ...
}
</code></pre>

<p>When a post is boosted across the network, the instances can see that there is rich metadata associated with the link. If there is an image associate with the post, that will be loaded from the cache on the original Mastodon instance - avoiding overloading the website.</p>

<p>Now, there is a flaw in this idea. A <em>malicious</em> Mastodon server could serve up a fake OGP image and description. So a link to McDonald's might display a fake image promoting Burger King.</p>

<p>To protect against this, a receiving instance could randomly or periodically check the OGP metadata that they receive. If it has been changed, they can update it.</p>

<p>Perhaps a diagram would help?</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2022/11/Mastodon-OGP-Diagram.png" alt="Crappy line drawing explaining the above." width="787" height="416" class="aligncenter size-full wp-image-44270">

<h2 id="what-other-people-say-about-the-problem"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#what-other-people-say-about-the-problem">What other people say about the problem</a></h2>

<div class="activitypub-embed u-in-reply-to h-cite"> <div class="activitypub-embed-header p-author h-card"> <img class="u-photo" src="https://asset.circumstances.run/accounts/avatars/109/330/846/558/995/088/original/9aae78ca8a673cb2.png" alt=""> <div class="activitypub-embed-header-text"> <h2 class="p-name" id="david-gerard"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#david-gerard">David Gerard</a></h2> <a href="https://circumstances.run/users/davidgerard" class="ap-account u-url">@davidgerard@circumstances.run</a> </div> </div> <div class="activitypub-embed-content"> <div class="ap-subtitle p-summary e-content"><p>yes, you should put a cache in front of a blog. nginx and wp-supercache do well. but.</p><p>mastodon's auto-DDOS feature is still obnoxious. and in a social network, technically designed in obnoxiousness is incompetent.</p><p>i realise it'd need extension of activitypub, but is anyone working on sending prerendered cards with the URL? just to save 1000 servers hammering the URL to generate their own cards locally.</p></div> </div> <div class="activitypub-embed-meta"> <a href="https://circumstances.run/users/davidgerard/statuses/109421964176048304" class="ap-stat ap-date dt-published u-in-reply-to">2022-11-28, 14:44</a> <span class="ap-stat"> <strong>7</strong> boosts </span> <span class="ap-stat"> <strong>23</strong> favorites </span> </div> </div>

<style>/** * ActivityPub embed styles. */ .activitypub-embed { background: #fff; border: 1px solid #e6e6e6; border-radius: 12px; padding: 0; max-width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .activitypub-reply-block .activitypub-embed { margin: 1em 0; } .activitypub-embed-header { padding: 15px; display: flex; align-items: center; gap: 10px; } .activitypub-embed-header img { width: 48px; height: 48px; border-radius: 50%; } .activitypub-embed-header-text { flex-grow: 1; } .activitypub-embed-header-text h2 { color: #000; font-size: 15px; font-weight: 600; margin: 0; padding: 0; } .activitypub-embed-header-text .ap-account { color: #687684; font-size: 14px; text-decoration: none; } .activitypub-embed-content { padding: 0 15px 15px; } .activitypub-embed-content .ap-title { font-size: 23px; font-weight: 600; margin: 0 0 10px; padding: 0; color: #000; } .activitypub-embed-content .ap-subtitle { font-size: 15px; color: #000; margin: 0 0 15px; } .activitypub-embed-content .ap-preview { border: 1px solid #e6e6e6; border-radius: 8px; overflow: hidden; } .activitypub-embed-content .ap-preview img { width: 100%; height: auto; display: block; } .activitypub-embed-content .ap-preview { border-radius: 8px; box-sizing: border-box; display: grid; gap: 2px; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; margin: 1em 0 0; min-height: 64px; overflow: hidden; position: relative; width: 100%; } .activitypub-embed-content .ap-preview.layout-1 { grid-template-columns: 1fr; grid-template-rows: 1fr; } .activitypub-embed-content .ap-preview.layout-2 { aspect-ratio: auto; grid-template-rows: 1fr; height: auto; } .activitypub-embed-content .ap-preview.layout-3 > img:first-child { grid-row: span 2; } .activitypub-embed-content .ap-preview img { border: 0; box-sizing: border-box; display: inline-block; height: 100%; object-fit: cover; overflow: hidden; position: relative; width: 100%; } .activitypub-embed-content .ap-preview video, .activitypub-embed-content .ap-preview audio { max-width: 100%; display: block; grid-column: 1 / span 2; } .activitypub-embed-content .ap-preview audio { width: 100%; } .activitypub-embed-content .ap-preview-text { padding: 15px; } .activitypub-embed-meta { padding: 15px; border-top: 1px solid #e6e6e6; color: #687684; font-size: 13px; display: flex; gap: 15px; } .activitypub-embed-meta .ap-stat { display: flex; align-items: center; gap: 5px; } @media only screen and (max-width: 399px) { .activitypub-embed-meta span.ap-stat { display: none !important; } } .activitypub-embed-meta a.ap-stat { color: inherit; text-decoration: none; } .activitypub-embed-meta strong { font-weight: 600; color: #000; } .activitypub-embed-meta .ap-stat-label { color: #687684; } </style>

<h2 id="feedback"><a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#feedback">Feedback?</a></h2>

<p>Is this a problem? Does this present a viable solution? Have I missed something obvious? Please leave a comment and let me know 😃</p>

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

<li id="fn:splel">
<p>This is a spleling mistake which is part of the specification so cannot be changed.&nbsp;<a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#fnref:splel" class="footnote-backref" role="doc-backlink">↩︎</a></p>
</li>

<li id="fn:spell">
<p>This one <em>is</em> spelled correctly. Which makes life confusing for all involved.&nbsp;<a href="https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/#fnref:spell" 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=44259&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2022/11/webmentions-privacy-and-ddos-oh-my/feed/</wfw:commentRss>
			<slash:comments>16</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Getting Started with Mastodon's Conversations API]]></title>
		<link>https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/</link>
					<comments>https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Thu, 17 Nov 2022 12:34:10 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[api]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[NaBloPoMo]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[tutorial]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=44124</guid>

					<description><![CDATA[The social network service &#34;Mastodon&#34; allows people to publish posts. People can reply to those posts. Other people can reply to those replies - and so on.  What does that look like in the API?  Here&#039;s a quick guide to the concepts you need to know - and some code to help you visualise conversations.  When you scroll through the website, you normally see a list of replies.  It looks like this:    …]]></description>
										<content:encoded><![CDATA[<p>The social network service "Mastodon" allows people to publish posts. People can reply to those posts. Other people can reply to those replies - and so on.  What does that look like in the API?  Here's a quick guide to the concepts you need to know - and some code to help you visualise conversations.</p>

<p>When you scroll through the website, you normally see a list of replies.  It looks like this:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2022/11/conversation-fs8.png" alt="A list of posts. People are writing comments, but there's no link to whom they are replying." width="418" height="721" class="aligncenter size-full wp-image-44135">

<p>Because it acts as a one-dimensional list, there's no easy way to figure out which post someone is replying to.</p>

<p>The data structure underlying the conversation is quite different.  It actually looks like this:</p>

<img src="https://shkspr.mobi/blog/wp-content/uploads/2022/11/Conversation-ASCII-tree-fs8.png" alt="A threaded conversation. You can see the order in which people have replied to each other - and what posts they are referencing." width="315" height="379" class="aligncenter size-full wp-image-44136">

<h2 id="concepts"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#concepts">Concepts</a></h2>

<p>In Mastodon's API, a post is called a <code>status</code>.</p>

<p>Every status on Mastodon has an ID.  This is <em>usually</em> a <a href="https://shkspr.mobi/blog/2022/11/building-an-on-this-day-service-for-mastodon/">Snowflake ID</a> which is represented as a number.</p>

<p>When someone replies to a status on Mastodon, they create a new status which has a field called <code>in_reply_to_id</code>. As its name suggests, has the ID of the status they are replying to.</p>

<p>Let's imagine this simple conversation:</p>

<ol>
<li>Ada: "How are you?"</li>
<li>Bob: "I'm fine. And you?"</li>
<li>Ada: "Quite well, thank you!"</li>
</ol>

<p>Message 2 is in reply to message 1.  Message 3 is in reply to message 2.</p>

<p>In Mastodon's jargon, message 1 is the <em>ancestor</em> of message 2. Similarly, message 3 is the <em>descendant</em> of message 2.</p>

<pre><code class="language-text">  → Descendants →
1--------2-------3
   ← Ancestors ←
</code></pre>

<h3 id="branches"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#branches">Branches</a></h3>

<p>Now, let's imagine a more complicated conversation - one with branches!</p>

<pre><code class="language-text">1. Alice: What's your favourite pizza topping?
├── 2. Bette: Pineapple
│   ├── 4. Chuck: You make me sick!
│   └── 7. Dave: Yeah, I love pineapple too
└── 3. Chuck: Mushroom are the best
    ├── 5. Alice: Really?
    │   └── 6. Dave: Button mushrooms are best!
    └── 8. Elle: I like them too!
</code></pre>

<p>As you can see, people reply in threads.  In this example, <code>2</code> is a different "branch" of the conversations than <code>3</code>.</p>

<p>It looks a bit more complicated with hundreds of replies, but that's it! That's all you need to know!</p>

<h2 id="api"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#api">API</a></h2>

<p>If you want to download a <em>single</em> status with an ID of <code>1234</code> the API call is <a href="https://docs.joinmastodon.org/methods/statuses/"><code>/api/v1/statuses/1234</code></a></p>

<p>If you want to download a conversation, it is a little bit more complicated. Mastdon's API calls a conversation a <a href="https://docs.joinmastodon.org/entities/context/"><code>context</code></a></p>

<p>Let's take the above simple example - Ada and Bob speaking. Ada's first status has an ID of <code>1</code>.  To get the conversation, the API call is <code>/api/v1/statuses/1/context</code></p>

<p>That returns two things:</p>

<ul>
<li>A list of <code>ancestors</code>. This is empty because <code>1</code> is the first status in this conversation.</li>
<li>A list of <code>descendants</code>. This contains statuses <code>2</code> and <code>3</code>.</li>
</ul>

<p>You will note, the <code>context</code> does <strong>not</strong> return the status <code>1</code> itself.</p>

<p>Let's suppose that, instead of asking for the context of status <code>1</code>, we instead asked for <code>2</code>. This would return:</p>

<ul>
<li>A list of <code>ancestors</code>. This contains status <code>1</code>.</li>
<li>A list of <code>descendants</code>. This contains status <code>3</code>.</li>
</ul>

<p>What about if we asked for <code>3</code>? This would return:</p>

<ul>
<li>A list of <code>ancestors</code>. This contains status <code>1</code> and <code>2</code></li>
<li>A list of <code>descendants</code>. This is empty because <code>3</code> is the last message in this conversation.</li>
</ul>

<h3 id="branches"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#branches">Branches</a></h3>

<p>When it comes to complex threads - like the pizza example - things become a bit more difficult.  Let's see the example again:</p>

<pre><code class="language-text">1. Alice: What's your favourite pizza topping?
├── 2. Bette: Pineapple
│   ├── 4. Chuck: You make me sick!
│   └── 7. Dave: Yeah, I love pineapple too
└── 3. Chuck: Mushroom are the best
    ├── 5. Alice: Really?
    │   └── 6. Dave: Button mushrooms are best!
    └── 8. Elle: I like them too!
</code></pre>

<p>Suppose we ask for the <code>context</code> of the message with ID <code>5</code>.  This would return:</p>

<ul>
<li>A list of <code>ancestors</code>. This contains statuses <code>1</code> and <code>3</code></li>
<li>A list of <code>descendants</code>. This contains status <code>6</code>.</li>
</ul>

<p>That's it!?!? Where are the rest? They are part of a <em>different</em> conversation branch. Even status <code>8</code> isn't returned because it's a reply to <code>3</code>, not <code>5</code>.</p>

<p>In order to get the full conversation, we need to be sneaky!</p>

<p>The list of <code>ancestors</code> contains the first message in the conversation.  So we can grab that, and then call <code>context</code> again for its ID.</p>

<p>Let's dive into some Python code to see how it works.</p>

<h2 id="code"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#code">Code</a></h2>

<p>This uses the <a href="https://mastodonpy.readthedocs.io/">Mastodon.py</a> library for calling the Mastodon API and the <a href="https://treelib.readthedocs.io/en/latest/">Python treelib</a> to create a conversation tree data structure.</p>

<p>This code connects to Mastodon and receives the status for a single ID.</p>

<pre><code class="language-python">from mastodon import Mastodon
from treelib import Node, Tree

mastodon = Mastodon( api_base_url="https://mastodon.example", access_token="Your personal access token from your instance" )

status_id =  109348943537057532 
status = mastodon.status(status_id)
</code></pre>

<p>Getting the conversation means calling the <code>context</code> API:</p>

<pre><code class="language-python">conversation = mastodon.status_context(status_id)
</code></pre>

<p><mark>⚠ Note:</mark> Calling the <code>context</code> on a large thread may take a long time. The longer the conversation, the longer you'll have to wait.</p>

<p>If there are ancestors, that means we are only on a single branch.  The 0th ancestor is the top of the conversation tree.  So let's get the <code>context</code> for that top status:</p>

<pre><code class="language-python">if len(conversation["ancestors"]) &gt; 0 :
   status = conversation["ancestors"][0]
   status_id = status["id"]
   conversation = mastodon.status_context(status_id)
</code></pre>

<p>Next, we need to create a data structure to hold the conversation.  We'll start by adding to it the first status in the conversation:</p>

<pre><code class="language-python">tree = Tree()

tree.create_node(status["uri"], status["id"])
</code></pre>

<p>Finally, we add any replies which are in the <code>descendants</code>. It is possible that some earlier statuses have been deleted. So we won't add any status which are replies to deleted statuses:</p>

<pre><code class="language-python">for status in conversation["descendants"] :
   try :
      tree.create_node(status["uri"], status["id"], parent=status["in_reply_to_id"])
   except :
      #  If a parent node is missing
      print("Problem adding node to the tree")
</code></pre>

<p>That's it! Let's show the tree:</p>

<pre><code class="language-python">tree.show()
</code></pre>

<p>Here's what it should look like:</p>

<pre><code class="language-text">2022-11-14 20:02 Edent: Today I was meant to be flying in to San Francisco to attend Twitter's Developer Conference - Chirp.Twitter had paid for my flights and hotel, because I was one of their developer insiders. I planned to spend the week meeting friends old and new.Instead, Alan the Hyperprat canceled the conference. So I'm staying in the UK.So I'm going to spend the week hacking on Mastdon's #API and building cool shit.  That'll show him!You can see what I'm working on at https://shkspr.mobi/blog/2022/11/building-an-on-this-day-service-for-mastodon/ https://mastodon.social/users/Edent/statuses/109343943300929632
├── 2022-11-14 20:10 Edent: Oh! And I was meant to be attending a Belle &amp; Sebastian gig tonight. I canceled those tickets for I could fly to SF.So far, I reckon Alan's acquisition of Twitter has cost me close to £190.Wonder if he's good for the money? https://mastodon.social/users/Edent/statuses/109343972435801664
│   ├── 2022-11-14 20:14 thehodge: @Edent reminds me of the time I was booked to speak at a conference in Munich and I excitedly booked a behind the scenes tour of the worlds largest miniature city!Then the company went under!Gutted. https://mastodon.social/users/thehodge/statuses/109343989481494630
│   ├── 2022-11-14 21:16 Janiqueka: @Edent the way my bill for him keeps increasing https://mastodon.online/users/Janiqueka/statuses/109344233355230523
│   ├── 2022-11-14 21:19 henry: @Edent I was due to be at B&amp;S tomorrow but it’s been postponed again.. not sure if that makes it better or worse for you! https://social.lc/users/henry/statuses/109344244402822729
│   │   └── 2022-11-15 04:53 Edent: @henry again!? Ah well!Hope you get to see them soon. https://mastodon.social/users/Edent/statuses/109346031194446940
│   ├── 2022-11-15 09:18 Amandafclark: @Edent send him an invoice :) https://mastodon.social/users/Amandafclark/statuses/109347071811426672
│   └── 2022-11-15 11:29 Edent: One of the #MastodonAPI projects I'm working on is a better way to view long &amp; complex threads.You may have seen me build something similar for the other site a while ago - demo at https://shkspr.mobi/blog/2021/09/augmented-reality-twitter-conversations/ - so I'm hoping I can do something similarly interesting.Main limitation is getting *all* of the conversation threads. It looks like the context API isn't paginated. But I might be being thick. https://mastodon.social/users/Edent/statuses/109347587353822637
│       ├── 2022-11-15 11:36 bensb: @Edent Excellent project. You might have seen, but there's also this feature request for better 🧵 handling: https://github.com/mastodon/mastodon/issues/8615 https://genomic.social/users/bensb/statuses/109347612990393791
│       ├── 2022-11-15 11:39 Edent: Cor! That @katebevan is good for engagement! Look at all those conversations she's kicked off! https://mastodon.social/users/Edent/statuses/109347627634008550
│       │   ├── 2022-11-15 11:58 Edent: Indeed, how could they be?That means that ID of a reply is different depending on where you see it.So the ID of this post is:mastodon. social /@ edent/ 123456But when you see it on your server, it might appear as:your. server /@ edent/ 987654The #MastodonAPI copes with this really well. But it is a mite confusing to get one's head around. https://mastodon.social/users/Edent/statuses/109347703064222520
│       │   │   ├── 2022-11-15 12:02 erincandescent: @Edent the numeric IDs are not part of the protocol - it's all URL based. Pleroma uses UUIDs for example https://queer.af/users/erincandescent/statuses/109347716173491502
│       │   │   │   └── 2022-11-15 12:06 Edent: @erincandescent oh! That's interesting. Thanks. https://mastodon.social/users/Edent/statuses/109347734283971306
</code></pre>

<p>Once you have a tree, you can format the contents however you like.</p>

<h2 id="grab-the-code"><a href="https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/#grab-the-code">Grab the code</a></h2>

<p>You can <a href="https://codeberg.org/edent/Mastodon_Tools">download the code for my Mastodon API tools from CodeBerg</a>. Enjoy!</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=44124&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2022/11/getting-started-with-mastodons-conversations-api/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title><![CDATA[Twitter's archive doesn't have alt text - but Mastodon's does!]]></title>
		<link>https://shkspr.mobi/blog/2022/11/twitters-archive-doesnt-have-alt-text-but-mastodons-does/</link>
					<comments>https://shkspr.mobi/blog/2022/11/twitters-archive-doesnt-have-alt-text-but-mastodons-does/#comments</comments>
				<dc:creator><![CDATA[@edent]]></dc:creator>
		<pubDate>Tue, 15 Nov 2022 12:34:45 +0000</pubDate>
				<category><![CDATA[/etc/]]></category>
		<category><![CDATA[a11y]]></category>
		<category><![CDATA[accessibility]]></category>
		<category><![CDATA[mastodon]]></category>
		<category><![CDATA[MastodonAPI]]></category>
		<category><![CDATA[NaBloPoMo]]></category>
		<category><![CDATA[twitter]]></category>
		<guid isPermaLink="false">https://shkspr.mobi/blog/?p=43986</guid>

					<description><![CDATA[Because I don&#039;t trust Alan, the Hyperprat who now runs Twitter, I decided to download my Twitter archive before setting my account to dormant.  About a decade ago, I wrote about how the Twitter archive works and where it is deficient.  Things have got better, but there are still annoying limitations.  For example, Hannah Kolbeck - founder of the Alt Text Reminder Bot recently pointed out that…]]></description>
										<content:encoded><![CDATA[<p>Because I don't trust Alan, the Hyperprat who now runs Twitter, I decided to download my Twitter archive before setting my account to dormant.</p>

<p>About a decade ago, <a href="https://shkspr.mobi/blog/2013/02/deficiencies-in-the-twitter-archive/">I wrote about how the Twitter archive works and where it is deficient</a>.  Things have got better, but there are still annoying limitations.</p>

<p>For example, <a href="https://www.patreon.com/posts/introducing-alt-70133193">Hannah Kolbeck - founder of the Alt Text Reminder Bot</a> recently pointed out that there's no alt text in the archives.</p>

<p>Here's a snippet of Twitter's JSON for an image I posted:</p>

<pre><code class="language-json">"media" : [
   {
      "expanded_url" : "https://twitter.com/edent/status/1579574033720705025/photo/1",
      "indices" : [
        "66",
        "89"
      ],
      "url" : "https://t.co/J1hr0ZfbTl",
      "media_url" : "http://pbs.twimg.com/media/FevGM32XEAA0FX2.jpg",
      "id_str" : "1579574018776174592",
      "id" : "1579574018776174592",
      "media_url_https" : "https://pbs.twimg.com/media/FevGM32XEAA0FX2.jpg",
      "sizes" : {
           "small" : {
                "w" : "680",
                "h" : "510",
                "resize" : "fit"
              },
              "medium" : {
                "w" : "1200",
                "h" : "900",
                "resize" : "fit"
              },
              "thumb" : {
                "w" : "150",
                "h" : "150",
                "resize" : "crop"
              },
              "large" : {
                "w" : "1236",
                "h" : "927",
                "resize" : "fit"
              }
       },
       "type" : "photo",
       "display_url" : "pic.twitter.com/J1hr0ZfbTl"
     }
],
</code></pre>

<p>Lots of different media sizing options, but no room for accessibility.</p>

<p>By comparison, the <a href="https://joinmastodon.org/">Mastodon social network</a> gives you the alt text. Here's a snippet of Mastodon's JSON for the same image which was cross-posted:</p>

<pre><code class="language-json">"attachment": [
   {
     "type": "Document",
     "mediaType": "image/jpeg",
     "url": "/media_attachments/files/109/145/933/102/890/212/original/84ae501e39f45091.jpg",
     "name": "A sign for priority seating. The pregnant person's face has been replaced by 😍. The person holding a baby has a face of 😫. The elderly person with a cane has 🥴.",
     "blurhash": "UhKdk{0LRit6-:t6WCWC-oxaRmWBozt7xaa|",
     "width": 1236,
     "height": 927
   }
],
</code></pre>

<p>Mastodon is a friendlier alternative to Twitter and - mostly - gets accessibility right.  There's still some work to do</p>

<iframe src="https://mastodon.social/@Edent/109332316198045303/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" height="500" allowfullscreen="allowfullscreen"></iframe>

<script src="https://static-cdn.mastodon.social/embed.js" async="async"></script>

<p>You can fix Twitter's missing alt text using <a href="https://web.archive.org/web/20220815042000/https://alt-text.org/">Hannah's Alt Text Archive Tool</a>.  That'll get you a JSON file full of your alt text, which you can use to recreate your archive.</p>

<p>Look, it's obvious that <a href="https://techcrunch.com/2022/11/04/elon-musk-twitter-layoffs/">Alan doesn't give a flying fuck about accessibility</a>, so I don't expect this to change any time soon.</p>

<p>Instead, people should do what they did when MySpace went to shit; move to a different platform.</p>

<p><a href="https://joinmastodon.org/">Join Mastodon today</a>!</p>
<img src="https://shkspr.mobi/blog/wp-content/themes/edent-wordpress-theme/info/okgo.php?ID=43986&HTTP_REFERER=RSS" alt="" width="1" height="1" loading="eager">]]></content:encoded>
					
					<wfw:commentRss>https://shkspr.mobi/blog/2022/11/twitters-archive-doesnt-have-alt-text-but-mastodons-does/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
