This is an irregular periodical update about what I've been working on in recent months :)

(Warning: vibe-coding will be mentioned, more than once…)

New projects

RatProto 🐀

This is a Ruby command-line tool that combines several of my ATProto Ruby gems to do a few unrelated things that might be useful to quickly run from the command line, like:

  • fetch a record by at:// URI

  • look up a DID document or resolve a handle

  • check an account's status

  • stream filtered events from a firehose or Jetstream

I wrote about it here:

RatProto for AtProto 🐀

RatProto for AtProto 🐀

Ruby CLI for digging inside some atproto things

Kuba Suder 🇵🇱🇺🇦
RatProto for AtProto 🐀

CSP Maker 📃

This is basically just the ActionDispatch::ContentSecurityPolicy class for defining a CSP using a DSL, extracted from Rails to a standalone gem, so it can be used inside Sinatra/Rack or other apps. The DSL is the same, all the integrations with Rails are removed, and there's an optional Rack middleware that doesn't depend on ActionPack.

I had a helper like this before copy-pasted in a few projects, but I figured it's better to have it included from a single place.

Oxygène

This started as wanting to redesign the repo import process in Lycan, realizing I don't have code for parsing repo .car files, building that code using what I had in Skyfall, then realizing it's really slow, and ended up as a 2-week-long session with Codex, going through many cycles of profiling & optimization, and eventually the lower layer of parsing-related parts got extracted to a new gem I called Oxygène, so you can use those without importing the firehose-related parts. I wrote about this here:

Making Ruby go brrrr (with Codex)

Making Ruby go brrrr (with Codex)

Skyfall optimizations and a new gem

Kuba Suder 🇵🇱🇺🇦

Minestrone 🍲

This is, uh… a mostly vibe-coded simplified fork of old Capistrano 2. Yeah, I know…

See, I started using Capistrano for Ruby app deploys in early 2010s when I was working for a Rails software house, I had it configured in all my Ruby apps, and I didn't really have a reason to change it. If it ain't broke… But using outdated software is not always good for various reasons, and Capistrano project moved on to v. 3.x long ago. Except this was more of a full rewrite, changing a lot of things in the API, and I knew from looking at the docs a few times over the years that I'd need to spend a day or two going through all the documentation, updating my configs, plus some things like the "copy" strategy (deploy straight from local disk vs. from a git repo) which I sometimes use were removed, so I'd need to find a way to do them differently. So that's work, with probably not much benefit. So I just stuck with the legacy 2.x branch for over a decade, since it worked as is.

I asked around sometimes what things like that people use these days, but it seems that most use some kind of Docker/container based systems, which I… really don't want to get into, and those few who don't, use some kind of custom-made deploy scripts, or maybe Nix. There's another simpler Ruby deploy tool called mina, but it doesn't seem to be maintained anymore.

So I finally decided to bite the bullet, read the docs properly and update all my configs. I started doing that, and in the end… the whole thing worked slower. Well thanks for nothing.

It seems that this rewritten system in Cap 3 based on their "SSHKit" library, with a maybe cleaner but much more verbose DSL, also inherently makes much more calls to do the same thing, cd'ing to a directory for every command or running some checks for file/directory existence, I don't remember exactly. But it was something like 15s to 30s difference in deploy time. Not a huge deal, but why would I do work in order to make things worse?

This doesn't necessarily mean it's a bad tool, it just always had a focus on slightly more complicated setups (I've literally never used the roles thing there), and with the rewrite it went further in that direction, at the cost of simpler use cases.

So in the end I thought: ah fuck it, I'm just gonna fork it. And since this was a time when coding agents got really good already, I asked the bot for help, first to understand what the architecture of the project was, and then to gradually strip out everything I don't need: support for multiple servers, roles, stages, parallel commands, weird deploy strategies, obscure version control systems, and so on. Just keep a way to deploy a Ruby app to one server from git and run some extra tasks, with the revision system, callbacks and so on. And the remaining code should be much easier to understand and maintain going forward.

I called it "minestrone", because "mini capistrano… ministrano?" 🙃 (Also I like soups.)

I didn't put much more work into it, but I've switched the deploy configs in all my Ruby apps to this forked version and everything works fine, plus I managed to simplify/remove some boilerplate that was required in 2.x for historical reasons, and added support for latest Bundler.

If someone's interested in using this tool, I'll be happy to help and add some things if needed, but if not (more likely), I'm totally fine just being the single user of it, it solves my problem.

mackuba.eu/minestrone
A simplified fork of old Capistrano 2.x - a tool for deploying Ruby applications to a server
https://tangled.org/mackuba.eu/minestrone

Blog

I started a new series of blog posts on my main blog called "ATProto in practice", which currently has 1 episode ;) about DIDs & handles. But a second one (about XRPC APIs) is coming soon, hopefully.

