The goal of December Adventure is to code a bit every day and write about it. Using this as an opportunity to fill my blog with some content while also holding myself accountable to actually code a bit every day.
My overall goal is to get working federation into my pet project sharepa, a library of things for small communities. Ideally, this would prevent people from buying stuff for one-time jobs if they can instead borrow it from someone in their community. It's currently nowhere near finished but I want to build it with federation in mind from the beginning to avoid competing instances and having to manage multiple accounts when part of multiple communities.
It's not a goal to be widely compatible with the broader fediverse as this would be unmaintainalbe. I'm simply using ActivityPub as a protocol so I don't have to reinvent the wheel with regards to federated networks.
Day 0
Capturing the state of things before starting: Basic dereferencing works. Users can dereference accounts from other instances. Only the neccesary amount of information is shared during this process to protect user data.
Day 1
Started working on getting Follow
activites supported. Turns out that
the go-ap
library uses a signing function that cannot be customized. Since I
also want to understand ActivityPub better, I decided to implement the
processing functionality myself instead.
Day 2
Got the follow requests to be sent over the wire sucessfully! Have to figure out a way now how to cleanly handle the handshaking issue. When dereferencing a remote actor, both instances will not share the full actor before validating the key. Previously this wasn't an issue as the second instance didn't need the actor later on but now that follow requests need to be shown somehow, this information is important.
Day 3
I followed the same approach that GoToSocial uses and it actually worked! Follow requests and their respective actors get correctly stored on the remote instance 🥳.
This change comes at the cost of having to rewrite my tests since the
authentication functionality is now coupled to the Federator
service. Ideally
I'll write a test case testing the AuthenticateRequest
function directly but I
might wait until I am confident I won't redo this entire thing again.
Tomorrow I'll probably work on adding distributed tracing to this so I can visualize the request flow for easier debugging.
Day 4
The traces are in! This should help debugging more complex flows later on. One
thing that's missing is propagating the context to the database queries.
Currently this is not the case as the Store
interface of go-ap
doesn't expect a
context parameter. But now that I've decided to write my own processing logic, I
no longer need to rely on that interface and will probably replace it with just
my DB
interface. Yay for deleting code!
Day 5
Nothing too exciting today. Picked up where I left off yesterday and made sure to propagate the context everywhere. Now the select statements show up correctly and I can also trace specific functions in the code. Tomorrow I'll look into storing the follow request locally as well and accepting it from the remote instance.
Day 6
Saving the follow request locally was pretty straightforward, so was implementing
the UI for notifications. There is no dedicated notification model yet so I'm
just displaying the follow requests with Accept and Reject buttons. Saving the
follow locally works and the acting server correctly sends it out but on the
receiving end the processing for the Accept
type is still missing. Hopefully I
can get this worked out tomorrow.
Day 7
It is done! Follow requests get propagated correctly and the receiving account can accept them! Rejecting is not an option but this should be trivial to implement now that the rest is in place. Ignoring is also always an option. Also implemented the UI to track whether you're currently following someone.
With all this focus on federation, I completely neglected local accounts so I should probably tackle that next. Once this is done though, the actual useful part of sharing proposals/intents/offers can begin!
Day 8
Today I focused on seting up a good testing framework in anticipation of adding more features. Based on the excellent article How I write HTTP services in Go after 13 years by Mat Ryer, I built a test framework spawning two complete instances rather than stubbing/mocking everyting. I'll still use mocks for some other test cases but in the federation case, end-to-end testing is preferable.
With a solid test base, I could also clean up the code a bit and put the
dereferencer
, federator
and storage
into their own packages.
Day 9
Local follows were easy enough to implement by checking if the target account is local and skipping delivery if this is the case. To actually find local users, I polished up the search and account page. This shows the next obvious thing to work on: federated media. This will require background processing so I might postpone this until Saturday, when I'll have more time to spend on this.
Day 10
Fixed a few issues regarding the local activity detection and implemented dynamic notification count display using HTMX. Currently it is implemented using polling but ideally this should instead use server-sent events. For this to work though, I'll need to add hooks to the database interface which can then notify any active sessions. Thinking about how to implement dynamic elements, I'd also like to draft a document around the design decisions and philosophy to keep myself accountable. HTMX was alwasy a consideration from the start but all features should be available without any JS enabled as well.
Day 11
Refactored the database models a bit to get rid of the Intent
model. This is a
remenant of the initial design based on ValueFlows but I came to realize that
this doesn't add any benefit in the current context and only adds complexity.
The ValueFlows spec certainly helped getting some of the terminology correct but
I'll diverge from that to keep things as simple as possible. Based on that, I
also took the opportunity to overhaul the creation page. Looks a bit more
coherent now.
Day 12
Implemented dependency injection through a state.State
struct which now allows
the db implementation to reference other parts of itself. This state struct will
also become very usefull once I get to implementing workers. The immediate
benefit today was the implementation of a SSE endpoint for notifications instead
of polling. This removes a lot of log spam and results in instant notifications.
The underline decoration is still bugging me but that's something for the
future.
Day 13
Not much time today but I figured out why the underline behavior is so weird! I
always thought that HTML ignores all whitespace but it turns out this is only
the case in the block context. In the inline formatting context, whitespace is
handled differently. Using a flexbox div instead of relying on a large span for
the nav
action items fixed the issue while keeping the source code well
formated!
Day 14
Didn't get to work on this much today either but I laid the foundation of async processing using neoq. The reason for choosing this library over something like river is support for an in-memory backend. I want sharepa to be as accesible as possible and requiring another service to be running as well gets in the way of that.
Day 15
Originally I wanted to work on federated proposals. Had to "quickly" finishup
the async processing for follows which spiraled out of control due to issues
with the federation tests. I don't fully understand it yet but the issue had to
do with the database connection sometimes just being wrong. This manifested in
the worker accessing an empty (but still connected!) database. Setting
?cache=shared
fixed the issue but I don't yet know why. I assume bun
, the DB
interface, opens separate connections per goroutine in some cases which could
cause the symptoms seen here but I can't guarantee it. For now, the issue is
fixed and async processing works. I don't really love the API I've built but
it's workable. Maybe I'll have some time in the evening to do the proposal
federation as well 🤞.
Evening update: Outbound federation is implemented! Next up: actually storing the received proposal.
Day 16
Didn't have the time to work on sharepa today 🤷.
Day 17
Spent a lot of time on JSON marshal/unmarshal and got it working! Honestly, I'm a bit surprised that it works now. Next step: media or offer federation - not sure what I'll pick though.
Day 18
Today I got rid of the storage.Repository
interface in favor of putting the
logic in the database. Thinking of it now, I might actually revert that though.
My goal was to get generic AP loading into the worker which already had a DB
reference but I just realized I could have also simply built a new repository in
the worker as the repo is just a container 🤦.
Also got the outbound federation of offers working so at least that's progress.
Day 19
Implemented saving of offers - things are coming together! The constant refactorings paid of and implementing new activities is no longer daunting. I still have some ideas on how to optimize further but I won't get that get in the way of getting something out there.
Day 20
Expected to get more done today but got overwhelmed with other tasks. Nonetheless, I got outward federation of offer acceptance working and cleaned up the codebase a bit. Neat!
Day 21
Nothing to report today - spent time doing other things :).
Day 22
Implemented the receiving end of offers. This made it clear that I'll need to figure out a way to cleanly pass DB/state to federation logic. Will get to that eventually 😬.
Day 23
Today I got media federation working! It's not ideal yet as there are no access controls but getting this working marks a point at which this software is ✨ useable ✨. I'm pretty pleased with how far I got in such a short time and am looking forward to pushing things further and get this out into the world. With basic federation out of the way, I get to clean up the user interface, add actual features and make this a delight to use!
Day 24
Spent this christmas morning getting the start/end selection for offers implemented. It's still a bit wonky and can use a bit more user feedback but it works! Again 100% JS optional.
Day 25
It's time to add some delight! I've added support for multiple images in a
proposal and set up everything for i18n
support. Not everything is translated
yet but the foundation is set up. Images are now cached as well with the
addition of cache-control headers. I also ventured deep into the depths of CSS
and found a way to get dynamic calendar highlighting all without JS!
Day 26
This morning I worked on the search page a bit since this will be the primary discovery mechanism. It still needs more love for it to be reference from other parts of the application but proposals can now be searched for. I also built a mechanism to display short lived flash messages. This allows for ephemeral status updates like "Proposal created" or "Settings updated". To build interactive zones, I've decided to use alpine.js as it works really well in conjunction with htmx. I've also looked at hyperscript but it looks a bit too complicated for the level of interactivity I'm looking for.
Day 27 & 28
Took these two days to unwind a bit, nothing to report on.
Day 29
Started implementing OIDC so I can get this deployed for my friends. Had some troubles passing the required things around until I remembered cookies are a thing. Now the nested OIDC flow works as expected. I still need to clean this up as it's full of hardcoded values and the user creation also doesn't happen yet but I'm happy to have gotten this far in just a few hours.
Day 30
What started with the simple goal of making the external auth providers configurable ended with me completely restructuring the configuration approach. Instead of viper, I'm now following the approach taken by most grafana databases (mimir/tempo/loki/pyroscope) and expose all configuration values using the command line. This helps to keep the config local to the component where it is used and simplifies parsing. It also forces me to refactor some of the auth logic which isn't well seperated from the web component. It's not yet finished but I think I like where this is going.
Day 31
From a progress standpoint I finished up with the new configuration framework.
I'm very happy with the way it turned out. It also got me to remove the special
testrig startup in favor of a seed-test-data
option so I no longer have to
maintain this seperately.
Reflecting on my december adventure, I couldn't be happier! I got a working federation setup and produced something useful! Sharepa still has some way to go before its first "live" version but now that the federation logic is implemented, I'm much more confident that it will be a usefull tool soon.
I learned a lot about ActivityPub and implementation patterns as well as sustainable go development patterns. While not doing much with them, HTMX and Alpine.js are also welcome additions to my toolkit. The most important thing I learned however was the usefullnes of a devlog. Once I've added some kind of notes system to this website, I might continue doing this.
If someone other than me is actually reading this, thanks! I don't have any smart closing words or something to sell you so I'll just leave you with one of my favourite quotes:
Geh raus, mach Sport und umgib dich mit Leuten, die dir gut tun.
(Go outside, be active and surround yourself with people who are good for you)