How to serverless your parenting skills - or the story of how I learn bad dad jokes from a bot

A few months ago, my little devil Julián was born and I noticed that in order to be the SuperCoolDad(tm), I needed to up my humor — and as a software engineer what is better than writing software to easy my journey to humoristic gold.

Picture of me and Juli

How did I come up with this crazy bot idea?

The day before my son was born, my brother-in-law who is also a software developer arrived in Berlin. We had invited him to stay with us for a few months to help my wife and me out with our beloved baby boy. In his free time, my brother-in-law worked on improving his Python skills. He’s a massive Telegram user, an instant messaging service, and he had an idea to write a bot to track the FIFA World Cup 2018 games and statistics.

Soon after helping him get his bot set up, and seeing how it worked, I instantly noticed the enormous amount of value of an on-demand feed of relevant information.

Now I have a medium to deliver knowledge! A Telegram bot to tell me very bad jokes on demand!

Picture of the bot

Deciding on the technology stack

I now had an idea — I just had to decide how to execute it.

Then it hit me, bots have an exclusively on-demand (either push or pull) communication flow, and having used AWS Lambda for some experiments previously, I decided it was the perfect way to host the bot. As requests are very sparse and rather short-lived — a single function was all I needed have the complete life-cycle of the bot encapsulated.

Using the Serverless framework, a framework for creating AWS Lambda-based (and other similar stacks) applications, for automating the deploy pipeline, I now had a substantial basis to start my hacking.

Python was my language of choice. And I used Contentful as the backend to store my jokes and important notifications for users.

The Telegram Bot API

I have to say, that the Telegram Bot API is probably one of the best-designed bot APIs available out there because it provides extremely simple and clear endpoints with all the information required available at hand and very nicely put together.

Telegram has two methods of interaction for bots: Updates or Webhooks. With updates, you get a polling URL for a server to fetch new messages. With webhooks, every time a new message is fetched, Telegram will send a request to an endpoint of your choice.

For this bot, we are going to use the Webhook method of interaction, in which per each received message, an HTTP call is sent to a user-defined endpoint. In our case, our Lambda function. And for replies, we use the sendMessage endpoint.

Fetching Jokes

Disclaimer: All jokes quote their sources and are covered by Fair Use.

Now I have the pipeline decided, but I'm lacking content! So I started fetching jokes from Twitter feeds, scraping them using BeautifulSoup4 and storing them into Contentful.

With some jokes to start testing my bot and play around with it, I could now prove that my stack worked properly, and that I could have some fun. But content was very sparse, accounting only for a few dozen jokes.

Co-workers to the rescue! My manager, apparently a dad-joke master, recommended me an API for fetching dad jokes, which I stored in Contentful to be able to have them share my defined structure and be able to fetch them all at once, a few minutes later, I now had over 400 jokes to play with and start my humour education.

Writing bots made easy

It was amazing to learn how simple it is to add more functionality to this bot, To give you an example, this is what the /tellmeajoke command code looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import random
from models import Joke
from controllers.utils import send_message


def tell_me_a_joke(_message, _data, chat_id):
    jokes = Joke.all()
    joke = random.choice(jokes)

    response = "Here you go:\n{0}\n\nSource: {1}".format(
        joke.content,
        joke.source
    )
    send_message(response, chat_id)

This is all thanks to a nice abstraction I came up with to deal with bot commands, using a Dispatcher pattern to trigger all the commands. By providing all callable functions with a standard set of parameters, it was extremely easy to hook them all to a single endpoint, which would then trigger the correct command. This is the code that's used for that purpose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def endpoint_dispatcher(event, context):
    data = json.loads(event['body'])
    message = str(data['message']['text'])
    chat_id = data['message']['chat']['id']

    registered_functions = {
        '/start': start,
        '/tellmeajoke': tell_me_a_joke,
        '/createbroadcast': create_broadcast,
        '/forgetme': forget_me,
        '/help': help_
    }

    command = message.lower().split()[0]
    print('Called command: {0}'.format(command))

    if '/' not in command or command not in registered_functions:
        print('Command not recognized')
        send_message(help_text(), chat_id)
        return {"statusCode": 200}

    registered_functions[command](message, data, chat_id)
    return {"statusCode": 200}

This way, I was now able to simplify my code greatly.

Lessons learned

While I discovered that creating this bot is incredibly simple and fun to do, there are some key learnings from a few obstacles faced during its development:

  • Deploy to test - testing the code on the actual runtime requires a redeploy for every change, while this is a single command, it can take up to a minute to be ready.
  • No remote debugging - AWS Lambda doesn't allow for remote debugging. Therefore you rely heavily on logging, so better polish your logging skills!
  • Mocking is cumbersome - Creating a mock AWS Lambda instance locally, while possible, is a bit complex, making the "deploy to test" approach favorable for smaller projects even with its downsides. For larger projects, taking the extra effort to implement this, would be extremely important.

But of course, it's not all bad! So…

What are the advantages?

  • Fast - AWS Lambda + Contentful are an extremely great combination for delivering data on-demand, with Contentful's Delivery API, you get globally cached content with close to instant response times. AWS Lambda's average response time is also in the millisecond, which makes this incredibly efficient.
  • Easy - With Contentful SDKs, implementing a backend took close to no time. The Serverless framework makes dealing with AWS Lambda as easy as running a single command.
  • Iterable - Within the last few days, I've been adding some more functionality to the bot, like creating broadcasts from within the bot as an Admin-only command. Or the "forget me" functionality, for allowing users to unsubscribe from bot broadcasts. They took less than 5 minutes each to implement.

Next steps

Now with the bot built, of course, my personal next steps are to start learning some awesome jokes! But, for the bot itself, I'd like to migrate the user session storage to DynamoDB or a similar data storage, so that I can keep in Contentful only the actual content.

I also encourage all of you to check out the code, it explains step by step how to setup and deploy your own version of this bot.

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.