Hi all,
quick context for why I'm posting this. I run my own blog on WordPress and I use the ActivityPub plugin there. It just works in the background, my posts show up in Mastodon timelines without me thinking about it.
My wife runs her counselling practice site on Grav and she wanted the same thing. I looked around and there's no native ActivityPub plugin for Grav that I could find. There is bridgyfed which goes through the brid.gy bridge, but that's a different mechanism and I wanted something that runs in the same box as the blog.
So I tried to build it. Solo, in my spare time, over the last couple of days. No idea if I did it well or if half my design choices are wrong. I'm not a Grav developer by trade, I copied some patterns from grav-plugin-form for the boring bits and worked out the ActivityPub side by reading the spec and a lot of the wordpress-activitypub source.
Repo: https://github.com/Kernel-Error/grav-plugin-fediverse-publisher
Release: v0.1.0 ( https://github.com/Kernel-Error/grav-plugin-fediverse-publisher/releases/tag/v0.1.0 )
Where it runs right now
Exactly one production site, my wife's blog. Grav 1.7.52, Grav-Admin 1.10.51, PHP 8.3 on FreeBSD. Two real followers, one from mastodon.social and one from bonn.social. New posts she publishes via the Admin land in both their Mastodon home timelines within about 15 seconds, with the right title, summary, image and hashtags from her taxonomy.tag frontmatter. Profile page (bio, avatar, header) renders correctly on Mastodon.
I also tested it locally against GoToSocial 0.21 in a podman setup, the follow handshake and broadcast both work there too.
What the plugin does
- WebFinger + NodeInfo + Actor JSON-LD at the spec-required paths
- Inbound Follow / Undo Follow with HTTP-signature verification (draft-cavage-12), digest check, date-skew, identity binding, SSRF-hardened keyId fetch
- Outbound Accept push when somebody follows
- Auto-broadcast on Grav Admin page-save:
onFlexAfterSaveevent hooks into the save, builds a Create activity wrapping a Note or Article (depending on length), pushes to every active follower summaryplusattachment(image from the body or from$page->media()if there's no inline<img>)- Hashtag federation from
taxonomy.tag, with the#-prefixednameMastodon needs to index, plushrefpointing back at Grav's per-tag landing page - Push queue in SQLite with per-row retry / backoff / dead-lettering, drained by the Grav scheduler tick
- Two operator CLIs:
broadcast:post <route>for manual re-broadcast if a save event was missed,push:purge-deadfor housekeeping
What it doesn't do yet
Updateactivity on re-saves. Right now a re-save sends a freshCreate. Mastodon dedupes byobject.idso the visible result is fine, but I'm pretty sure Pleroma and Misskey will not handle that nicely- Delete federation. When a post is deleted in Grav the follower-side keeps the stale copy until that server happens to refetch the URL
- Showing inbound likes, boosts and replies on the source site (broadcast only for v0.1)
- Multi-actor. One Grav site equals one Fediverse actor, no per-author handles
- Authorized-fetch (the
AUTHORIZED_FETCH=trueMastodon variant) is only partially supported via a heuristic for the inbox URL. Full signed-GET path is on the list UpdateandDeleteare the next two roadmap items but I haven't started on them
Things I'm genuinely unsure about
- Whether the architecture (SQLite for state, draft-cavage signatures, scheduler-driven push worker) is anywhere near how Grav-native plugins are usually built. It's what I landed on by reading other plugins and the ADRs I wrote for myself, but I'm probably missing established conventions
- Whether subscribing to both
onFlexAfterSaveandonAdminAfterSaveis the right idea (Grav-Admin 1.10 routes saves through Flex, but classic save paths fire the other event, so I'm hedging). The push queue dedupes via a UNIQUE constraint so double-firing is safe, but maybe there's a cleaner way - Whether anyone other than my wife's setup would actually want this. There's already
feed2tootfor the "post to Mastodon when RSS updates" case which is a perfectly fine alternative for a lot of people - Whether the multi-site config gotcha (Admin saves to per-host yaml but CLI reads only the global) is something the plugin should warn about loudly or just document. Right now it's documented in the README troubleshooting section but it bit me on day one of the deploy
What I'm hoping for from this thread
If you have a Grav blog, even a test one, would you be willing to give it a try and tell me what breaks? README has the install steps. The one config field you absolutely need to set is federation.canonical_host (the public https URL of your site, the plugin refuses to enable without it).
Code review is also welcome. I'm sure there are PHP, Grav and ActivityPub idioms I got wrong.
And the actual question I'm wrestling with: does this make sense as a real plugin that goes into the GPM index, or is it solving a problem too few people have? If the latter, I'll just keep it as my wife's setup and not bother submitting. If the former, I want to make sure it behaves on at least one setup that isn't mine before I formally submit.
Thanks for any look you can spare. Even "tried it, blew up on first save, here's the log" is useful information.