<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?><feed
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:thr="http://purl.org/syndication/thread/1.0"
	xml:lang="en-US"
	><title>Sacha Chua - category - mastodon</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/category/mastodon/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/category/mastodon" />
  <id>https://sachachua.com/blog/category/mastodon/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2025-03-27T17:20:15Z</updated>
<entry>
		<title type="html">Tweaking my 11ty blog to link to the Mastodon post defined in an Org Mode property</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-27T17:20:15Z</updated>
    <published>2025-03-27T17:20:15Z</published>
    <category term="11ty" />
<category term="mastodon" />
<category term="org" />
		<id>https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/</id>
		<content type="html"><![CDATA[<p>
One of the things I like about blogging from Org
Mode in Emacs is that it's easy to add properties
to the section that I'm working on and then use
those property values elsewhere. For example, I've
modified Emacs to simplify <a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/#:~:text=If%20it%20has%20and%20I%20don't%20have%20a%20Mastodon%20toot%20field%20yet%2C%20maybe%20I%20can%20automatically%20set%20that%20property%2C%20assuming%20I%20end%20up%20back%20in%20the%20Org%20Mode%20file%20I%20started%20it%20from.">tooting a link to my
blog post</a> and <a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/#:~:text=If%20it%20has%20and%20I%20don't%20have%20a%20Mastodon%20toot%20field%20yet%2C%20maybe%20I%20can%20automatically%20set%20that%20property%2C%20assuming%20I%20end%20up%20back%20in%20the%20Org%20Mode%20file%20I%20started%20it%20from.">saving the Mastodon status URL</a> in
the <code>EXPORT_MASTODON</code> property. Then I can use
that in my <a href="https://www.11ty.dev/">11ty</a> static site generation process to
include a link to the Mastodon thread as a comment
option.
</p>

<p>
First, I need to export the property and include
it in the front matter. I use <a href="https://www.11ty.dev/docs/data-template-dir/">.11tydata.json</a> files
to store the details for each blog post. I
modified <a href="https://github.com/sachac/ox-11ty/blob/master/ox-11ty.el">ox-11ty.el</a> so that I could specify
functions to change the front matter
(<code>org-11ty-front-matter-functions</code>,
<code>org-11ty&#45;&#45;front-matter</code>):
</p>


<div class="org-src-container">
<pre class="src src-elisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">org-11ty-front-matter-functions</span> nil
  <span class="org-doc">"Functions to call with the current front matter plist and info."</span>)
(<span class="org-keyword">defun</span> <span class="org-function-name">org-11ty&#45;&#45;front-matter</span> (info)
  <span class="org-doc">"Return front matter for INFO."</span>
  (<span class="org-keyword">let*</span> ((date (plist-get info <span class="org-builtin">:date</span>))
         (title (plist-get info <span class="org-builtin">:title</span>))
         (modified (plist-get info <span class="org-builtin">:modified</span>))
         (permalink (plist-get info <span class="org-builtin">:permalink</span>))
         (categories (plist-get info <span class="org-builtin">:categories</span>))
         (collections (plist-get info <span class="org-builtin">:collections</span>))
         (extra (<span class="org-keyword">if</span> (plist-get info <span class="org-builtin">:extra</span>) (json-parse-string
                                             (plist-get info <span class="org-builtin">:extra</span>)
                                             <span class="org-builtin">:object-type</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">plist</span>))))
    (seq-reduce
     (<span class="org-keyword">lambda</span> (prev val)
       (funcall val prev info))
     org-11ty-front-matter-functions
     (append
      extra
      (list <span class="org-builtin">:permalink</span> permalink
            <span class="org-builtin">:date</span> (<span class="org-keyword">if</span> (listp date) (car date) date)
            <span class="org-builtin">:modified</span> (<span class="org-keyword">if</span> (listp modified) (car modified) modified)
            <span class="org-builtin">:title</span> (<span class="org-keyword">if</span> (listp title) (car title) title)
            <span class="org-builtin">:categories</span> (<span class="org-keyword">if</span> (stringp categories) (split-string categories) categories)
            <span class="org-builtin">:tags</span> (<span class="org-keyword">if</span> (stringp collections) (split-string collections) collections))))))
</pre>
</div>


<p>
Then I added <a href="https://sachachua.com/dotemacs#org-mode-publishing-11ty-static-site-generation-include-mastodon-field-in-front-matter">the <code>EXPORT_MASTODON</code> Org property</a> as
part of the front matter. This took a little
figuring out because I needed to pass it as one of
<code>org-export-backend-options</code>, where the parameter
is defined as <code>MASTODON</code> but the actual property
needs to be called <code>EXPORT_MASTODON</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-11ty-add-mastodon-to-front-matter</span> (front-matter info)
  (plist-put front-matter <span class="org-builtin">:mastodon</span> (plist-get info <span class="org-builtin">:mastodon</span>)))
(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ox-11ty</span>
  (<span class="org-keyword">cl-pushnew</span>
   <span class="org-highlight-quoted-quote">'</span>(<span class="org-builtin">:mastodon</span> <span class="org-string">"MASTODON"</span> nil nil)
   (org-export-backend-options (org-export-get-backend <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">11ty</span>)))
  (add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-11ty-front-matter-functions</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-org-11ty-add-mastodon-to-front-matter</span>))
</pre>
</div>


<p>
Then I added the Mastodon field as an option to my
<a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/comments.cjs">comments.cjs</a> shortcode. This was a little tricky
because I'm not sure I'm passing the data
correctly to the shortcode (sometimes it ends up
as item.data, sometimes it's item.data.data,
&#x2026;?), but with <code>?.</code>, I can just throw all the
possibilities in there and it'll eventually find
the right one.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">const</span> <span class="org-variable-name">pluginRss</span> = require(<span class="org-string">'@11ty/eleventy-plugin-rss'</span>);
module.exports = <span class="org-keyword">function</span>(<span class="org-variable-name">eleventyConfig</span>) {
  <span class="org-keyword">function</span> <span class="org-function-name">getCommentChoices</span>(<span class="org-variable-name">data</span>, <span class="org-variable-name">ref</span>) {
    <span class="org-keyword">const</span> <span class="org-variable-name">mastodonUrl</span> = data.mastodon || data.page?.mastodon || data.data?.mastodon;
    <span class="org-keyword">const</span> <span class="org-variable-name">mastodon</span> = mastodonUrl &amp;&amp; <span class="org-string">`&lt;a href="${mastodonUrl}" target="_blank" rel="noopener noreferrer"&gt;comment on Mastodon&lt;/a&gt;`</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">url</span> = ref.absoluteUrl(data.url || data.permalink || data.data?.url || data.data?.permalink, data.metadata?.url || data.data?.metadata?.url);
    <span class="org-keyword">const</span> <span class="org-variable-name">subject</span> = encodeURIComponent(<span class="org-string">'Comment on '</span> + url);
    <span class="org-keyword">const</span> <span class="org-variable-name">body</span> = encodeURIComponent(<span class="org-string">"Name you want to be credited by (if any): \nMessage: \nCan I share your comment so other people can learn from it? Yes/No\n"</span>);
    <span class="org-keyword">const</span> <span class="org-variable-name">email</span> = <span class="org-string">`&lt;a href="mailto:sacha@sachachua.com?subject=${subject}&amp;body=${body}"&gt;e-mail me at sacha@sachachua.com&lt;/a&gt;`</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">disqusLink</span> = url + <span class="org-string">'#comment'</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">disqusForm</span> = data.metadata?.disqusShortname &amp;&amp; <span class="org-string">`&lt;div id="disqus_thread"&gt;&lt;/div&gt;</span>
<span class="org-string">&lt;script&gt;</span>
<span class="org-string"> var disqus_config = function () {</span>
<span class="org-string">   this.page.url = "${url}";</span>
<span class="org-string">   this.page.identifier = "${data.id || ''} ${data.metadata?.url || ''}?p=${ data.id || data.permalink || this.page?.url}";</span>
<span class="org-string">   this.page.disqusTitle = "${ data.title }"</span>
<span class="org-string">   this.page.postId = "${ data.id || data.permalink || this.page?.url }"</span>
<span class="org-string"> };</span>
<span class="org-string"> (function() { // DON'T EDIT BELOW THIS LINE</span>
<span class="org-string">   var d = document, s = d.createElement('script');</span>
<span class="org-string">   s.src = 'https://${ data.metadata?.disqusShortname }.disqus.com/embed.js';</span>
<span class="org-string">   s.setAttribute('data-timestamp', +new Date());</span>
<span class="org-string">   (d.head || d.body).appendChild(s);</span>
<span class="org-string"> })();</span>
<span class="org-string">&lt;/script&gt;</span>
<span class="org-string">&lt;noscript&gt;Disqus requires Javascript, but you can still e-mail me if you want!&lt;/noscript&gt;`</span>;
    <span class="org-keyword">return</span> { mastodon, disqusLink, disqusForm, email };
  }
  eleventyConfig.addShortcode(<span class="org-string">'comments'</span>, <span class="org-keyword">function</span>(<span class="org-variable-name">data</span>, <span class="org-variable-name">linksOnly</span>=<span class="org-constant">false</span>) {
    <span class="org-keyword">const</span> { mastodon, disqusForm, disqusLink, email } = getCommentChoices(data, <span class="org-constant">this</span>);
    <span class="org-keyword">if</span> (linksOnly) {
      <span class="org-keyword">return</span> <span class="org-string">`You can ${mastodon ? mastodon + ', ' : ''}&lt;a href="${disqusLink}"&gt;comment with Disqus (JS required)&lt;/a&gt;${mastodon ? ',' : ''} or ${email}.`</span>;
    } <span class="org-keyword">else</span> {
      <span class="org-keyword">return</span> <span class="org-string">`&lt;div id="comment"&gt;&lt;/div&gt;</span>
<span class="org-string">You can ${mastodon ? mastodon + ', ' : ''}comment with Disqus (JS required)${mastodon ? ', ' : ''} or you can ${email}.</span>
<span class="org-string">${disqusForm || ''}`</span>;}
  });
}
</pre>
</div>


<p>
I included it in my <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/post.cjs">post.cjs</a> shortcode:
</p>


<div class="org-src-container">
<pre class="src src-js">module.exports = eleventyConfig =&gt;
eleventyConfig.addShortcode(<span class="org-string">'post'</span>, <span class="org-keyword">async</span> <span class="org-keyword">function</span>(<span class="org-variable-name">item</span>, <span class="org-variable-name">index</span>, <span class="org-variable-name">includeComments</span>) {
  <span class="org-keyword">let</span> <span class="org-variable-name">comments</span> = <span class="org-string">'&lt;div class="comments"&gt;'</span> + (includeComments ? <span class="org-constant">this</span>.comments(item) : <span class="org-constant">this</span>.comments(item, <span class="org-constant">true</span>)) + <span class="org-string">'&lt;/div&gt;'</span>;
  <span class="org-keyword">let</span> <span class="org-variable-name">categoryList</span> = item.categories || item.data &amp;&amp; item.data.categories;
  <span class="org-keyword">let</span> <span class="org-variable-name">categoriesFooter</span> = <span class="org-string">''</span>, <span class="org-variable-name">categories</span> = <span class="org-string">''</span>;
  <span class="org-keyword">if</span> (categoryList &amp;&amp; categoryList.length &gt; 0) {
    categoriesFooter = <span class="org-string">`&lt;div class="footer-categories"&gt;More posts about ${this.categoryList(categoryList)}&lt;/div&gt;`</span>;
    categories = <span class="org-string">`| &lt;span class="categories"&gt;${this.categoryList(categoryList)}&lt;/span&gt;`</span>;
  }

  <span class="org-keyword">return</span>  <span class="org-string">`&lt;article class="post" id="index${index}" data-url="${item.url || item.permalink || ''}"&gt;</span>
<span class="org-string">&lt;header&gt;&lt;h2 data-pagefind-meta="title"&gt;&lt;a href="${item.url || item.permalink || ''}"&gt;${item.title || item.data &amp;&amp; item.data.title}&lt;/a&gt;&lt;/h2&gt;</span>
<span class="org-string">${this.timeInfo(item)}${categories}</span>
<span class="org-string">&lt;/header&gt;</span>
<span class="org-string">&lt;div class="entry"&gt;</span>
<span class="org-string">${await (item.templateContent || item.layoutContent || item.data?.content || item.content || item.inputContent)}</span>
<span class="org-string">&lt;/div&gt;</span>
<span class="org-string">${comments}</span>
<span class="org-string">${categoriesFooter}</span>
<span class="org-string">&lt;/article&gt;`</span>;
});
</pre>
</div>


<p>
I also included it in my <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/rssItem.cjs">RSS item</a> template to make
it easier for people to send me comments without
having to dig through my website for contact info.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">const</span> <span class="org-variable-name">posthtml</span> = require(<span class="org-string">"posthtml"</span>);
<span class="org-keyword">const</span> <span class="org-variable-name">urls</span> = require(<span class="org-string">"posthtml-urls"</span>);

module.exports = (eleventyConfig) =&gt; {
  eleventyConfig.addAsyncShortcode(<span class="org-string">'rssItem'</span>, <span class="org-keyword">async</span> <span class="org-keyword">function</span>(<span class="org-variable-name">item</span>) {
    <span class="org-keyword">let</span> <span class="org-variable-name">content</span> = item.templateContent.replace(<span class="org-string">/&#45;&#45;/</span>g, <span class="org-string">'&amp;#45;&amp;#45;'</span>);
    <span class="org-keyword">if</span> (<span class="org-constant">this</span>.transformWithHtmlBase) {
      content = <span class="org-keyword">await</span> <span class="org-constant">this</span>.transformWithHtmlBase(content);
    }
    <span class="org-keyword">return</span> <span class="org-string">`&lt;item&gt;</span>
<span class="org-string">    &lt;title&gt;${item.data.title}&lt;/title&gt;</span>
<span class="org-string">    &lt;link&gt;${this.absoluteUrl(item.url, item.data.metadata.url)}&lt;/link&gt;</span>
<span class="org-string">    &lt;dc:creator&gt;&lt;![CDATA[${item.data.metadata.author.name}]]&gt;&lt;/dc:creator&gt;</span>
<span class="org-string">    &lt;pubDate&gt;${item.date.toUTCString()}&lt;/pubDate&gt;</span>
<span class="org-string">    ${item.data.categories?.map((cat) =&gt; `</span>&lt;category&gt;${cat}&lt;/category&gt;<span class="org-string">`).join("\n") || ''}</span>
<span class="org-string">    &lt;guid isPermaLink="false"&gt;${this.guid(item)}&lt;/guid&gt;</span>
<span class="org-string">    &lt;description&gt;&lt;![CDATA[${content}</span>
<span class="org-string">&lt;p&gt;${this.comments(item, true)}&lt;/p&gt;]]&gt;&lt;/description&gt;</span>
<span class="org-string">    &lt;/item&gt;`</span>;
  });
};
</pre>
</div>


<p>
The new workflow I'm trying out seems to be working:
</p>

<ol class="org-ol">
<li>Keep <code>npx eleventy &#45;&#45;serve</code> running in the
background, using <code>.eleventyignore</code> to make
rebuilds reasonably fast.</li>
<li>Export the subtree with <code>C-c e s 1 1</code>, which uses <code>org-export-dispatch</code> to call <code>my-org-11ty-export</code> with the subtree.</li>
<li>After about 10 seconds, use <code>my-org-11ty-copy-just-this-post</code> and verify.</li>
<li>Use <code>my-mastodon-11ty-toot-post</code> to compose a toot. Edit the toot and post it.</li>
<li>Check that the <code>EXPORT_MASTODON</code> property has been set.</li>
<li>Export the subtree again, this time with the front matter.</li>
<li>Publish my whole blog.</li>
</ol>

<p>
Next, I'm thinking of modifying
<code>my-mastodon-11ty-toot-post</code> so that it includes a
list of links to blog posts I might be building on
or responding to, and possibly the handles of
people related to those blog posts or topics.
Hmm&#x2026;
</p>
<div><a href="https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQCB0QP0S3XPJ81EP4CK8XD9" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Ftweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">mastodon.el: Copy toot content as Org Mode</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-content-as-org-mode/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-26T16:31:29Z</updated>
    <published>2025-03-26T16:31:29Z</published>
    <category term="mastodon" />
<category term="org" />
<category term="emacs" />
		<id>https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-content-as-org-mode/</id>
		<content type="html"><![CDATA[<p>
Sometimes I want to copy a toot and include it in
my Org Mode notes, like when I post a thought and
then want to flesh it out into a blog post. This
code defines <code>my-mastodon-org-copy-toot-content</code>,
which converts the toot text to Org Mode format
using Pandoc and puts it in the kill ring so I can
yank it somewhere else.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-toot-at-url</span> (<span class="org-type">&amp;optional</span> url)
  <span class="org-doc">"Return JSON toot object at URL.</span>
<span class="org-doc">If URL is nil, return JSON toot object at point."</span>
  (<span class="org-keyword">if</span> url
      (<span class="org-keyword">let*</span> ((search (format <span class="org-string">"%s/api/v2/search"</span> mastodon-instance-url))
             (params <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"q"</span> . ,url)
                       (<span class="org-string">"resolve"</span> . <span class="org-string">"t"</span>))) <span class="org-comment-delimiter">; </span><span class="org-comment">webfinger</span>
             (response (mastodon-http&#45;&#45;get-json search params <span class="org-builtin">:silent</span>)))
        (car (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">statuses</span> response)))
    (mastodon-toot&#45;&#45;base-toot-or-item-json)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-org-copy-toot-content</span> (<span class="org-type">&amp;optional</span> url)
  <span class="org-doc">"Copy the current toot's content as Org Mode.</span>
<span class="org-doc">Use pandoc to convert.</span>

<span class="org-doc">When called with \\[</span><span class="org-doc"><span class="org-constant">universal-argument</span></span><span class="org-doc">], prompt for a URL."</span>
  (<span class="org-keyword">interactive</span> (list
                (<span class="org-keyword">when</span> current-prefix-arg
                  (read-string <span class="org-string">"URL: "</span>))))

  (<span class="org-keyword">let</span> ((toot (my-mastodon-toot-at-url url)))
    (<span class="org-keyword">with-temp-buffer</span>
      (insert (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">content</span> toot))
      (call-process-region nil nil <span class="org-string">"pandoc"</span> t t nil <span class="org-string">"-f"</span> <span class="org-string">"html"</span> <span class="org-string">"-t"</span> <span class="org-string">"org"</span>)
      (kill-new
       (concat
        (org-link-make-string
         (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> toot)
         (concat <span class="org-string">"@"</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">acct</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">account</span> toot))))
        <span class="org-string">":\n\n#+begin_quote\n"</span>
        (string-trim (buffer-string)) <span class="org-string">"\n#+end_quote\n"</span>))
      (message <span class="org-string">"Copied."</span>))))
</pre>
</div>


<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon-mastodon-el-copy-toot-content-as-org-mode">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-content-as-org-mode/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ9NF4K6V8MY8S6KCWRYCZS3" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fmastodon-el-copy-toot-content-as-org-mode%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">mastodon.el: Collect handles in clipboard (Emacs kill ring)</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/mastodon-el-collect-handles-in-kill-ring/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-26T14:25:40Z</updated>
    <published>2025-03-26T14:25:40Z</published>
    <category term="mastodon" />
<category term="emacs" />
		<id>https://sachachua.com/blog/2025/03/mastodon-el-collect-handles-in-kill-ring/</id>
		<content type="html"><![CDATA[<p>
I sometimes want to thank a bunch of people for
contributing to a Mastodon conversation. The
following code lets me collect handles in a single
kill ring entry by calling it with my point over a
handle or a toot, or with an active region.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-mastodon-handle</span> <span class="org-string">"@sacha@social.sachachua.com"</span>)
(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-copy-handle</span> (<span class="org-type">&amp;optional</span> start-new beg end)
  <span class="org-doc">"Append Mastodon handles to the kill ring.</span>

<span class="org-doc">Use the handle at point or the author of the toot.  If called with a</span>
<span class="org-doc">region, collect all handles in the region.</span>

<span class="org-doc">Append to the current kill if it starts with @. If not, start a new</span>
<span class="org-doc">kill. Call with \\[</span><span class="org-doc"><span class="org-constant">universal-argument</span></span><span class="org-doc">] to always start a new list.</span>

<span class="org-doc">Omit my own handle, as specified in `</span><span class="org-doc"><span class="org-constant">my-mastodon-handle</span></span><span class="org-doc">'."</span>
  (<span class="org-keyword">interactive</span> (list current-prefix-arg
                     (<span class="org-keyword">when</span> (region-active-p) (region-beginning))
                     (<span class="org-keyword">when</span> (region-active-p) (region-end))))
  (<span class="org-keyword">let</span> ((handle
         (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> beg end)
             <span class="org-comment-delimiter">;; </span><span class="org-comment">collect handles in region</span>
             (<span class="org-keyword">save-excursion</span>
               (goto-char beg)
               (<span class="org-keyword">let</span> (list)
                 <span class="org-comment-delimiter">;; </span><span class="org-comment">Collect all handles from the specified region</span>
                 (<span class="org-keyword">while</span> (&lt; (point) end)
                   (<span class="org-keyword">let</span> ((mastodon-handle (get-text-property (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-handle</span>))
                         (button (get-text-property (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">button</span>)))
                     (<span class="org-keyword">cond</span>
                      (mastodon-handle
                       (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> (string-match <span class="org-string">"@"</span> mastodon-handle)
                                  (<span class="org-keyword">or</span> (null my-mastodon-handle)
                                      (not (string= my-mastodon-handle mastodon-handle))))
                         (<span class="org-keyword">cl-pushnew</span>
                          (concat (<span class="org-keyword">if</span> (string-match <span class="org-string">"^@"</span> mastodon-handle) <span class="org-string">""</span>
                                    <span class="org-string">"@"</span>)
                                  mastodon-handle)
                          list
                          <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
                       (goto-char (next-single-property-change (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-handle</span> nil end)))
                      ((<span class="org-keyword">and</span> button (looking-at <span class="org-string">"@"</span>))
                       (<span class="org-keyword">let</span> ((text-start (point))
                             (text-end (<span class="org-keyword">or</span> (next-single-property-change (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">button</span> nil end) end)))
                         (<span class="org-keyword">dolist</span> (h (split-string (buffer-substring-no-properties text-start text-end) <span class="org-string">", \n\t"</span>))
                           (<span class="org-keyword">unless</span> (<span class="org-keyword">and</span> my-mastodon-handle (string= my-mastodon-handle h))
                             (<span class="org-keyword">cl-pushnew</span> h list <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>)))
                         (goto-char text-end)))
                      (t
                       <span class="org-comment-delimiter">;; </span><span class="org-comment">collect authors of toots too</span>
                       (<span class="org-keyword">when-let*</span>
                           ((toot (mastodon-toot&#45;&#45;base-toot-or-item-json))
                            (author (<span class="org-keyword">and</span> toot
                                         (concat <span class="org-string">"@"</span>
                                                 (alist-get
                                                  <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">acct</span>
                                                  (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">account</span> (mastodon-toot&#45;&#45;base-toot-or-item-json)))))))
                         (<span class="org-keyword">unless</span> (<span class="org-keyword">and</span> my-mastodon-handle (string= my-mastodon-handle author))
                           (<span class="org-keyword">cl-pushnew</span>
                            author
                            list
                            <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>)))
                       (goto-char (next-property-change (point) nil end))))))
                 (<span class="org-keyword">setq</span> handle (string-join (seq-uniq list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>) <span class="org-string">" "</span>))))
           (concat <span class="org-string">"@"</span>
                   (<span class="org-keyword">or</span>
                    (get-text-property (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-handle</span>)
                    (alist-get
                     <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">acct</span>
                     (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">account</span> (mastodon-toot&#45;&#45;base-toot-or-item-json))))))))
    (<span class="org-keyword">if</span> (<span class="org-keyword">or</span> start-new (null kill-ring) (not (string-match <span class="org-string">"^@"</span> (car kill-ring))))
        (kill-new handle)
      (<span class="org-keyword">dolist</span> (h (split-string handle <span class="org-string">" "</span>))
        (<span class="org-keyword">unless</span> (member h (split-string <span class="org-string">" "</span> (car kill-ring)))
          (<span class="org-keyword">setf</span> (car kill-ring) (concat (car kill-ring) <span class="org-string">" "</span> h)))))
    (message <span class="org-string">"%s"</span> (car kill-ring))))
</pre>
</div>


<p>
Another perk of tooting from Emacs using <a href="https://codeberg.org/martianh/mastodon.el">mastodon.el</a>. =)
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon-mastodon-el-collect-handles-in-kill-ring">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2025/03/mastodon-el-collect-handles-in-kill-ring/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ9ECZFN6KE1JNWHF680X345" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fmastodon-el-collect-handles-in-kill-ring%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">mastodon.el: Copy toot URL after posting; also, copying just this post with 11ty</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-24T00:41:53Z</updated>
    <published>2025-03-24T00:41:53Z</published>
    <category term="mastodon" />
<category term="emacs" />
<category term="11ty" />
		<id>https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/</id>
		<content type="html"><![CDATA[<p>
I often want to copy the toot URL after posting a
new toot about a blog post so that I can update
the blog post with it. Since I post from Emacs
using <a href="https://codeberg.org/martianh/mastodon.el">mastodon.el</a>, I can probably figure out how
to get the URL after tooting. A quick-and-dirty
way is to retrieve the latest status.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-mastodon-toot-posted-hook</span> nil <span class="org-doc">"Called with the item."</span>)

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-copy-toot-url</span> (toot)
  (<span class="org-keyword">interactive</span> (list (my-mastodon-latest-toot)))
  (kill-new (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> toot)))
(add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-copy-toot-url</span>)

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-latest-toot</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon-http</span>)
  (<span class="org-keyword">let*</span> ((json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
         (json-object-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">alist</span>))
    (car
     (mastodon-http&#45;&#45;get-json
      (mastodon-http&#45;&#45;api
       (format <span class="org-string">"accounts/%s/statuses?count=1&amp;limit=1&amp;exclude_reblogs=t"</span>
               (mastodon-auth&#45;&#45;get-account-id)))
      nil <span class="org-builtin">:silent</span>))))

(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot</span>
  (<span class="org-keyword">when</span> (functionp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot-send</span>)
    (advice-add
     <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot-send</span>
     <span class="org-builtin">:after</span>
     (<span class="org-keyword">lambda</span> (<span class="org-type">&amp;rest</span> _)
       (run-hook-with-args <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> (my-mastodon-latest-toot)))))
  (<span class="org-keyword">when</span> (functionp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;send</span>)
    (advice-add
     <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;send</span>
     <span class="org-builtin">:after</span>
     (<span class="org-keyword">lambda</span> (<span class="org-type">&amp;rest</span> _)
       (run-hook-with-args <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> (my-mastodon-latest-toot))))))
</pre>
</div>


<p>
I considered overriding the keybinding in
<code>mastodon-toot-mode-map</code>, but I figured using
advice would mean I can copy things even after
automated toots.
</p>

<p>
A more elegant way to do this might be to modify
<code>mastodon-toot-send</code> to <code>run-hook-with-args</code> a
variable with the <code>response</code> as an argument, but
this will do for now.
</p>

<p>
I used a hook in my advice so that I can change
the behaviour from other functions. For example, I
have some code to compose a toot with <a href="https://sachachua.com/dotemacs#mastodon-tooting-a-link-to-the-current-post">a link to
the current post</a>. After I send a toot, I want to
check if the toot contains the current entry's
permalink. If it has and I don't have a Mastodon
toot field yet, maybe I can automatically set that
property, assuming I end up back in the Org Mode
file I started it from.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-org-maybe-set-toot-url</span> (toot)
  (<span class="org-keyword">when</span> (derived-mode-p <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-mode</span>)
    (<span class="org-keyword">let</span> ((permalink (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>)))
      (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> permalink
                 (string-match (regexp-quote permalink) (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">content</span> toot))
                 (not (org-entry-get-with-inheritance <span class="org-string">"MASTODON"</span>)))
        (<span class="org-keyword">save-excursion</span>
          (goto-char (org-find-property <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>
                                        permalink))
          (org-entry-put
           (point)
           <span class="org-string">"EXPORT_MASTODON"</span>
           (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> toot))
          (message <span class="org-string">"Toot URL set: %s, republish if needed"</span> toot))))))
(add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-org-maybe-set-toot-url</span>)
</pre>
</div>


<p>
If I combine that with a development copy of my
blog that ignores most of my posts so it compiles
faster and a function that copies just the current
post's files over, I can quickly make a post
available at its permalink (which means the link
in the toot will work) before I recompile the rest
of the blog, which takes a number of minutes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-11ty-copy-just-this-post</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">when</span> (derived-mode-p <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-mode</span>)
    (<span class="org-keyword">let</span> ((file (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_FILE_NAME"</span>))
          (path my-11ty-base-dir))
      (call-process <span class="org-string">"chmod"</span> nil nil nil <span class="org-string">"ugo+rwX"</span> <span class="org-string">"-R"</span> (expand-file-name file (expand-file-name <span class="org-string">"_local"</span> path)))
      (call-process <span class="org-string">"rsync"</span> nil (get-buffer-create <span class="org-string">"*rsync*"</span>) nil <span class="org-string">"-avze"</span> <span class="org-string">"ssh"</span>
                    (expand-file-name file (expand-file-name <span class="org-string">"_local"</span> path))
                    (concat <span class="org-string">"web:/var/www/static-blog/"</span> file))
      (browse-url (concat (replace-regexp-in-string <span class="org-string">"/$"</span> <span class="org-string">""</span> my-blog-base-url)
                          (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>))))))
</pre>
</div>


<p>
The proper blog updates (index page, RSS/ATOM
feeds, category pages, prev/next links, etc.) can happen when the
publishing is finished.
</p>

<p>
So my draft workflow is:
</p>

<ol class="org-ol">
<li>Write the post.</li>
<li>Export it to the local <code>NODE_ENV=dev npx eleventy &#45;&#45;serve &#45;&#45;quiet</code> with <a href="https://github.com/sachac/ox-11ty">ox-11ty</a>.</li>
<li>Check that it looks okay locally.</li>
<li>Use <code>my-org-11ty-copy-just-this-post</code> and confirm that it looks fine.</li>
<li>Compose a toot with <a href="https://sachachua.com/dotemacs#mastodon-tooting-a-link-to-the-current-post">my-mastodon-11ty-toot-post</a>
and check if sending it updates the Mastodon
toot.</li>
<li>Re-export the post.</li>
<li>Run my blog publishing process. <code>NODE_ENV=production npx eleventy &#45;&#45;quiet</code> and then rsync.</li>
</ol>

<p>
Let's see if this works&#x2026;
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ2TC154Z4YXG35A52NQPKG8" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fmastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Archiving public toots on my blog</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2024/09/archiving-public-toots-on-my-blog/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2024-09-18T14:40:22Z</updated>
    <published>2024-09-18T14:40:22Z</published>
    <category term="mastodon" />
<category term="emacs" />
<category term="org" />
		<id>https://sachachua.com/blog/2024/09/archiving-public-toots-on-my-blog/</id>
		<content type="html"><![CDATA[<p>
I want to compile my global microblog posts into weekly posts so that they're archived on my blog.
It might make sense to make them list items so that I can move them around easily.
</p>

<p>
</p><details><summary>my-mastodon-insert-my-statuses-since</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-insert-my-statuses-since</span> (date)
  (<span class="org-keyword">interactive</span> (list (org-read-date <span class="org-string">"Since date: "</span>)))
  (insert
   (format <span class="org-string">"#+begin_toot_archive\n%s\n#+end_toot_archive\n"</span>
           (mapconcat
            (<span class="org-keyword">lambda</span> (o)
              (format <span class="org-string">"- %s\n  #+begin_quote\n  #+begin_export html\n%s\n  #+end_export\n  #+end_quote\n\n"</span>
                      (org-link-make-string (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> o) (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> o))
                      (org-ascii&#45;&#45;indent-string (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">content</span> o) 2))
              <span class="org-comment-delimiter">;; </span><span class="org-comment">(format "#+begin_quote\n#+begin_export html\n%s\n#+end_export\n#+end_quote\n\n%s\n\n"</span>
              <span class="org-comment-delimiter">;; </span><span class="org-comment">       (assoc-default 'content o)</span>
              <span class="org-comment-delimiter">;; </span><span class="org-comment">       (org-link-make-string (assoc-default 'url o) (assoc-default 'created_at o)))</span>
              )
            (seq-filter
             (<span class="org-keyword">lambda</span> (o)
               (string= (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">visibility</span> o) <span class="org-string">"public"</span>))
             (my-mastodon-fetch-posts-after
              (format <span class="org-string">"accounts/%s/statuses?count=40&amp;exclude_reblogs=t&amp;exclude_replies=t"</span> (mastodon-auth&#45;&#45;get-account-id))
              date))
            <span class="org-string">""</span>))))
</pre></div></details>
<p></p>

<p>
Here's a little thing I used to convert a two-level list into my collapsible sections:
</p>

<p>
</p><details><summary>my-org-convert-list-to-collapsible-details</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-convert-list-to-collapsible-details</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">let</span> ((list (org-list-to-lisp t)))
    (mapc (<span class="org-keyword">lambda</span> (o)
            (<span class="org-keyword">when</span> (stringp (car o))
              (insert
               (format
                <span class="org-string">"#+begin_my_details %s :open t\n%s#+end_my_details\n"</span>
                (car o)
                (mapconcat
                 (<span class="org-keyword">lambda</span> (s)
                   (concat <span class="org-string">"- "</span> (string-trim (org-ascii&#45;&#45;indent-string (car s) 2)) <span class="org-string">"\n"</span>))
                 (cdr (cadr o)))))))
          (cdr list))))
</pre></div></details>
<p></p>

<p>
And here are my toots from the past week, roughly categorized into collapsible sections:
</p>

<div class="toot_archive" id="org0aadb99">
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>EmacsConf</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J80ZQSXSCQT6P9C27FX7FZJF">2024-09-17T21:55:39.065Z</a> CFP, draft schedule
</p>
<blockquote>
<p>The <a href="https://social.sachachua.com/tags/emacsconf" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>EmacsConf</span></a> call for proposals (<a href="https://emacsconf.org/2024/cfp/" rel="nofollow noreferrer noopener" target="_blank">https://emacsconf.org/2024/cfp/</a>) target date is this Friday (Sept 20), so I've started drafting the schedule. Thanks to my SVG schedule visualization code and function for checking availability constraints from last year ( <a href="https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/" rel="nofollow noreferrer noopener" target="_blank">https://sachachua.com/blog/2023/09/emacsconf-backstage-scheduling-with-svgs/</a>), it took only about an hour to sketch this out: <a href="https://emacsconf.org/2024/organizers-notebook/#draft-schedule" rel="nofollow noreferrer noopener" target="_blank">https://emacsconf.org/2024/organizers-notebook/#draft-schedule</a> . We can run two tracks simultaneously and I can also slightly reduce the buffer between talks, so there's plenty of space for more <a href="https://social.sachachua.com/tags/emacs" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>Emacs</span></a> talks if people want to propose them or nudge people to propose them. =)</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7VAF3QFY1M0EYNAZRR5D47A">2024-09-15T17:07:41.935Z</a> diversity
</p>
<blockquote>
<p>I feel complicated feelings about <a href="https://social.sachachua.com/tags/emacsconf" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>EmacsConf</span></a> and diversity. On one hand, yes, I would love to have a mix of speakers that reflects the mix of interesting stories and people I come across in the <a href="https://social.sachachua.com/tags/emacs" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>Emacs</span></a> community. (I wouldn't get rid of or discourage anyone; I just want more! :) )<br><br>On the other hand, preparing and giving a presentation is a lot of work, and I have first-hand appreciation of how difficult it can be to find time to think - much less predict a specific time to have a conversation. (I'm only just beginning to be able to have some thinking time that isn't accompanied by the guilt of letting my kiddo binge-watch YouTube videos or the uncertainties of sacrificing my sleep, and I still rarely schedule anything for myself.)<br><br>In addition, there are little risks that other people might not even have on their radar. All it takes is one person developing a parasocial relationship or fixation, or someone getting grumpy about someone's pronouns or personal characteristics or opinions, and then deciding to go and ruin someone's day (or life)... I'd hate to encourage someone to put themselves out there and end up with that happening to them, even if it's not at all their fault or mine.<br><br>So yeah, it's a little hard for me to reach out. I can deal with impostor syndrome making people feel like they might not have much to say (share what you're learning! We're all figuring things out together), but I'm not so sure about the other concerns. While I'd like to think that in the Emacs community we often have a convivial atmosphere, sometimes it gets weird here too.<br><br>I'm not sure what to do here aside from thinking out loud. I wish I could wave a magic wand and solve some structural issues that could make things more equitable, but that's waaay above my paygrade. I can keep working on figuring out how to make use of fragmented time, and maybe that will help other people too. I like working on the captions for EmacsConf; they help me a lot, too. I can experiment with workflows for sharing what I'm learning in a way that doesn't require a lot of focus time, speech fluency (I occasionally stutter and have to redo things), or a powerful computer. (Emacs is totally my nonlinear video editor.) I can make an indirect request for more people to consider proposing a talk for <a href="https://emacsconf.org/2024/cfp/" rel="nofollow noreferrer noopener" target="_blank">https://emacsconf.org/2024/cfp/</a> (target date is Sept 20, but I think the other organizers are considering extending it too), even with all the caveats my anxious brain suggests. (I know, I'm terrible at sales. :) ) And really, EmacsConf isn't important in the grand scheme of things, it's just a fun excuse to get together and connect with other people who like this stuff too. :)<br><br>I wonder how this can be better. Thoughts?</p>
</blockquote></li>
</ul>


</details>
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>Emacs</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J808CSQ1BJ5MXATADDNB6W8W">2024-09-17T15:07:41.153Z</a> consult-omni
</p>
<blockquote>
<p>All right, I just got consult-omni and a Google custom search JSON API key set up so that I can call consult-omni-google, type keywords, pick the correct match, and insert it as an Org Mode link (or linkify the current region). I can think of more tweaks (embark-act on the current word or region to linkify it), but this is already pretty neat.</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7XJDFTQ817142MBEVN33HT4">2024-09-16T14:05:06.263Z</a> - user-init-file
</p>
<blockquote>
<p>Is there already an interactive <a href="https://social.sachachua.com/tags/emacs" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>emacs</span></a> command for opening user-init-file? I think that could be handy for newbies if we could just tell them to use "M-x visit-user-init-file" or even "Select 'Open init file' from the menu", although I suppose by the time we ask them to fiddle with the init file to add stuff to it, it's fine to encourage them to be comfortable with C-h v user-init-file and then maybe even teach them about M-x ffap at that point. Hmm...</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7XHM16VWF2VF1D866P6RQYM">2024-09-16T13:51:12.091Z</a> - casual-symbol-overlay
</p>
<blockquote>
<p>Trying out casual-symbol-overlay (<a href="http://yummymelon.com/devnull/announcing-casual-symbol-overlay.html" rel="nofollow noreferrer noopener" target="_blank">http://yummymelon.com/devnull/announcing-casual-symbol-overlay.html</a>) by hooking it into embark-act, which I've bound to `C-.`:<br><br>```emacs-lisp<br>(use-package casual-symbol-overlay<br>:if my-laptop-p<br>:init<br>(with-eval-after-load 'embark<br>(keymap-set embark-symbol-map "z" #'casual-symbol-overlay-tmenu)))<br>```</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7H147EFB9M7X8M70RC0D74W">2024-09-11T17:12:03.791Z</a> no nested lists for Org Babel
</p>
<blockquote>
<p>TIL that <a href="https://social.sachachua.com/tags/orgmode" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>OrgMode</span></a> Babel only takes the top level of nested lists passed in via :var (<a href="https://orgmode.org/manual/Environment-of-a-Code-Block.html" rel="nofollow noreferrer noopener" target="_blank">https://orgmode.org/manual/Environment-of-a-Code-Block.html</a> - Note that only the top-level list items are passed along. Nested list items are ignored.) When I try the manual example on my computer, I do indeed get only the top-level list items, unlike the nested data from <a href="https://mail.gnu.org/archive/html/emacs-orgmode/2020-10/msg00536.html" rel="nofollow noreferrer noopener" target="_blank">https://mail.gnu.org/archive/html/emacs-orgmode/2020-10/msg00536.html</a> . Of course, now that makes me want nested lists for both input and output...</p>
</blockquote></li>
</ul>


</details>
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>Mastodon</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7XW48X4R4DK5KJQMM0GJKZB">2024-09-16T16:54:50.020Z</a> access token generator
</p>
<blockquote>
<p>Both <a href="https://github.com/edsu/feediverse" rel="nofollow noreferrer noopener" target="_blank">https://github.com/edsu/feediverse</a> and <a href="https://github.com/Wikinaut/better_feediverse" rel="nofollow noreferrer noopener" target="_blank">https://github.com/Wikinaut/better_feediverse</a> gave me "Invalid user name, password, or redirect_uris: ('Mastodon API returned error', 400, 'Bad Request', 'unsupported_grant_type')" when I tried to set it up with my GoToSocial instance. Fortunately, this access token generator worked: <a href="https://takahashim.github.io/mastodon-access-token/" rel="nofollow noreferrer noopener" target="_blank">https://takahashim.github.io/mastodon-access-token/</a></p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7XWKD0QYEBE7W5151Q8JQRV">2024-09-16T17:03:05.751Z</a> feediverse
</p>
<blockquote>
<p>To get feediverse working, I also needed to edit my feediverse.py to add `version_check_mode='none'` to the Mastodon initialization, after `access_token`.</p>
</blockquote></li>
</ul>


</details>
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>Moving to P52</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J81719GXH35E3CV07CZQKK20">2024-09-18T00:03:09.981Z</a> WhisperX
</p>
<blockquote>
<p>Now that I have word-level timestamps from WhisperX (<a href="https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/" rel="nofollow noreferrer noopener" target="_blank">https://sachachua.com/blog/2024/09/using-whisperx-to-get-word-level-timestamps-for-audio-editing-with-emacs-and-subed-record/</a>), I think I'll be able to write an elisp equivalent of the merging/splitting strategies of <a href="https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-words" rel="nofollow noreferrer noopener" target="_blank">https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-words</a> to merge subtitles considering gap, duration, length, and maximum number of words.</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7VAG6PXK90AJPXEGZ6SJ36M">2024-09-15T17:08:17.757Z</a> upgrade
</p>
<blockquote>
<p>(reposting, forgot to make it public)<br><br>I installed a 2TB Crucial T500 NVMe into my Lenovo P52 so that I can try dual-booting into Linux, since it was hard to figure out how I could get all my usual conveniences in WSL.<br><br>A preliminary test with a fresh Kubuntu install showed that my 11ty static blog generation takes about the same time as it does on the X230T, which is a little surprising considering the newer processor and the faster SSD, but maybe I'll have to look for speed gains elsewhere there. I think whisper.cpp is a lot more usable on this computer though, so I'm looking forward to taking advantage of that. The P52 might also make video editing possible, and it might support more modern monitors. It is a fair bit larger and heavier, though. I might end up still using both.<br><br>Anyway, I decided to redo the install by cloning my previous SSD. I want to see if I can skip the step of setting all those things up (although I'll need to redo the Syncthing config, of course). I don't have the extra parts that would let me install the 2.5" SSD from my X230T directly into the P52, but W- has a drive dock that works off USB 2.0. Slow and steady, but that's fine, I can run things overnight. I woke up today to find out that dd doesn't handle extended partitions and needs me to dd them one by one. That's cool, I'll just have that running in the background today.<br><br>If the clone doesn't work or if it's too much trouble to take the clone and give it its own identity, I'll probably wipe it and do another install. Since the X230T is on Kubuntu, I think I'll keep it on Kubuntu as well, to minimize the things I need to keep in my head as I switch between computers. My home directory is in a separate partition, so I can keep it if I want to try something different.<br><br>Now I just have to wait a few hours for these dd commands...</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7GPZWBJ6YRRFMY71B3X6PA7">2024-09-11T14:14:55.602Z</a> static blog
</p>
<blockquote>
<p>My "plugins is not iterable" issue got fixed when I downgraded `@11ty/eleventy` from `@beta` to `@2.0.1`. Yay, that's one thing off my list!</p>
</blockquote></li>
</ul>


</details>
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>Other tech stuff</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J82HF82RFMK50C3ADTBC6J89">2024-09-18T12:24:47.448Z</a> Syncthing send only
</p>
<blockquote>
<p>I noticed that some of my <a href="https://social.sachachua.com/tags/syncthing" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>Syncthing</span></a> folders were in Send Only mode, but I needed Send & Receive so that my Orgzly Revived changes would propagate back to my laptop. It turned out to be related to <a href="https://forum.syncthing.net/t/syncthing-readonly-access-on-storage/21459/2" rel="nofollow noreferrer noopener" target="_blank">https://forum.syncthing.net/t/syncthing-readonly-access-on-storage/21459/2</a> . All I needed was to use the Syncthing web interface on my phone to edit the advanced options for those folders and set them back to Send & Receive.</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7YBC9WKR45KVF0CS4HJAYT3">2024-09-16T21:21:21.811Z</a> Rails
</p>
<blockquote>
<p>Finally decided to invest some time into dusting off a Rails-based project and bringing it into modern times. I'm going to do step-wise upgrades from Ruby 2.1.10 and Rails 4.1.7 following <a href="https://www.fastruby.io/blog/rails/upgrade/rails-upgrade-series.html" rel="nofollow noreferrer noopener" target="_blank">https://www.fastruby.io/blog/rails/upgrade/rails-upgrade-series.html</a> and <a href="https://www.fastruby.io/blog/ruby/rails/versions/compatibility-table.html" rel="nofollow noreferrer noopener" target="_blank">https://www.fastruby.io/blog/ruby/rails/versions/compatibility-table.html</a> . Thiiiis tempted to just ditch everything and start a Node/React/SQLite thing that I can import my data into instead.</p>
</blockquote></li>
</ul>


</details>
<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;" open="">
                  <summary><strong>Parenting</strong></summary>
<ul class="org-ul">
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7ZZ99GT3Q1J166PKCXFE8NF">2024-09-17T12:28:29.082Z</a> emotion check-in
</p>
<blockquote>
<p>I appreciate my kiddo's grade 3 teacher. =) She's currently doing the morning check-in of emotions (how's everyone feeling) using 9 images of Grogu with different facial expressions, which gets the kids (1) laughing, (2) interpreting facial expressions that aren't explicitly labeled, and (3) figuring out what they're feeling.</p>
</blockquote></li>
<li><p>
<a href="https://social.sachachua.com/@sacha/statuses/01J7K4H54Y4P7MR1BXW2BSH1F4">2024-09-12T12:50:02.014Z</a> pull system
</p>
<blockquote>
<p>The kiddo is 8 and I'm developing a better understanding of what "fiercely independent" means. One of the things I'm working on learning is how to shut up and trust the process. =) I've started thinking of it like the pull system of Lean manufacturing principles. Things work out better when I wait for her to ask a question (to pull from me) because at that point, she's ready to hear the answer.</p>
</blockquote></li>
</ul>


</details>

</div>

<p>
As it turns out, <code>org-list-to-org</code> uses the Org export mechanism, so it quietly discards things like <code>#+begin_export html</code> blocks. I decided to hard-code assumptions about the list's structure instead, which works for now.
</p>
<div><a href="https://sachachua.com/blog/2024/09/archiving-public-toots-on-my-blog/index.org">View org source for this post</a></div><p>You can <a href="https://sachachua.com/blog/2024/09/archiving-public-toots-on-my-blog/#comment">view 1 comment</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F09%2Farchiving-public-toots-on-my-blog%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Collecting Emacs News from Mastodon</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2024/09/collecting-emacs-news-from-mastodon/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-06T18:13:39Z</updated>
    <published>2024-09-16T17:07:16Z</published>
    <category term="emacs" />
<category term="mastodon" />
		<id>https://sachachua.com/blog/2024/09/collecting-emacs-news-from-mastodon/</id>
		<content type="html"><![CDATA[<div class="update" id="org6bcfdd5">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2025-03-06 Thu]</span></span>: Use <code>my-org-link-url-from-string</code>.
</p>

</div>

<p>
One of the things I like about browsing Mastodon in Emacs using
<a href="https://codeberg.org/martianh/mastodon.el">mastodon.el</a> is that I can modify my workflow to make things easier.
For example, I often come across links that I'd like to save for Emacs
News. I want to boost the post and save it to an Org file, and I can
do that with a single keystroke. It uses the <code>my-mastodon-store-link</code> function
defined elsewhere in <a href="https://sachachua.com/dotemacs/#mastodon">my config</a>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">use-package</span> org
  <span class="org-builtin">:config</span>
  (add-to-list
   <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-capture-templates</span>
   <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"&#128240;"</span> <span class="org-string">"Emacs News"</span> entry (file+headline <span class="org-string">"~/sync/orgzly/news.org"</span> <span class="org-string">"Collect Emacs News"</span>)
     <span class="org-string">"* %a  :news:</span>

<span class="org-string">#+begin_quote</span>
<span class="org-string">%:text</span>
<span class="org-string">#+end_quote</span>

<span class="org-string">"</span>
     <span class="org-builtin">:prepend</span> t <span class="org-builtin">:immediate-finish</span> t)))
(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-save-toot-for-emacs-news</span> ()
  (<span class="org-keyword">interactive</span>)
  <span class="org-comment-delimiter">;; </span><span class="org-comment">boost if not already boosted</span>
  (<span class="org-keyword">unless</span> (get-text-property
           (car
            (mastodon-tl&#45;&#45;find-property-range <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">byline</span> (point)))
           <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">boosted-p</span>)
    (mastodon-toot&#45;&#45;toggle-boost-or-favourite <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">boost</span>))
  <span class="org-comment-delimiter">;; </span><span class="org-comment">store a link and capture the note</span>
  (org-capture nil <span class="org-string">"&#128240;"</span>))

(<span class="org-keyword">use-package</span> mastodon
  <span class="org-builtin">:bind</span> (<span class="org-builtin">:map</span> mastodon-mode-map (<span class="org-string">"w"</span> . my-mastodon-save-toot-for-emacs-news)))
</pre>
</div>


<p>
This puts a bunch of notes in my
<code>~/sync/orgzly/news.org</code> file. Then I can use
<code>my-emacs-news-summarize-mastodon-items</code> to
summarize a bunch of items I've captured from
Mastodon, taking the title from the first link and
including a link to the toot using the author's
handle. This is what it looks like:
</p>


<figure id="orgeee5779">
<img src="https://sachachua.com/blog/2024/09/collecting-emacs-news-from-mastodon/output-2024-09-16-13:14:38.gif" alt="output-2024-09-16-13:14:38.gif">

<figcaption><span class="figure-number">Figure 1: </span>Quick screencast summarizing Mastodon toots</figcaption>
</figure>

<p>
Here's the code that makes that happen:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacs-news-summarize-mastodon-items</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">while</span> (not (eobp))
    (<span class="org-keyword">let*</span> ((info (my-mastodon-get-note-info))
           (title (<span class="org-keyword">when</span> (car (plist-get info <span class="org-builtin">:links</span>))
                    (my-page-title (car (plist-get info <span class="org-builtin">:links</span>)))))
           (summary (read-string
                     (<span class="org-keyword">if</span> title
                         (format <span class="org-string">"Summary (%s): "</span> title)
                       <span class="org-string">"Summary: "</span>)
                     title)))
      (org-cut-subtree)
      (<span class="org-keyword">unless</span> (string= summary <span class="org-string">""</span>)
        (insert <span class="org-string">"- "</span> (org-link-make-string
                      (<span class="org-keyword">or</span> (car (plist-get info <span class="org-builtin">:links</span>))
                          (plist-get info <span class="org-builtin">:url</span>))
                      summary)
                (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> (car (plist-get info <span class="org-builtin">:links</span>))
                         (plist-get info <span class="org-builtin">:handle</span>))
                    (concat <span class="org-string">" ("</span> (org-link-make-string (plist-get info <span class="org-builtin">:url</span>)
                                                       (plist-get info <span class="org-builtin">:handle</span>))
                            <span class="org-string">")"</span>)
                  <span class="org-string">""</span>)
                <span class="org-string">"\n"</span>)))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-match-groups</span> (<span class="org-type">&amp;optional</span> object)
  <span class="org-doc">"Return the matching groups, good for debugging regexps."</span>
  (seq-map-indexed (<span class="org-keyword">lambda</span> (entry i)
                     (list i entry
                           (<span class="org-keyword">and</span> (car entry)
                                (<span class="org-keyword">if</span> object
                                    (substring object (car entry) (cadr entry))
                                  (buffer-substring (car entry) (cadr entry))))))
                   (seq-partition
                    (match-data t)
                    2)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-link-url-from-string</span> (s)
  <span class="org-doc">"Return the link URL from S."</span>
  (<span class="org-keyword">if</span> (string-match org-link-any-re s)
      (<span class="org-keyword">or</span>
       (match-string 7 s)
       (match-string 2 s))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-get-note-info</span> ()
  <span class="org-doc">"Return (:handle ... :url ... :links ... :text) for the current subtree."</span>
  (<span class="org-keyword">let</span> ((url (<span class="org-keyword">let</span> ((title (org-entry-get (point) <span class="org-string">"ITEM"</span>)))
               (<span class="org-keyword">if</span> (string-match org-link-any-re title)
                   (<span class="org-keyword">or</span>
                    (match-string 7 title)
                    (match-string 2 title)))))
        beg end
        handle)
    (<span class="org-keyword">save-excursion</span>
      (org-back-to-heading)
      (org-end-of-meta-data)
      (<span class="org-keyword">setq</span> beg (point))
      (<span class="org-keyword">setq</span> end (org-end-of-subtree))
      (<span class="org-keyword">cond</span>
       ((string-match <span class="org-string">"\\[\\[https://bsky\\.app/.+?\\]\\[</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">\\]\\]"</span> url)
        (<span class="org-keyword">setq</span> handle (match-string 1 url)))
       ((string-match <span class="org-string">"https://</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">/</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">@.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">/"</span> url)
        (<span class="org-keyword">setq</span> handle (concat
                      (match-string 2 url) <span class="org-string">"@"</span> (match-string 1 url))))
       ((string-match <span class="org-string">"https://</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">/</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">/p/[0-9]+\\.[0-9]+"</span> url)
        (<span class="org-keyword">setq</span> handle (concat
                      <span class="org-string">"@"</span> (match-string 2 url) <span class="org-string">"@"</span> (match-string 1 url)))))
      (list
       <span class="org-builtin">:handle</span> handle
       <span class="org-builtin">:url</span> (<span class="org-keyword">if</span> (string-match org-link-bracket-re url) (match-string 1 url) url)
       <span class="org-builtin">:links</span> (reverse (mapcar (<span class="org-keyword">lambda</span> (o) (org-element-property <span class="org-builtin">:raw-link</span> o))
                               (my-org-get-links-in-region beg end)))
       <span class="org-builtin">:text</span> (string-trim (buffer-substring-no-properties beg end))))))

(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">my-mastodon-get-note-info</span> ()
 (<span class="org-keyword">should</span>
  (equal
   (<span class="org-keyword">with-temp-buffer</span>
     (insert <span class="org-string">"** SOMEDAY https://mastodon.online/@jcastp/111762105597746747         :news:</span>
<span class="org-string">:PROPERTIES:</span>
<span class="org-string">:CREATED:  [2024-01-22 Mon 05:51]</span>
<span class="org-string">:END:</span>

<span class="org-string">jcastp@mastodon.online - I've shared my emacs config: https://codeberg.org/jcastp/emacs.d</span>

<span class="org-string">After years of reading other's configs, copying really useful snippets, and tinkering a little bit myself, I wanted to give something back, although I'm still an amateur (and it shows, but I want to improve!)</span>

<span class="org-string">If you can find there something you can use, then I'm happy to be useful to the community.</span>

<span class="org-string">#emacs</span>
<span class="org-string">"</span>)
     (org-mode)
     (my-mastodon-get-note-info))
   <span class="org-highlight-quoted-quote">'</span>(<span class="org-builtin">:handle</span> <span class="org-string">"@jcastp@mastodon.online"</span>
             <span class="org-builtin">:url</span>
             <span class="org-string">"https://mastodon.online/@jcastp/111762105597746747"</span>
             <span class="org-builtin">:links</span>
             (<span class="org-string">"https://codeberg.org/jcastp/emacs.d"</span>)
             <span class="org-builtin">:text</span>
             <span class="org-string">"jcastp@mastodon.online - I've shared my emacs config: https://codeberg.org/jcastp/emacs.d\n\nAfter years of reading other's configs, copying really useful snippets, and tinkering a little bit myself, I wanted to give something back, although I'm still an amateur (and it shows, but I want to improve!)\n\nIf you can find there something you can use, then I'm happy to be useful to the community.\n\n#emacs"</span>))))
</pre>
</div>


<p>
It turns headings into something like this:
</p>

<ul class="org-ul">
<li><a href="https://gitlab.di.unimi.it/-/snippets/4">Insert a screenshot image in latex-mode with the help of org-download</a> (<a href="https://mathstodon.xyz/@mmonga/113118159165252806">@mmonga@mathstodon.xyz</a>)</li>
</ul>

<p>
which I can then copy into my Emacs News Org Mode file and categorize with some keyboard shortcuts.
</p>

<p>
This works particularly well with <a href="https://sachachua.com/dotemacs#mastodon-combined-timeline">my combined Mastodon timelines</a>, because then I can look through all the #emacs posts from mastodon.social, emacs.ch, and social.sachachua.com in one go.
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon-news">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2024/09/collecting-emacs-news-from-mastodon/index.org">View org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F09%2Fcollecting-emacs-news-from-mastodon%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Combining Mastodon timelines using mastodon.el</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2024/09/combining-mastodon-timelines-using-mastodon-el/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-09-15T14:06:53Z</updated>
    <published>2024-09-13T16:55:25Z</published>
    <category term="emacs" />
<category term="mastodon" />
		<id>https://sachachua.com/blog/2024/09/combining-mastodon-timelines-using-mastodon-el/</id>
		<content type="html"><![CDATA[<div class="update" id="org0b86c0e">
<ul class="org-ul">
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2025-09-15">[2025-09-15 Mon]</time></span>: Removed <code>edited_at</code> attribute.</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2025-03-24">[2025-03-24 Mon]</time></span>: Updated <code>my-mastodon-follow-user</code> to use <code>alist-get</code>.</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2024-11-04">[2024-11-04 Mon]</time></span>: Make tag a parameter.</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2024-10-07">[2024-10-07 Mon]</time></span>: Added screenshot.</li>
<li><span class="timestamp-wrapper"><time class="timestamp" datetime="2024-09-16">[2024-09-16 Mon]</time></span>: Read JSON arrays as lists to be compatible with the latest mastodon.el.</li>
</ul>

</div>

<p data-link="t">
.<img src="https://sachachua.com/blog/2024/09/combining-mastodon-timelines-using-mastodon-el/2024-10-07-10-37-39.svg" alt="2024-10-07-10-37-39.svg" class="org-svg" data-link="t">
</p>

<p>
I like checking out the #emacs hashtag when I put together Emacs News. In the past, I usually browsed the hashtag timeline on emacs.ch, which also picked up updates from other people that emacs.ch was following. Now that <a href="https://sachachua.com/blog/2024/09/moving-from-sachac-emacs-ch-to-sacha-social-sachachua-com/">I've moved to @sacha@social.sachachua.com</a> and <a href="https://emacs.ch/@emacs/113087752901949391">emacs.ch is winding down</a>, I wanted to see if there was a way for me to see a combined view using <a href="https://mastodon.social/api/v1/timelines/tag/emacs?count=40">mastodon.social's API feed</a> (paging by
<code>max_id</code> as needed). I haven't enabled public
timeline feeds on my server, so I also need to reuse the OAuth mechanics from mastodon.el.
</p>

<p>
First, let's start by making a unified timeline. By digging around in <code>mastodon-tl.el</code>, I found that I could easily create a timeline view by passing it a vector of toot JSONs.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-fetch-posts-after</span> (base-url after-date)
  <span class="org-doc">"Page backwards through BASE-URL using max_id for all the posts after AFTER-DATE."</span>
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">plz</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon-http</span>)
  (<span class="org-keyword">let</span> ((results [])
        (url base-url)
        (use-mastodon-el (not (string-match <span class="org-string">"^http"</span> base-url)))
        (json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
        page filtered)
    (<span class="org-keyword">while</span> url
      (<span class="org-keyword">setq</span> page (<span class="org-keyword">if</span> use-mastodon-el
                     (mastodon-http&#45;&#45;get-json (mastodon-http&#45;&#45;api url) nil <span class="org-builtin">:silent</span>)
                   (seq-map (<span class="org-keyword">lambda</span> (o)
                              (cons (cons <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">external</span> t) o))
                            (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> url <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>)))
            filtered (seq-filter (<span class="org-keyword">lambda</span> (o) (string&lt; after-date (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> o)))
                                 page))
      (<span class="org-keyword">if</span> filtered
          (<span class="org-keyword">progn</span>
            (<span class="org-keyword">setq</span> results (seq-concatenate <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">vector</span> filtered results)
                  url (concat base-url (<span class="org-keyword">if</span> (string-match <span class="org-string">"\\?"</span> base-url) <span class="org-string">"&amp;"</span> <span class="org-string">"?"</span>)
                              <span class="org-string">"max_id="</span> (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> (elt (last page) 0))))
            (message <span class="org-string">"%s %s"</span> (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> (elt (last page) 0)) url))
        (<span class="org-keyword">setq</span> url nil)))
    results))

(<span class="org-keyword">when</span> (functionp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">memoize</span>)
  (<span class="org-keyword">unless</span> (get <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-fetch-posts-after</span> <span class="org-builtin">:memoize-original-function</span>)
    (memoize <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-fetch-posts-after</span>)))
(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-combined-tag-timeline</span> (later-than tag <span class="org-type">&amp;optional</span> servers)
  <span class="org-doc">"Display items after LATER-THAN about TAG from SERVERS and the current mastodon.el account."</span>
  (<span class="org-keyword">interactive</span> (list
                (org-read-date nil nil nil nil nil <span class="org-string">"-Mon"</span>)
                <span class="org-string">"#emacs"</span>
                <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"mastodon.social"</span> <span class="org-string">"fosstodon.org"</span>)))
  (<span class="org-keyword">setq</span> servers (<span class="org-keyword">or</span> servers <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"mastodon.social"</span> <span class="org-string">"fosstodon.org"</span>)))
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon-tl</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon-toot</span>)
  (<span class="org-keyword">if</span> (stringp later-than)
      (<span class="org-keyword">setq</span> later-than (org-read-date nil nil later-than)))
  (<span class="org-keyword">setq</span> tag (replace-regexp-in-string <span class="org-string">"#"</span> <span class="org-string">""</span> tag))
  (<span class="org-keyword">let*</span> ((limit 40)
         (sources (cons (format <span class="org-string">"timelines/tag/%s?limit=%d"</span> tag limit)
                        (mapcar (<span class="org-keyword">lambda</span> (s)
                                  (format <span class="org-string">"https://%s/api/v1/timelines/tag/%s?limit=%d"</span> s tag limit))
                                servers)))
         (combined
          (seq-map
           <span class="org-comment-delimiter">;; </span><span class="org-comment">remove edited_at</span>
           (<span class="org-keyword">lambda</span> (o) (assoc-delete-all <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">edited_at</span> o))
           (sort
            (seq-reduce (<span class="org-keyword">lambda</span> (prev val)
                          (seq-union prev
                                     (<span class="org-keyword">condition-case</span> nil
                                         (my-mastodon-fetch-posts-after val later-than)
                                       (<span class="org-warning">error</span> nil))
                                     (<span class="org-keyword">lambda</span> (a b) (string= (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">uri</span> a)
                                                            (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">uri</span> b)))))
                        sources [])
            (<span class="org-keyword">lambda</span> (a b)
              (string&lt; (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> b)
                       (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> a)))))))
    (<span class="org-keyword">with-current-buffer</span> (get-buffer-create <span class="org-string">"*Combined*"</span>)
      (<span class="org-keyword">let</span> ((inhibit-read-only t))
        (erase-buffer)
        (mastodon-tl&#45;&#45;timeline combined)
        (mastodon-mode))
      (<span class="org-keyword">setq</span> mastodon-tl&#45;&#45;buffer-spec <span class="org-highlight-quoted-quote">`</span>(account ,(cons mastodon-active-user mastodon-instance-url) buffer-name ,(buffer-name)))
      (display-buffer (current-buffer)))))
</code></pre>
</div>


<p>
The tricky thing is that boosting and replying in
mastodon.el both use the toot IDs instead of the
toot URLs, so they only work for toots that came
in via my current mastodon.el account. Toots from
other timelines might not have been fetched by my
server yet. Adding an <code>external</code> property lets me
find that in the <code>item_json</code> text property in the
timeline buffer. For those toots, I can use
<code>(mastodon-url-lookup (mastodon-toot&#45;&#45;toot-url))</code>
to open the toot in a new buffer that does allow
boosting or replying, which is probably enough for
my purposes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-lookup-toot</span> ()
  (<span class="org-keyword">interactive</span>)
  (mastodon-url-lookup (mastodon-toot&#45;&#45;toot-url)))
</code></pre>
</div>


<p>
When I go through Emacs News, I have a shortcut
that boosts a post and saves it to as an Org Mode
capture with a link to the toot. I sometimes want
to reply, too. So I just need to intervene before
boosting and replying. Boosting and favoriting
both use <code>mastodon-toot&#45;&#45;action</code>, which looks up
the <code>base-item-id</code> text property. Replying looks
up the <code>item-json</code> property and gets the <code>id</code> from
it.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-text-property-update-at-point</span> (pos prop value)
  (<span class="org-keyword">let</span> ((start (previous-single-property-change (<span class="org-keyword">or</span> pos (point)) prop))
        (end (next-single-property-change (<span class="org-keyword">or</span> pos (point)) prop)))
    (put-text-property (<span class="org-keyword">or</span> start (point-min))
                       (<span class="org-keyword">or</span> end (point-max))
                       prop value)))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-update-external-item-id</span> (<span class="org-type">&amp;rest</span> _)
  (<span class="org-keyword">when</span> (mastodon-tl&#45;&#45;field <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">external</span> (mastodon-tl&#45;&#45;property <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">item-json</span>))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">ask the server to resolve it</span>
    (<span class="org-keyword">let*</span> ((response (mastodon-http&#45;&#45;get-json (format <span class="org-string">"%s/api/v2/search"</span> mastodon-instance-url)
                                              <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"q"</span> . ,(mastodon-toot&#45;&#45;toot-url))
                                                (<span class="org-string">"resolve"</span> . <span class="org-string">"t"</span>))))
           (id (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> (seq-first (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">statuses</span> response))))
           (inhibit-read-only t)
           (json (get-text-property (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">item-json</span>)))
      (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> id json)
        (my-text-property-update-at-point (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">base-item-id</span> id)
        (my-text-property-update-at-point (point) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">item-json</span>
                                          (<span class="org-keyword">progn</span>
                                            (<span class="org-keyword">setf</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> json) id)
                                            (<span class="org-keyword">setf</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">external</span> json) nil)
                                            json))))))
</code></pre>
</div>


<p>
So now all I need to do is make sure that this is called before the relevant mastodon.el functions if I'm looking at an external toot.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-tl</span>
  (advice-add <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;action</span> <span class="org-builtin">:before</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-update-external-item-id</span>)
  (advice-add <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;reply</span> <span class="org-builtin">:before</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-update-external-item-id</span>)
  (advice-add <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-tl&#45;&#45;thread</span> <span class="org-builtin">:before</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-update-external-item-id</span>))
</code></pre>
</div>


<p>
The only thing is that I need to press RET after loading a thread with T (<code>mastodon-tl&#45;&#45;thread</code>) for some reason, but that's okay. Now I can boost and save posts with my usual Emacs News shortcut, and I can reply easily too.
</p>

<p>
I'm curious: how many toots would I be missing if I looked at only one instance's hashtag? Let's look at the #emacs hashtag toots on 2024-09-12:
</p>

<details><summary>Mastodon comparison</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp" id="orgaa5209b"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-three-way-comparison</span> (seq1 seq2 seq3 <span class="org-type">&amp;optional</span> test-fn)
  <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"1"</span> ,@(seq-difference seq1 (seq-union seq2 seq3 test-fn) test-fn))
    (<span class="org-string">"2"</span> ,@(seq-difference seq2 (seq-union seq1 seq3 test-fn) test-fn))
    (<span class="org-string">"3"</span> ,@(seq-difference seq3 (seq-union seq1 seq2 test-fn) test-fn))
    (<span class="org-string">"1&amp;2"</span> ,@(seq-difference (seq-intersection seq1 seq2 test-fn) seq3 test-fn))
    (<span class="org-string">"1&amp;3"</span> ,@(seq-difference (seq-intersection seq1 seq3 test-fn) seq2 test-fn))
    (<span class="org-string">"2&amp;3"</span> ,@(seq-difference (seq-intersection seq2 seq3 test-fn) seq1 test-fn))
    (<span class="org-string">"1&amp;2&amp;3"</span> ,@(seq-intersection (seq-intersection seq2 seq3 test-fn) seq1 test-fn))))
(<span class="org-keyword">defun</span> <span class="org-function-name">my-three-way-comparison-report</span> (label1 seq1 label2 seq2 label3 seq3 <span class="org-type">&amp;optional</span> test-fn)
  (<span class="org-keyword">let</span> ((list (my-three-way-comparison seq1 seq2 seq3)))
    <span class="org-highlight-quoted-quote">`</span>((,(format <span class="org-string">"%s only"</span> label1) ,@(assoc-default <span class="org-string">"1"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (,(format <span class="org-string">"%s only"</span> label2) ,@(assoc-default <span class="org-string">"2"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (,(format <span class="org-string">"%s only"</span> label3) ,@(assoc-default <span class="org-string">"3"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (,(format <span class="org-string">"%s &amp; %s"</span> label1 label2) ,@(assoc-default <span class="org-string">"1&amp;2"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (,(format <span class="org-string">"%s &amp; %s"</span> label1 label3) ,@(assoc-default <span class="org-string">"1&amp;3"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (,(format <span class="org-string">"%s &amp; %s"</span> label2 label3) ,@(assoc-default <span class="org-string">"2&amp;3"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))
      (<span class="org-string">"all"</span> ,@(assoc-default <span class="org-string">"1&amp;2&amp;3"</span> list <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>)))))

(<span class="org-keyword">assert</span> (equal (my-three-way-comparison <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"A"</span> <span class="org-string">"A&amp;B"</span> <span class="org-string">"A&amp;C"</span> <span class="org-string">"A&amp;B&amp;C"</span> <span class="org-string">"A1"</span>)
                                        <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"B"</span> <span class="org-string">"A&amp;B"</span> <span class="org-string">"A&amp;B&amp;C"</span> <span class="org-string">"B&amp;C"</span>)
                                        <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"C"</span> <span class="org-string">"A&amp;C"</span> <span class="org-string">"A&amp;B&amp;C"</span> <span class="org-string">"B&amp;C"</span>))
               <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"1"</span> <span class="org-string">"A"</span> <span class="org-string">"A1"</span>)
                 (<span class="org-string">"2"</span> <span class="org-string">"B"</span>)
                 (<span class="org-string">"3"</span> <span class="org-string">"C"</span>)
                 (<span class="org-string">"1&amp;2"</span> <span class="org-string">"A&amp;B"</span>)
                 (<span class="org-string">"1&amp;3"</span> <span class="org-string">"A&amp;C"</span>)
                 (<span class="org-string">"2&amp;3"</span> <span class="org-string">"B&amp;C"</span>)
                 (<span class="org-string">"1&amp;2&amp;3"</span> <span class="org-string">"A&amp;B&amp;C"</span>))))

(<span class="org-keyword">let*</span> ((later-than <span class="org-string">"2024-09-12"</span>)
       (earlier-than <span class="org-string">"2024-09-13"</span>)
       (results
        (mapcar (<span class="org-keyword">lambda</span> (o)
                  (cons (car o)
                        (seq-map (<span class="org-keyword">lambda</span> (o) (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">uri</span> o))
                                 (seq-filter (<span class="org-keyword">lambda</span> (toot)
                                               (string&lt; (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">created_at</span> toot)
                                                        earlier-than))
                                             (my-mastodon-fetch-posts-after
                                              (format <span class="org-string">"%stimelines/tag/emacs?count=40"</span> (cdr o))
                                              later-than)))))
                <span class="org-highlight-quoted-quote">`</span>((mastodon-social . <span class="org-string">"https://mastodon.social/api/v1/"</span>)
                  (emacs-ch . <span class="org-string">"https://emacs.ch/api/v1/"</span>)
                  (my-instance . <span class="org-string">""</span>))))
       (intersections
        (<span class="org-keyword">let-alist</span> results
          (my-three-way-comparison-report
           <span class="org-string">"mastodon.social"</span>
           .mastodon-social
           <span class="org-string">"emacs.ch"</span>
           .emacs-ch
           <span class="org-string">"my instance"</span>
           .my-instance
           <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">string=</span>))))
  (mapcar
   (<span class="org-keyword">lambda</span> (row)
     (list (elt row 0) (length (cdr row))
           (string-join
            (seq-map-indexed (<span class="org-keyword">lambda</span> (o i)
                               (org-link-make-string o (number-to-string (1+ i))))
                             (cdr row))
            <span class="org-string">" "</span>)))
   intersections))
</code></pre>
</div>

</details>

<div class="bordered" id="orgdf0fd19">
<table>


<colgroup>
<col class="org-left">

<col class="org-right">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">mastodon.social only</td>
<td class="org-right">3</td>
<td class="org-left"><a href="https://ubuntu.social/users/askubuntu/statuses/113126414967731060">1</a> <a href="https://fosstodon.org/users/yabozdar/statuses/113124526863236697">2</a> <a href="https://kolektiva.social/users/capita_picat/statuses/113124061187135721">3</a></td>
</tr>

<tr>
<td class="org-left">emacs.ch only</td>
<td class="org-right">1</td>
<td class="org-left"><a href="https://emacs.ch/users/pi/statuses/113121971331083227">1</a></td>
</tr>

<tr>
<td class="org-left">my instance only</td>
<td class="org-right">0</td>
<td class="org-left">&nbsp;</td>
</tr>

<tr>
<td class="org-left">mastodon.social &amp; emacs.ch</td>
<td class="org-right">9</td>
<td class="org-left"><a href="https://mastodon.sdf.org/users/weigand/statuses/113126692283977409">1</a> <a href="https://hostux.social/users/lfa/statuses/113126305338701924">2</a> <a href="https://hostux.social/users/fsf/statuses/113125825197787920">3</a> <a href="https://fosstodon.org/users/dliden/statuses/113125752869246982">4</a> <a href="https://mas.to/users/evgandr/statuses/113125145671040666">5</a> <a href="https://communick.news/post/1596509">6</a> <a href="https://metalhead.club/users/stevoooo/statuses/113123854333728005">7</a> <a href="https://hachyderm.io/users/fastloris/statuses/113127270000776260">8</a> <a href="https://hostux.social/users/fsf/statuses/113126828026063155">9</a></td>
</tr>

<tr>
<td class="org-left">mastodon.social &amp; my instance</td>
<td class="org-right">0</td>
<td class="org-left">&nbsp;</td>
</tr>

<tr>
<td class="org-left">emacs.ch &amp; my instance</td>
<td class="org-right">1</td>
<td class="org-left"><a href="https://emacs.ch/users/pi/statuses/113126219575933425">1</a></td>
</tr>

<tr>
<td class="org-left">all</td>
<td class="org-right">11</td>
<td class="org-left"><a href="https://social.howellcloud.org/users/james/statuses/113125822427655325">1</a> <a href="https://fosstodon.org/users/robjperez/statuses/113125629561857570">2</a> <a href="https://mastodon.sdf.org/users/bmp/statuses/113125454491301488">3</a> <a href="https://sonomu.club/users/kf/statuses/113125290519531930">4</a> <a href="https://mastodon.social/users/adbenitez/statuses/113125114284046731">5</a> <a href="https://mastodon.social/users/lisp_discussions/statuses/113124797381303645">6</a> <a href="https://mathstodon.xyz/users/lepisma/statuses/113124048647122410">7</a> <a href="https://mastodon.communick.com/users/raphael/statuses/113123503889071870">8</a> <a href="https://emacs.ch/users/ramin_hal9001/statuses/113123160658553524">9</a> <a href="https://fosstodon.org/users/willbush/statuses/113123135443452059">10</a> <a href="https://recurse.social/users/mclare/statuses/113126571790537222">11</a></td>
</tr>
</tbody>
</table>

</div>

<p>
Here's an Euler diagram visualizing it.
</p>


<figure id="org5ec1529">
<img src="https://sachachua.com/blog/2024/09/combining-mastodon-timelines-using-mastodon-el/2024-09-13-12-45-10.png" alt="2024-09-13-12-45-10.png">

<figcaption><span class="figure-number">Figure 1: </span>#emacs posts on 2024-09-12 - an Euler diagram showing the table above</figcaption>
</figure>

<p>
I love that I can tinker with mastodon.el to get
it to combine the timelines. (I'm crossing the
streams!) Yay Emacs!
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon-combined-timeline">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2024/09/combining-mastodon-timelines-using-mastodon-el/index.org">View org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F09%2Fcombining-mastodon-timelines-using-mastodon-el%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry>
</feed>