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.
(defvarmy-mastodon-handle"@sacha@social.sachachua.com")
(defunmy-mastodon-copy-handle (&optional start-new beg end)
"Append Mastodon handles to the kill ring.Use the handle at point or the author of the toot. If called with aregion, collect all handles in the region.Append to the current kill if it starts with @. If not, start a newkill. Call with \\[universal-argument\\] to always start a new list.Omit my own handle, as specified in `my-mastodon-handle'."
(interactive (list current-prefix-arg
(when (region-active-p) (region-beginning))
(when (region-active-p) (region-end))))
(let ((handle
(if (and beg end)
;; collect handles in region
(save-excursion
(goto-char beg)
(let (list)
;; Collect all handles from the specified region
(while (< (point) end)
(let ((mastodon-handle (get-text-property (point) 'mastodon-handle))
(button (get-text-property (point) 'button)))
(cond
(mastodon-handle
(when (and (string-match "@" mastodon-handle)
(or (null my-mastodon-handle)
(not (string= my-mastodon-handle mastodon-handle))))
(cl-pushnew
(concat (if (string-match "^@" mastodon-handle) """@")
mastodon-handle)
list
:test#'string=))
(goto-char (next-single-property-change (point) 'mastodon-handle nil end)))
((and button (looking-at "@"))
(let ((text-start (point))
(text-end (or (next-single-property-change (point) 'button nil end) end)))
(dolist (h (split-string (buffer-substring-no-properties text-start text-end) ", \n\t"))
(unless (and my-mastodon-handle (string= my-mastodon-handle h))
(cl-pushnew h list :test#'string=)))
(goto-char text-end)))
(t
;; collect authors of toots too
(when-let*
((toot (mastodon-toot--base-toot-or-item-json))
(author (and toot
(concat "@"
(alist-get
'acct
(alist-get 'account (mastodon-toot--base-toot-or-item-json)))))))
(unless (and my-mastodon-handle (string= my-mastodon-handle author))
(cl-pushnew
author
list
:test#'string=)))
(goto-char (next-property-change (point) nil end))))))
(setq handle (string-join (seq-uniq list #'string=) " "))))
(concat "@"
(or
(get-text-property (point) 'mastodon-handle)
(alist-get
'acct
(alist-get 'account (mastodon-toot--base-toot-or-item-json))))))))
(if (or start-new (null kill-ring) (not (string-match "^@" (car kill-ring))))
(kill-new handle)
(dolist (h (split-string handle " "))
(unless (member h (split-string " " (car kill-ring)))
(setf (car kill-ring) (concat (car kill-ring) " " h)))))
(message "%s" (car kill-ring))))
Another perk of tooting from Emacs using mastodon.el. =)
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 mastodon.el, I can probably figure out how
to get the URL after tooting. A quick-and-dirty
way is to retrieve the latest status.
I considered overriding the keybinding in
mastodon-toot-mode-map, but I figured using
advice would mean I can copy things even after
automated toots.
A more elegant way to do this might be to modify
mastodon-toot-send to run-hook-with-args a
variable with the response as an argument, but
this will do for now.
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 link to
the current post. 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.
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.
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.
I feel complicated feelings about #EmacsConf 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 #Emacs community. (I wouldn't get rid of or discourage anyone; I just want more! :) )
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.)
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.
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.
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 https://emacsconf.org/2024/cfp/ (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. :)
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.
Is there already an interactive #emacs 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...
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`.
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.
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.
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.
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.
Now I just have to wait a few hours for these dd commands...
I noticed that some of my #Syncthing 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 https://forum.syncthing.net/t/syncthing-readonly-access-on-storage/21459/2 . 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.
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.
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.
As it turns out, org-list-to-org uses the Org export mechanism, so it quietly discards things like #+begin_export html blocks. I decided to hard-code assumptions about the list's structure instead, which works for now.
[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)))
(defunmy-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:
(defunmy-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")))))
(defunmy-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)))
(defunmy-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))))
(defunmy-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-deftestmy-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.dAfter 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"))))
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.
[2025-03-24 Mon]: Updated my-mastodon-follow-user to use alist-get.
[2024-11-04 Mon]: Make tag a parameter.
[2024-10-07 Mon]: Added screenshot.
[2024-09-16 Mon]: Read JSON arrays as lists to be compatible with the latest mastodon.el.
.
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 I've moved to @sacha@social.sachachua.com and emacs.ch is winding down, I wanted to see if there was a way for me to see a combined view using mastodon.social's API feed (paging by
max_id as needed). I haven't enabled public
timeline feeds on my server, so I also need to reuse the OAuth mechanics from mastodon.el.
First, let's start by making a unified timeline. By digging around in mastodon-tl.el, I found that I could easily create a timeline view by passing it a vector of toot JSONs.
(defunmy-mastodon-fetch-posts-after (base-url after-date)
"Page backwards through BASE-URL using max_id for all the posts after AFTER-DATE."
(require'plz)
(require'mastodon-http)
(let ((results [])
(url base-url)
(use-mastodon-el (not (string-match "^http" base-url)))
(json-array-type 'list)
page filtered)
(while url
(setq page (if use-mastodon-el
(mastodon-http--get-json (mastodon-http--api url) nil :silent)
(seq-map (lambda (o)
(cons (cons 'external t) o))
(plz 'get url :as#'json-read)))
filtered (seq-filter (lambda (o) (string< after-date (assoc-default 'created_at o)))
page))
(if filtered
(progn
(setq results (seq-concatenate 'vector filtered results)
url (concat base-url (if (string-match "\\?" base-url) "&""?")
"max_id=" (assoc-default 'id (elt (last page) 0))))
(message "%s %s" (assoc-default 'created_at (elt (last page) 0)) url))
(setq url nil)))
results))
(defunmy-mastodon-combined-tag-timeline (later-than tag &optional servers)
"Display items after LATER-THAN about TAG from SERVERS and the current mastodon.el account."
(interactive (list
(org-read-date nil nil nil nil nil "-Mon")
"#emacs"'("mastodon.social""fosstodon.org")))
(setq servers (or servers '("mastodon.social""fosstodon.org")))
(require'mastodon)
(require'mastodon-tl)
(require'mastodon-toot)
(if (stringp later-than)
(setq later-than (org-read-date nil nil later-than)))
(setq tag (replace-regexp-in-string "#""" tag))
(let* ((limit 40)
(sources (cons (format "timelines/tag/%s?limit=%d" tag limit)
(mapcar (lambda (s)
(format "https://%s/api/v1/timelines/tag/%s?limit=%d" s tag limit))
servers)))
(combined
(sort
(seq-reduce (lambda (prev val)
(seq-union prev
(my-mastodon-fetch-posts-after val later-than)
(lambda (a b) (string= (assoc-default 'uri a)
(assoc-default 'uri b)))))
sources [])
(lambda (a b)
(string< (assoc-default 'created_at b)
(assoc-default 'created_at a))))))
(with-current-buffer (get-buffer-create "*Combined*")
(let ((inhibit-read-only t))
(erase-buffer)
(mastodon-tl--timeline combined)
(mastodon-mode))
(setq mastodon-tl--buffer-spec `(account ,(cons mastodon-active-user mastodon-instance-url) buffer-name ,(buffer-name)))
(display-buffer (current-buffer)))))
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 external property lets me
find that in the item_json text property in the
timeline buffer. For those toots, I can use
(mastodon-url-lookup (mastodon-toot--toot-url))
to open the toot in a new buffer that does allow
boosting or replying, which is probably enough for
my purposes.
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 mastodon-toot--action, which looks up
the base-item-id text property. Replying looks
up the item-json property and gets the id from
it.
The only thing is that I need to press RET after loading a thread with T (mastodon-tl--thread) 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.
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:
I want to use my microblog posts on Mastodon as building blocks for
longer posts on my blog. Getting them into an Org file makes it easier
to link to them or refile them to other parts of my Org files so that
I can build up my notes.
If you get mastodon-auth--handle-token-response: Mastodon-auth--access-token: invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. while trying to set up mastodon.el from MELPA, you might have an outdated version. Try the following steps: