Collecting Emacs News from Mastodon

Posted: - Modified: | emacs, mastodon

[2025-03-06 Thu]: Use my-org-link-url-from-string.

One of the things I like about browsing Mastodon in Emacs using mastodon.el 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 my-mastodon-store-link function defined elsewhere in my config.

(use-package org
  :config
  (add-to-list
   'org-capture-templates
   '("📰" "Emacs News" entry (file+headline "~/sync/orgzly/news.org" "Collect Emacs News")
     "* %a  :news:

#+begin_quote
%:text
#+end_quote

"
     :prepend t :immediate-finish t)))
(defun my-mastodon-save-toot-for-emacs-news ()
  (interactive)
  ;; boost if not already boosted
  (unless (get-text-property
           (car
            (mastodon-tl--find-property-range 'byline (point)))
           'boosted-p)
    (mastodon-toot--toggle-boost-or-favourite 'boost))
  ;; store a link and capture the note
  (org-capture nil "📰"))

(use-package mastodon
  :bind (:map mastodon-mode-map ("w" . my-mastodon-save-toot-for-emacs-news)))

This puts a bunch of notes in my ~/sync/orgzly/news.org file. Then I can use my-emacs-news-summarize-mastodon-items 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:

output-2024-09-16-13:14:38.gif
Figure 1: Quick screencast summarizing Mastodon toots

Here's the code that makes that happen:

(defun my-emacs-news-summarize-mastodon-items ()
  (interactive)
  (while (not (eobp))
    (let* ((info (my-mastodon-get-note-info))
           (title (when (car (plist-get info :links))
                    (my-page-title (car (plist-get info :links)))))
           (summary (read-string
                     (if title
                         (format "Summary (%s): " title)
                       "Summary: ")
                     title)))
      (org-cut-subtree)
      (unless (string= summary "")
        (insert "- " (org-link-make-string
                      (or (car (plist-get info :links))
                          (plist-get info :url))
                      summary)
                (if (and (car (plist-get info :links))
                         (plist-get info :handle))
                    (concat " (" (org-link-make-string (plist-get info :url)
                                                       (plist-get info :handle))
                            ")")
                  "")
                "\n")))))

(defun my-match-groups (&optional object)
  "Return the matching groups, good for debugging regexps."
  (seq-map-indexed (lambda (entry i)
                     (list i entry
                           (and (car entry)
                                (if object
                                    (substring object (car entry) (cadr entry))
                                  (buffer-substring (car entry) (cadr entry))))))
                   (seq-partition
                    (match-data t)
                    2)))

(defun my-org-link-url-from-string (s)
  "Return the link URL from S."
  (if (string-match org-link-any-re s)
      (or
       (match-string 7 s)
       (match-string 2 s))))

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

(ert-deftest my-mastodon-get-note-info ()
 (should
  (equal
   (with-temp-buffer
     (insert "** SOMEDAY https://mastodon.online/@jcastp/111762105597746747         :news:
:PROPERTIES:
:CREATED:  [2024-01-22 Mon 05:51]
:END:

jcastp@mastodon.online - I've shared my emacs config: https://codeberg.org/jcastp/emacs.d

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!)

If you can find there something you can use, then I'm happy to be useful to the community.

#emacs
")
     (org-mode)
     (my-mastodon-get-note-info))
   '(:handle "@jcastp@mastodon.online"
             :url
             "https://mastodon.online/@jcastp/111762105597746747"
             :links
             ("https://codeberg.org/jcastp/emacs.d")
             :text
             "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"))))

It turns headings into something like this:

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

This works particularly well with my combined Mastodon timelines, because then I can look through all the #emacs posts from mastodon.social, emacs.ch, and social.sachachua.com in one go.

This is part of my Emacs configuration.
View org source for this post
You can comment with Disqus (JS required) or you can e-mail me at sacha@sachachua.com.