So remember a long time ago (Tuesday), when I blogged about using the Bluesky API with BoxLang? As expected, I'm following that up today with a look at using the Mastodon APIs. Personally, I'm down to just two social networks, Bluesky and Mastodon. Originally I was using Mastodon a lot more, but I've been vibing with Bluesky more lately so I tend to check it more often. That being said, whenever I release a new blog post, I've got an automated process to post to both, so I thought I should cover both for BoxLang as well.

Even better... I already did this in ColdFusion! Way back in October 2023, I blogged about the topic and even shared a simple ColdFusion component for it. That made 'translating' to BoxLang even easier.

Auth

As a refresher, authentication with Mastodon is simpler. In your profile, you go into your developer settings and create a token. That's it. All you need along with that is the server your account runs on. So I specified too environment variables for this, MASTO_TOKEN and MASTO_SERVER. I verified it like so:

MASTO_TOKEN = server.system.environment?.MASTO_TOKEN ?: '';
MASTO_SERVER = server.system.environment?.MASTO_SERVER ?: '';

if(MASTO_TOKEN == "" || MASTO_SERVER == "") {
	println('Ensure the Mastodon token env vars are set: MASTO_TOKEN and MASTO_SERVER');
	abort;
}

Posting Messages

As I said, you don't need anything more than that token, so posting a toot (message) is as simple as:

toot = 'Hello World from BoxLang, #now()#';

bx:http url='https://#MASTO_SERVER#/api/v1/statuses' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='formfield' name='status' value=toot;
}

Literally, that's it. You get a large post object back you can inspect if need be.

Using Images

How about images? Like Bluesky, they've got a different endpoint for that. Here's an example sending up a cat picture.

bx:http url='https://#MASTO_SERVER#/api/v2/media' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='file' name='file' file=expandPath('./cat1.jpg');
}

mediaOb = result.filecontent.fromJSON();

This returns a media object where all you need is the id, and to add it to your post, it's one line of code:

toot = 'Hello World from BoxLang, #now()#, with an image.';

bx:http url='https://#MASTO_SERVER#/api/v1/statuses' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='formfield' name='status' value=toot;
	bx:httpparam type='formfield' name='media_ids[]' value=mediaOb.id;
}

Here's a complete script showing this in action:

MASTO_TOKEN = server.system.environment?.MASTO_TOKEN ?: '';
MASTO_SERVER = server.system.environment?.MASTO_SERVER ?: '';

if(MASTO_TOKEN == "" || MASTO_SERVER == "") {
	println('Ensure the Mastodon token env vars are set: MASTO_TOKEN and MASTO_SERVER');
	abort;
}


toot = 'Hello World from BoxLang, #now()#';

bx:http url='https://#MASTO_SERVER#/api/v1/statuses' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='formfield' name='status' value=toot;
}

dump(result.fileContent);

// test with media

bx:http url='https://#MASTO_SERVER#/api/v2/media' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='file' name='file' file=expandPath('./cat1.jpg');
}

mediaOb = result.filecontent.fromJSON();
toot = 'Hello World from BoxLang, #now()#, with an image.';

bx:http url='https://#MASTO_SERVER#/api/v1/statuses' method='post' result='result' {
    bx:httpparam type='header' name='Authorization' value='Bearer #MASTO_TOKEN#';
    bx:httpparam type='formfield' name='status' value=toot;
	bx:httpparam type='formfield' name='media_ids[]' value=mediaOb.id;
}

dump(result.fileContent.fromJSON());

I turned this into a BoxLang class. It's a bit different from the Bluesky one in terms of API shape and I may address that at some point, but for now, here's that class:

class {

	property name="token" type="string";
	property name="server" type="string";

	public function uploadMedia(required string path) {
		checkAuth();

		bx:http url='https://#variables.server#/api/v2/media' method='post' result='result' {
			bx:httpparam type='header' name='Authorization' value='Bearer #variables.token#';
			bx:httpparam type='file' name='file' file=path;
		}

		return result.fileContent.fromJSON();

	}

	public function post(required string toot, image="") {
		checkAuth();

		bx:http url='https://#variables.server#/api/v1/statuses' method='post' result='result' {			
			bx:httpparam type='header' name='Authorization' value='Bearer #variables.token#';
			bx:httpparam type='formfield' name='status' value=toot;
			if(image !== '') {
				imageOb = uploadMedia(image);
				bx:httpparam type='formfield' name='media_ids[]' value=imageOb.id;
			}
		}

		return result.fileContent.fromJSON();

	}

	/*
	* Utility function to ensure auth is set. 
	*/
	private function checkAuth() {
		if(variables.token == "" || variables.server == "") {
			throw("Component initialized with blank, or missing, handle and password values.");
		}
	}
}

And an example using it:

masto = new mastodon(token=MASTO_TOKEN, server=MASTO_SERVER);

post = masto.post("Hello from the API, promise this is the last(ish) test.");
dump(post);

// now with an image test
post = masto.post("Honest, this should be the last test. Really.",expandPath('./cat1.jpg'));
dump(post);

println('done');

Here's my most recent test on one of my cooler bot accounts:

Post by @dragonhoards@mastodon.social
View on Mastodon

Fairly simple, right? If you take this along with the code from the previous post, you could easily automate posting to both in a few lines of code.

As before, you can find these samples in the BoxLang demos repo here: https://github.com/ortus-boxlang/bx-demos/tree/master/scripting Look for test_mastodon.bxs, test_mastodon2.bxs, and mastodon.bx. Enjoy!