December Adventure 2024

2024-12-01    15 minute read

Time for my second annual December Adventure!

Last year, I did some work on ↹lature, and I learned a lot about which parts of December Adventure work for me (and which don't). I go over why I enjoy December Adventure so much in the "retrospective" section of the article, but in short, I think the main benefits are feeling free to spend as little time as you want on it (as long as you do something) and having the flexibility to go in whatever direction you feel motivated to go.

I encourage y'all to check out other participants as well! There's a pretty comprehensive list at https://eli.li/december-adventure#section, and the #DecemberAdventure hashtag is usually pretty busy this time of year (mostly on the Fediverse, but maybe BlueSky has some going on too?).

This year, my project is:

crofu - Incremental converse crowdfunding platform

In short, crofu is a weird crowdfunding platform I've been working on for the last two months (and ideating for the last two years), and it happens to be the project I have the most momentum on right now. While it would be cool to work on ↹lature again, I think it would be really good synergy to use ↹lature development as a proof-of-concept/dogfooding opportunity for crofu, so I'm trying to work to get crofu in at least a testable state before I move on to more ↹lature stuff.

Day 1

I forgot December Adventure was starting until like the night before the first, so I was in the middle of a refactor. I was gonna finish it up today, but I got distracted with some type safety improvements around IDs. Pretty much everything in the database (Profiles, projects, tasks) is indexed by a UUID, and I want to prevent myself from using the wrong UUID in the wrong situation, so I wrapped each of them with a distinct type (for example: struct IdProject(uuid::Uuid)) and impl'd a bunch of traits for them using a macro.

macro_rules! id_type {
	($name:ident) => {
		#[derive(sqlx::Type, serde::Serialize)]
		// This lets me use this type directly when interfacing with `sqlx`
		// instead of having to unwrap it with `id.0` every time
		#[sqlx(transparent)]
		#[derive(Debug, Clone, Copy, PartialEq, Eq)]
		pub struct $name(Uuid);

		// This is necessary beacuse I use the id sometimes when generating HTML, mostly in forms.
		impl std::fmt::Display for $name {
			fn fmt<'a>(&self, f: &mut std::fmt::Formatter<'a>) -> Result<(), std::fmt::Error> {
				write!(f, "{}", self.0)
			}
		}

		impl From<Uuid> for $name {
			fn from(uuid: Uuid) -> Self {
				Self(uuid)
			}
		}

		impl From<$name> for Uuid {
			fn from(id: $name) -> Uuid {
				id.0
			}
		}

		// these let me check for equality between this wrapper type and a bare UUID
		impl PartialEq<Uuid> for $name {
			fn eq(&self, uuid: &Uuid) -> bool {
				self.0 == *uuid
			}
		}

		impl PartialEq<$name> for Uuid {
			fn eq(&self, id: &$name) -> bool {
				*self == id.0
			}
		}
	}
}

pub(crate) use id_type;

A fairly modest start to my December Adventure, but a good start nonetheless!

Day 2

Finished up the refactor I mentioned yesterday. The basic idea was to make accessing the database a little more ergonomic: instead of big methods like database.get_tasks_by_project_id(project_id) and database.edit_project_by_id(project_id, edits), I'd instead call database.project(project_id).tasks() and database.project(project_id).edit(edits). This not only makes it clearer what table is being edited, but also makes naming functions a lot easier.

One of the things I didn't really concern myself with was dealing with indexing based on slugs (for example, when using a URL endpoint like crofu.net/my-profile/a-project and performing some operation on the corresponding project (usually getting info), but using the slug (a-project in this case) since it doesn't have access to the UUID. The way I have it now, I'm just using the old method of having a top-level database.get_project_by_slug() method, but I think I might have a database.project_by_slug(project_slug) that returns the same thing as database.project(project_id).

Day 3

Spent the evening doing some CSS and layout improvements, mostly around the pledge editing box. My two main inspirations when it comes to website style are https://sr.ht and https://comradery.co, and it really shows. I'm not too worried about aesthetics or anything, and the only goal right now is to make it clear how to use the website. As a result, I don't have much to say about it, but here's a screenshot:

A webpage titled 'add cool feature XYZ to frontend'. In the center is a dark box listing the current pledge amount, an input box with the current pledge amount again, a checkbox labelled 'Pledge anonymously', and two buttons: a blue one labelled 'make pledge' and a red one labelled 'retract pledge'. Under the pledge entry box is a table listing the current pledges, of which there is currently only one.

Hopefully it's obvious which parts of the page do what.

Day 4

Today was mostly a cleanup day:

  • Fixed an issue where /foo resolves correctly, but /foo/ does not
  • Fixed project and task editing
  • Messed with the CSS on the login page, and added links to /register in /login, and vice versa
  • Added a little icon for private projects

Day 5

Added dashboard for general profile operations. The first page I added was to show all the pledges you've made so far:

A webpage titled 'All pledges', featuring a table listing tasks, their projects, and the amounts pledged to each task. Above the table is a total listing '$36'.