ATProto in Practice #1: Identity
M
MacKuba blog

ATProto in Practice #1: Identity

I’m starting a new blog post series that I’ve named “ATProto in Practice”. I want to go through some practical tasks that you’re likely to need when working with the protocol, using Ruby and my Ruby gems for the examples. These will (hopefully) be a bit shorter than my standard book-length blog posts here 🫠 If you’re new to the AT Protocol and you haven’t read my long Introduction to AT Protocol post that goes through the whole architecture and defines the various pieces of it, I recommend you read that one first, because I won’t be explaining everything from scratch again here. Let’s start with something pretty fundamental: user identity. DIDs & handles Every account has an immutable identifier called a DID (Decentralized Identifier), e.g. did:plc:z72i7hdynmk6r22z27h6tvur. This ID serves a similar role as an UUID might in an SQL database – it’s used everywhere behind the scenes in the protocol for any references between accounts and records, in URIs and so on. But generally every account also has a human-readable handle assigned to it, which can be changed at any time, and this handle is normally what you see in the UI, often in user-facing URLs, what you use to log in with, etc. The handle is a domain name, e.g. @python.org, and any existing domain name you own and use for your website can be used as your handle. As you start building on ATProto, one of the first things you might need to do is convert between these two identifiers. For example, a user logs in to your app using a handle and you need to first “resolve” it to a DID, or a URI or other reference in a record points to a DID, but you want to show the corresponding user’s handle in the UI, and the handle is not included in the data you got. A useful UI tool for quickly looking this up is internect.info. If you look up e.g. “firefox.com” there, you will get a page showing details of Firefox’s ATProto account: the DID (did:plc:m424cqoxwhxgjutbta7jmrur), when it was created, assigned PDS hostname, and so on. Now, how to do the same thing in code? Every DID has a so called DID Document, a JSON file storing some basic info about that identity, and among other things, that document includes the alsoKnownAs field, which lists the assigned handles, in the form of at:// URIs with only the first segment. Almost every account will have exactly one handle assigned, but it might happen that one will have zero or more than one. It might also happen that the array will contain some invalid strings which aren’t proper handles – e.g. don’t include the at:// prefix, or only contain one word and no periods. You need to filter only the valid ones. The assignment is bi-directional – a DID has one (usually) handle assigned in its document, and a handle resolves to a DID. You should ideally check the assignment in both directions, because it could happen that the other sign of the assignment is no longer valid, or worse – has never been real. That is, when you resolve a handle to a DID, check if th…

Kuba Suder 🇵🇱🇺🇦
ATProto in Practice #1: Identity

Also, I replaced the old local comments system there with comments loaded dynamically from Bluesky & Mastodon, plus I also added support for standard.site to the blog, syncing new & updated posts to my PDS on deploy:

Social comments on my blog
M
MacKuba blog

Social comments on my blog

I had a comments section on this blog since the beginning. I always felt like a comments section was kind of a requirement for a blog: it’s not just a website where you publish posts, it’s a website where you publish posts and other people can comment on them. I strongly disagreed with bloggers who openly declared that they’re removing comments from their blog because their blog is not a place for others to add their remarks, and that they can always write to the author on Twitter if they want to contact them. But one obvious problem with comments on a blog, which got worse and worse over time, was spam. People tried to solve it e.g. with captchas, though as the bots got smarter, captchas had to get more complicated and annoying. For a very long time, I had a super simple captcha in the comment form that just asked: “Are you a human? (yes/no)”. You just had to write “yes” in the text field. And for a very long time it was enough – obviously it’s trivial to make a bot add an additional query parameter, the point was that nobody would think about it when writing generic spambots, and my blog isn’t famous so the bot authors wouldn’t have come across it. Things got worse around 2018-19. I started getting comments (I had email notifications for them) that were spam that obviously got around the captcha. So I started tweaking the captcha, adding multiple slightly more complex questions, but still such that you could answer them if you’re a developer or an Apple user – “What’s the name of the Mac operating system”, “Who is currently the CEO of Apple”, and so on. But it only worked for some time, and then I started getting spam again, while making the questions progressively harder over time: At some point I figured that this had to be actual people, rather than bots, adding the comments (talking in general, not about Jerry here above), people sitting at a computer, browsing websites and filling the forms by hand, because there was no way a bot would figure out that the question was a captcha and that it had to google for a name of a function (note, this was in pre-AI times). The comments were usually either in Russian or Ukrainian, or linking to some local service companies in some random small town in the US. There was no way I could out-captcha an actual human that can google for an answer. But it wasn’t a huge problem, I didn’t get many of those comments, and it was fairly easy to filter them out still by content, so I didn’t bother changing the system further. But here’s the thing: it’s not 2010 or 2018 anymore, and some other things have changed in the meantime too. I think it’s a bit of a truism that blogs and “blogosphere” aren’t what they used to be 15+ years ago. People post their thoughts on social media, Instagrams, Facebooks, LinkedIns, Substacks, record Tiktoks, YouTube videos or podcasts. Reading and writing blogs and commenting on them is a very niche hobby, let’s face it. After 2020-21, when for a while I was getting a ton of comme…

