Creating a Bot for my site using Actors in F#

Date: 2024-10-25 | create | tech | fsharp | projects | one-million-checkboxes | actors |

I recently released One Million Checkboxes - a site that lets anyone check / uncheck checkboxes and have those synced across the internet.

As you might imagine, many people like to cause mayhem with public works which results in some crude shapes / words appearing relatively frequently. Largely I think this is part of the fun of making publicly-available tools - seeing what ppl do with it. But at the same time it's maybe not great to host these crude messages on my website.

To solve this I decided I would build a bot that would slowly introduce noise to the checkboxes. This lets the system run as it should but also means that no bad messages stay up forever.

In this post we'll go over how I built this bot using actors in F# with a MailboxProcessor.

One Million Checkboxes Overview

One Million Checkboxes is a website that displays a million checkboxes. When you check / uncheck a box, it gets synced to everyone else on the website.

  • Frontend: HTML + HTMX
  • Backend: F# + Giraffe
  • Data: In Memory

For us this means we need to update the data in memory if we want the bot to change the checkboxes.

See: How I built One Million Checkboxes.

Automated Bot using MailboxProcessor Actors

A MailboxProcessor in F# is basically a simple actor. An actor itself really is just a queue consumer:

  • Messages are stored in a queue
  • Actor consumes those messages and does some work

My bot's goal is to change boxes over time so that any bad messages eventually disappear (but don't do this so fast that it gets in the way of checking boxes).

If we think about what this needs - really it's just a long-running loop that has a delay. Every x seconds - check / uncheck a box. This fits pretty nicely into the idea of a cronjob and actors are just lightweight queue consumers and we can trigger events on a timer so actors seem to fit pretty nicely here.

I'm using F# to build my backend so I reached for a MailboxProcessor to build its core functionality. I built a small utility to create cron jobs using a MailboxProcessor for its implementation:

  • Create MailboxProcessor
  • It runs itself every x seconds
  • When it runs it calls the configured action
type CronTask = 
    | Run 

let createCronMailboxProcessor 
    (interval: TimeSpan)
    (action: unit -> Task<unit>)
    = 

    let mailbox = 
        MailboxProcessor.Start(fun inbox -> 
            let rec loop() = async {
                let! msg = inbox.Receive()
                match msg with
                | Run ->
                    do! action() |> Async.AwaitTask
                    do! Async.Sleep(int interval.TotalMilliseconds)
                    inbox.Post(Run)
                    return! loop()
            }
            loop()
        )

    mailbox.Post(Run)

Then I used this helper to run a function that checks / unchecks a random box every time it runs:

let updateRandomCheckbox 
    (serviceTree: CheckboxServiceTree)
    =
    task {
        let isChecked = 
            match random.Next(0, 3) with 
            | 0 -> true 
            | _ -> false 

        let index = 
            random.Next(0, serviceTree.CheckboxCount - 1)

        let repo = serviceTree.CheckboxRepo()

        do 
            repo.UpdateCheckbox
                {
                    IsChecked = isChecked
                    Index = index
                }
            |> ignore
    }

let createAutoChecker 
    (serviceTree: CheckboxServiceTree)
    =

    createCronMailboxProcessor
        serviceTree.CheckboxAutoCheckerInterval
        (fun() -> updateRandomCheckbox serviceTree)

Next

So yeah you can still go and draw crude messages on one million checkboxes - just know your creations have a lifetime and eventually will be scrubbed away into nothingness.

If you liked this post you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.