I also changed the URL format from crofu.net/my-profile to crofu.net/~my-profile, like how sourcehut does it. With the previous method, I had to block out a bunch of profile names like "dashboard" and "profile" and "login" so they wouldn't collide with the existing URLs. I might still make a list of forbidden usernames, but I'll probably stick to terms like "admin" that might be confusing. Since I had a single url.rs that I put all of my URL definitions in, this change was pretty quick and easy: (see diff)

I think tomorrow is going to be a documentation day, since I've got a few emails and info forms that I want to write.

Day 6

Wrote two emails, one to a local co-op and one to a lawyer specializing in co-ops. The one to the local co-op was pretty straightforward, just a "hi please help" sort of thing. I might publish the one I sent to the lawyer if I get their permission (technically, I think I'm allowed to do whatever, but I think it would be courteous to at least let them know first).

Wanted to write the info sheet that I'm sending out to creators for the trial run, but I didn't get around to it. I've also got one more email to write.

Day 7

Spent most of the day researching cooperatives and legal resources, and didn't actually put pen to paper. I think this sort of activity, while helpful, kinda goes against the spirit of December Adventure. Rather than planning every last detail, I feel like it's more about being able to shake off the stun and create something, no matter how minor or ill-conceived. Much of the stuff I worked on last December Adventure was refactoring work, and it's likely that I'll refactor it again, but that doesn't mean that it's wasted. A lot of the time, it's much faster and easier to try something out and see what goes wrong than to try to plan ahead for every hypothetical.

Plus, that's the main problem I've been having with crofu development so far: I've been doing so much planning and worrying and hypothesizing, and I have very little actual outcome to show for it, and I think the best way forward is to just put something together and put it out there, and lay out the tracks as I'm riding the train. There are a couple caveats, namely that I'm working with money here, and I'd like to be careful on that front, but even there, I should focus more on having conversations and reaching out to more people who have actually gone through this before rather than scouring the internet for blog posts and videos.

The goal tomorrow is to get back to coding.

Day 8

Added markdown support for descriptions. A super straightforward change, just cargo added a markdown package and wrapped a maud::PreEscaped(markdown::to_html(description)) around the existing description.

Faffed around a bit trying to figure out how object storage works, then I realized I'm probably overengineering, and decided just to store files on the local filesystem for the short term. We'll see how that pans out.

Day 9

Another quick day: added a "complete task" button and reorganized some files. Gave up on image uploads for now because they seem like a hassle and I don't really need them for the initial test.

Day 10,11

Totally missed day 10 because I was working on a sculpture (article incoming soonish), but day 11 was pretty successful. I'm still in the "haphazard improvisational slapping together" phase of the project (which frankly might never end), but I've got a basic payment flow working, which is one of the last parts that I actually need to run my trial. It's using an internal wallet with fake money, which definitely won't be how it works for the final project, but for now, it lets me run my self-funded trial phase. Looking forward, I think the best thing to do would be to make this a formal payment process so that people pay for pledges individually, but if it's not to legally fraught, I think a wallet would be worth considering. It allows people to distribute "free credits", either at a project level or at the site level, which could be a neat way for projects to interact with their communities. No clue what the tax implications are there though, so the most complicated I think I'll get is a cart system where people can pay for multiple pledges in a single transaction (though under the hood, it'd still be separate transactions, like how Bandcamp works).

Apparently Overwatch is doing a 6v6 test thing next week, so I'm trying to get to a solid foundation on this project by then so I don't have to be working on any big sweeping changes.

Day 12

Spent the evening getting an account set up at https://mythic-beasts.com. They put my order on hold for manual confirmation, but other than that, the setup was actually pretty straightforward. I ran into a lot of errors, but they all had clear error messages, and I learned a lot while fixing them. This is in stark contrast to my experience with ovhcloud, where I had no idea what was going wrong (sometimes I wasn't even aware something was wrong), and it was always something stupid, and usually their fault rather than mine. Also, mythic beasts uses a pretty functional html-driven site, again in contrast to ovhcloud, which is like 4 different single page webapps composed primarily of javascript.

Also tested lettre, an email library. I was kinda surprised how easy it was to just send an email. To be fair, I had already done a lot of the config inputs at arms length because I've been using meli and sending from a custom domain. But even so, its cool that old internet protocols like SMTP just kinda work.

Day 13

Short day, just finished setting up things on https://mythic-beasts.com. Got crofu running on my virtual server and verified that email is working, and was able to access it through the URL I bought for it. Mythic beasts continues to be excellently clear about capabilities and available services (and their costs).

Documentation is hit or miss though:

  • I thought I just needed to set up a mail address to be able to send emails via SMTP, but apparently I also needed a mailbox. Once I knew that's what I needed to do, though, it was really easy to set up.
  • Setting up SPF records and DKIM (email security stuff) were literally a single click each. Setting it up for this site (jaxter184.net) was 25 minutes of trial and error.
  • Still haven't gotten HTTPS, but I think I'm fine just using HTTP for the initial tests. They have clear documentation for HTTPS on their web hosting service, but not for their VPS service.

My total costs so far:

  • mythic-beasts
    • VPS (~$7/mo)
    • email (~$2/mo)
  • crofu.org domain registration ($15/yr)

Certainly not nothing, but it's wild to me that I can get someone to manage my server and email stuff for less than the cost of a Netflix or Spotify subscription. Though that maybe says more about Netflix and Spotify than it does about VPSes.

Tommorrow I'll be back to actual crofu development. The current plan is to get the login flow working smoothly, with clear error messages.

Day 14

Actually, I send credentials with the URL in a POST message, so I actually need to use HTTPS. I'll do that later though.

Today I wrote some shell scripts for testing web interaction. One of the things that the test revealed is that I've been using content tags when I should've been using void tags in a few cases (ex: <br></br> instead of just <br>), so I spent a few minutes fixing that.

paths="/ /register /login /fake /~jaxter184 /~jaxter184/tlature /~jaxter184/tlature/task/plugin-iir-eq"
for ea_path in $paths
do
	echo "=================" $ea_path "================="
	curl http://0.0.0.0:3000$ea_path --silent > out.html
	xmllint --html out.html > /dev/null
done

It seems to be falsely flagging my <nav> blocks, apparently because they're an HTML5 feature. Gonna ignore for now.

Also forgot to add money to the wallet of the task owner when a pledge is paid so I did that too. Did some weird stuff with postgresql transactions that I frankly don't fully understand. I'll have to come back to this before I start processing other peoples' money.

Didn't get around to improving login error messages, but I'll kick the can on that one.

Day 15

Finally improved the login error messages. I think it looks a little simple, but I'll do some trials and see how usable it is before making any more changes to how it looks. Also implemented email registration. Basically how it works is that there's a SQL table of user profiles and secret codes (UNIX timestamp plus a UUID):

create table if not exists email_verify
(
	id UUID primary key not null references "profiles"(id),
	code text not null unique
);

When the user registers, it creates an entry in the database, generating a new code. The code is overwritten whenever a new verification email is resent (which also happens whenever the email changes). The body of the email has a link that looks like https://crofu.org/verify-email?v=<insert secret code here>, which is a page that just checks whether the logged-in user's secret code in the table matches the one that's given in the URL parameter, and if it is, then it marks a column in the profiles table indicating that its email is verified.

Verifying your email doesn't do anything yet, but in the future, I think it'll be a prerequisite for donating and creating projects (pledging should have as few barriers as possible though).

Day 16

Literally spent my entire evening playing Nomia (very fun game despite being fairly early in development).

To abide by the December Adventure spirit, I added a few registration tests using the curl -> xmllint process I showed on Day 14. It involved using some XPath queries, which can get pretty gnarly depending on the website:

xmllint --html out.html --xpath '/html/body/div/form/fieldset/table/tr/td/label[@for="username"]/../../following-sibling::tr[1]/td[1]/text()'

XPath Breakdown

/html/body/div/form/fieldset/table/tr/td/label: The first part is pretty straightforward, and just goes down the tree looking for all the heirarchies that fit the pattern:

<html>
	<body>
		<div>
			<form>
				<fieldset>
					<!-- etc -->

[@for="username"]: This part checks the attributes of the <label> tags to find one that looks like <label for="username">

/../..: This just goes up the tag heirarchy the same way it works in a file path

/following-sibling::tr[1]: This is a pattern I found that just looks for sibling nodes (nodes that are at the same heirarchy level and share the same parent) and picks the first one

/td[1]: Picks the first <td> child node

text(): Prints the text contents, i.e. <tag>this part of the xml data, excluding the tags</tag>

Day 17

Added an account page for checking email verification status

Day 18

Added a button to the account page to resend the verification email. It's one of the slowest parts of the website since it has to do the whole email sending thing in the background. There's probably something I could do on that front, but I'll leave that for later.

Day 19

Added a rudimentary version of the expected donation score (a more complete version is described here: https://wiki.jaxter184.net/icc/scoring/). While doing this, I ran into a bunch of annoying SQL bugs. At one point, I converted all the sqlx macros into regular function calls, which skips some convenient checks on the SQL queries. I had originally done it to try to get it to compile on fly.io, but since I'm not using fly.io anymore, I think I'll convert them back.

Day 20

Despite spending 2.5 hours of my day playing Overwatch right after work, I ended up accomplishing a lot today.

The main thing I did today was, as I foreshadowed, changing all the sqlx function calls back into macros. There were a couple weird things to do with the enum types, but I just tossed a as "task_state: _" after every SELECT field in the SQL command and an as _ after every query argument in the Rust code, and that seemed to solve pretty much all the issues I was having with it.

  • Added error messages when you fail a login
  • Fixed a few miscellaneous bugs and refactored some currently unused task deletion code
  • Implemented HTTPS (basically just had to copy the axum rustls example and run certbot certonly --standalone)
A partial screenshot of a login page. The URL bar at the top shows a lock icon, indicating successful TLS encryption.
Note the 's' in the URL bar