modified | Tuesday 28 January 2025 |
---|
I’ve been using Bluesky a lot over the last few months and wanted to check out what the developer experience was like, building a simple bot felt like a good starting point. I’ve always like the old “Dumb” internet bots, the kind that just post a random saying or picture every so often and don’t require their own data center and nuclear reactor to run. I also love horrible dad jokes. So creating a bot that posts a random dad joke every day checked all the boxes for me.
I started by copying the Bluesky example bot from their repository. It is a very simple setup, create an environment file with the username and password for the bot account, compile the typescript and just run the index.js file. That gets you a bot that will post the smile emoji every 3 hours.
1import { BskyAgent } from '@atproto/api';
2import * as dotenv from 'dotenv';
3import { CronJob } from 'cron';
4import * as process from 'process';
5
6dotenv.config();
7
8// Create a Bluesky Agent
9const agent = new BskyAgent({
10 service: 'https://bsky.social',
11 })
12
13
14async function main() {
15 await agent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD!})
16 await agent.post({
17 text: "🙂"
18 });
19 console.log("Just posted!")
20}
21
22main();
23
24
25// Run this on a cron job
26const scheduleExpressionMinute = '* * * * *'; // Run once every minute for testing
27const scheduleExpression = '0 */3 * * *'; // Run once every three hours in prod
28
29const job = new CronJob(scheduleExpression, main); // change to scheduleExpressionMinute for testing
30
31job.start();
With most of the heavy lifting sorted out already it was just a matter of adding in some code to for getting a dad joke and updating the call to agent.post. Fortunately there is a Dad Joke API that I used in another one of my projects.I created an interface to handle the return from the API and added axios to handle the request.
1// Create interface for reponse from icanhazdadjoke
2interface JokeResponse {
3 id: string;
4 joke: string;
5 status: number;
6}
7
8async function postJoke(){
9 try {
10 // Fetch a joke from icanhazdadjoke
11 const response = await axios.get<JokeResponse>('https://icanhazdadjoke.com/', {
12 headers: {
13 Accept: 'application/json',
14 },
15 });
16
17 const jokeData = response.data;
18 // Authenticate with Bluesky
19 await agent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD!})
20 // Post the joke to Bluesky
21 await agent.post({
22 text: jokeData.joke,
23 });
24 console.log("Just posted: ", jokeData.joke);
25 } catch (error) {
26 console.error('Error fetching the joke:', error);
27 }
28}
29
And that was it, I had a bot that would post a dad joke every day at 9am. I ran it on my local machine for a few days and it worked perfectly.
Originally I had this bot running as a background process on my local machine. This works fine but I like to experiment on my local machine so its not always on and I did not want to hassle with setting up a cron job or anything like that. Besides a local cron job wouldn’t give me anything fancy like emails on failures or a history of the runs. And a small script like this? Perfect for Github Actions.
1# Github Action to post a joke to Blueksy every day at 9:00 AM by running index.js with node, action should be able to run on demand as well.
2name: Post Joke
3
4on:
5 schedule:
6 - cron: '0 9 * * *'
7 workflow_dispatch:
8
9# Two secrets should be set in the repository settings: BLUESKY_USERNAME and BLUESKY_PASSWORD
10jobs:
11 build:
12 runs-on: ubuntu-latest
13 env:
14 BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }}
15 BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
16 steps:
17 - uses: actions/checkout@v2
18
19 - name: Use Node.js
20 uses: actions/setup-node@v1
21 with:
22 node-version: '20.x'
23
24 - name: Install dependencies
25 run: npm install
26
27 - name: Run script
28 run: node index.js
Now for simplicity I decided that I would handle building the index.js file locally and just push that to the repository, you could absolutely set up a build step in the Github Actions workflow and for anything more sophisticated than this I would recommend it. And since commiting a .env file would send dependabot into a tizzy I’m using Github Secrets to store the username and password for the bot account.
The script itself does not change much. With Github handling the scheduling we can cut out the cron dependency and just call the postJoke function directly.
1import { BskyAgent } from '@atproto/api';
2import * as dotenv from 'dotenv';
3import * as process from 'process';
4import axios from 'axios';
5
6
7dotenv.config();
8
9// Create a Bluesky Agent
10const agent = new BskyAgent({
11 service: 'https://bsky.social',
12})
13
14// Create interface for reponse from icanhazdadjoke
15interface JokeResponse {
16 id: string;
17 joke: string;
18 status: number;
19}
20
21async function postJoke(){
22 try {
23 // Fetch a joke from icanhazdadjoke
24 const response = await axios.get<JokeResponse>('https://icanhazdadjoke.com/', {
25 headers: {
26 Accept: 'application/json',
27 },
28 });
29
30 const jokeData = response.data;
31 // Authenticate with Bluesky
32 await agent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD!})
33 // Post the joke to Bluesky
34 await agent.post({
35 text: jokeData.joke,
36 });
37 console.log("Just posted: ", jokeData.joke);
38 } catch (error) {
39 console.error('Error fetching the joke:', error);
40 }
41}
42postJoke();
And there you have it, a Bluesky bot running in Github Actions. About as dead simple as it gets. Check out the repository if you want to see the full code.