Lessons I learnt creating a Twitter reminder bot in go.

Add to bookmarks

Sat Nov 07 2020

Prelude

So recently, I wrote a Twitter bot to enlighten and encourage Nigerians to go out and vote during the 2023 elections in the wake of the #ENDSARS protests in Nigeria.

PS, If you don't know about it, I encourage you to read about how Nigerians are fighting for our right to live.

Building It

TLDR; You can find the code for this bot at github

So, I already wrote an article on how to create a bot in go here, so you can read that to figure out things like authentication and tweeting. In this article, I'm just going to be touching some more specific parts.

Splitting tweets

So, seeing how the bot picks content at random from the content.json file, I had to make sure that the generated content was split (at spaces, to avoid words breaking) into multiple tweets and that each tweet does not go past the max 280.

Relatively easy task, except the existence of special characters like the ā–‘ character that makes up the progress bar, is counted by twitter as two characters, as well as emojis.

My workaround was to go through each rune (basically a readable character) in the general content with runes := []rune(content). The reason why for _,c := range content wasn't used is that that splits the content into a sequence of bytes and some special characters (e.g Mandarin) are more than one byte, so working with a single byte out of that leads to an incomplete (and oftentimes unreadable character).

Then for each rune the bytes that make up that run are extracted. To workaround twitter's special counting, if the byte count is more than 2, it is counted as 2 characters instead of one (still unsure as to how Twitter determines which characters are counted as two for now). Then it recursively splits based on that.

Here is the code that makes up that part of the logic:

// since some characters count as two characters on twitter,
// from my checks, my assumption is if it is more than 2 bytes it is counted as two characters.
// so we need to split sequentially to support all characters available.
func splitTweets(content string, tweetLength int) []string {
    tweets := make([]string, 0)
    count := 0
    byteCount := 0
    runes := []rune(content)
    lastSpaceIndex := 0
    for _, r := range runes {
        bytes := make([]byte, 4)
        rLen := utf8.EncodeRune(bytes, r)
        if rLen > 2 {
            count += 2
        } else {
            count++
        }
        if r == 32 || r == 10 {
            lastSpaceIndex = byteCount
        }
        byteCount += rLen
        if count >= tweetLength {
            if lastSpaceIndex == 0 {
                tweets = append(tweets, content)
                return tweets
            } else {
                tweets = append(tweets, content[:lastSpaceIndex])
                remainder := content[lastSpaceIndex+1:]
                tweets = append(tweets, splitTweets(remainder, tweetLength)...)
                return tweets
            }
        }
    }
    // less than tweet length
    tweets = append(tweets, content)
    return tweets

}

Hosting/deploying.

Since it made no sense to provision a droplet/VM on the cloud somewhere to run a 30mb binary once per day, had to look for a cheaper alternative. Seeing how inexperienced I am with serverless technology, that wasn't really an option.

Another option was for me to use my site's droplet and have a cron worker run the binary daily, why didn't I go with this? I expected the repo was going to be updated frequently due to additional content so setting up some custom build/deploy webhook on my server to respond to PR merges also seemed like a headache.

What did I go with? Heroku, I set up a Heroku app and installed the https://devcenter.heroku.com/articles/scheduler to run a one-off task which calls the tweet command defined in my Procfile:

tweet: bin/sorosoke

It's particularly interesting to me, cause I actually got to use Heroku in an actual project, I've always sort of overlooked Heroku as a service.

Conclusion

It was quite an interesting side project and contributions are welcome to both the article and code improvements, It was quite the rushed project so it most likely still has a lot of things that could be fixed and improved.

Enjoy!