Kuba Suder 🇵🇱🇺🇦
Social comments on my blog

And, I finally got rid of jQuery there! 😂 (it was used for a very old lightbox plugin)

Updates

blue.mackuba.eu

  • on request from from Latinsky/Medsky, I added a detail view in the PDS stats table which shows how many accounts on the PDS use which handle suffix:

(currently this only shows up for Blacksky PDS, since it's the only big one that has multiple handle choices)

  • added a chart on the stats page that shows how the major non-Bluesky PDSes are growing:

  • also on this "weekly users per origin" chart, you can now toggle this % checkbox to see what % of all Bluesky users are posting on non-Bluesky PDSes, and how this changes over time:

Skythread

  • added a profile info popover with bio, pronouns, PDS info, and account creation date:

  • the "hidden replies" view now tries to get more info about why a specific reply isn't visible on Bluesky, e.g. what state the relevant account is in:

  • and it also tries to detect when a reply was hidden because the author got the infamous "needs review" shadowban-like label:

  • also, post edits done via mu.social are displayed:

  • and inline code and code blocks are highlighted like in Mu too:

  • added support for new gallery image embeds (for now just as a list of [Image] links like for the old embed)

  • added support for Klipy & Giphy gifs, and they're displayed with a new custom placeholder view with alt

  • and… I've been working on OAuth support a bit, but I can't get myself to work on that too much 🫣

Pulsar

  • I added an "uptime" column – there's a background job that tests connection to one source every few minutes, which adds up to a few pings a day to every source, and the column will show the % of successful pings to a given server over the last month. (Not like a literal ICMP ping, just make a websocket connection and wait for one event.)

  • I'm also trying to improve the algorithms calculating the coverage %, to better detect which source was doing something funny during the test, and to avoid interpreting a result as one relay having 100% and others all having less (when really it's one reporting a too large number that should be ignored as an outlier) – this is still ongoing work.

Ruby gems

  • added support for parsing public keys from DID documents to didkit, and finally proper JWT token verification to Lycan (which should also find its way to blue_factory for feed viewer authentication at some point)

  • I'm gradually improving the test coverage further in some of the gems, which got much easier now with the help of robots :]

Experiments

Run Map

For many years I had this idea in my head of some kind of app (ideally a native Mac app) that would let me easily browse my whole history of runs & bike rides that I have collected in RunKeeper, Strava and HealthKit. To let me view old runs on some routes I've forgotten about and give me ideas of where I could go again. But this always remained on the todo list, because I felt like there was a bit too much work to get this to any usable point.

Well, not anymore, when you have a bot that can implement this for you in one afternoon!

I'll need to spend more time on this before this is anything close to releasable in any form, but it's looking promising and should already be of some use to me:

Hannibal

This is an experiment in building a Safari extension for bsky.app, which hacks it to display bridged Mastodon 🐘 posts in full length as if they were normal native posts, instead of showing them truncated to 300 characters with a link to the original post. I used it for a bit, but turned it off later because I think it's contributing to the high memory use of all my Safari tabs (it needs to store received JSON data and then use it to re-render post components), so I'll need to optimize it before I can use it "in production".

Other

  • I made a simple Webkit view wrapper Mac app for FB Messenger website, so I can have it sitting in the Dock, always at hand, and show notification badges when I get new messages. It kinda works and I use it to chat with my sister, but Facebook's Messenger webapp is really shit and often stops loading or stops working for no reason and needs to be reloaded… I miss their old official Mac app, which also wasn't native, but it was better than this. :\

  • I vibe-coded a Mac ToDo list app for myself, which is designed to work exactly with my workflow: a list of projects in the sidebar, each project has tasks just grouped into a few scopes like "ready to do", "later" or "someday", and states like not started / partially done / blocked / completed (with marking a task as started & in progress as a core feature). Organized as "here's a general list of things you could be doing first from all projects, pick one you feel like working on today", not "tasks I will do on Tuesday", because I don't work this way. You can select a project in the sidebar and have this project's tasks grouped into scope sections, or select a scope and see all tasks in this scope grouped into project sections. I've been using it as my main task management app for a few months now, and I'm pretty happy with it (despite some annoying UI bugs I need to find time to fix).

Boring ongoing maintenance