Building a Simple Telegram Bot Using Google Cloud Functions

Many websites use bots to automate tasks and add useful (and sometimes harmful) functionality. For instance, there are reddit bots that can help you stabilize shaky videos, remind you of events or even vote on the usefulness of other bots. Telegram - an instant messaging service similar to WhatsApp - lets you create and manage bots on their platform using their Bot API. Bots on Telegram are officially identified and provide fun and useful services. Last month, while exploring Google Cloud Platform after getting some free student credits, I came across Google Cloud Functions. I realized that this hammer was perfect for the nail of setting up a simple Telegram bot.

Many years ago, when Telegram’s bot API was still young, I tried to create a bot that would send you random pictures of aurorae if you asked. That bot and the server that it lived on crashed a long time ago. But the bot’s name and API key lived on, still registered with Telegram’s servers. I decided to necromance this bot from the dead and inject it with some fun new functionality. Being a lover space exploration and what it represents for humanity, I had the idea of giving the bot the ability to send you random images from NASA with informative descriptions as seen on the NASA Image and Video Library.

Using the NASA Images API

The first problem to solve is getting a random image from the NASA Image and Video Library. At first, I thought that I’d have to use a web-scraping python library to extract the images. But things turned out to be much easier. NASA has quite a few APIs that they’ve listed on this page. Using their API, I can search through the images for any query I like and retrieve results in the form of JSON formatted data. The API uses HTTP GET requests (more info here). So for example, if I need to search for images related to planets using the API, I would open the URL: https://images-api.nasa.gov/search?q=planet.

There’s one extra step here. If you click on the api link above and examine the results, you’ll notice that it does not return all the results of a search at once. Instead, it gives you the first 100 results and gives you the option of getting more using the ‘page’ parameter in the web request. So if I want to access the results from the 5th page of a search, I’d use the URL: https://images-api.nasa.gov/search?q=planet&page=5.

So, to select a random result, I need to select a number between 1 and the total number of results and use modular arithmetic to figure out which page to get the result from. I encapsulated this logic in a single function that returns the URL and caption of a random result given a search query.

import urllib.parse
import urllib.request
import json
import random
import math
import traceback

def get_random_nasa_image(search_term='planet'):
    """
    Fetch a random image from the NASA media library.
    """
    try:
        # The API URL
        nasa_img_url = "https://images-api.nasa.gov/search"
        # Setup the search data
        send_data = {}
        send_data['q'] = search_term
        send_data['media_type'] = 'image'
        # Encode the url
        url_values = urllib.parse.urlencode(send_data)
        url = nasa_img_url + '?' + url_values
        data = urllib.request.urlopen(url)
        json_data = json.loads(data.read().decode('utf-8'))
        
        num_results = json_data['collection']['metadata']['total_hits']
        result_to_use = random.choice([i for i in range(num_results)])
        page_num = math.ceil(result_to_use/100.0)
        result_num_in_page = result_to_use%100
        
        if page_num != 1:
            # Do another request
            send_data['page'] = page_num
            url_values = urllib.parse.urlencode(send_data)
            url = nasa_img_url + '?' + url_values
            data = urllib.request.urlopen(url)
            json_data = json.loads(data.read().decode('utf-8'))
            image_url = json_data['collection']['items'][result_num_in_page]['links'][0]['href']
            image_caption = json_data['collection']['items'][result_num_in_page]['data'][0]['description']
            image_title = json_data['collection']['items'][result_num_in_page]['data'][0]['title']
        else:
            image_url = json_data['collection']['items'][result_num_in_page]['links'][0]['href']
            image_caption = json_data['collection']['items'][result_num_in_page]['data'][0]['title']
            image_title = json_data['collection']['items'][result_num_in_page]['data'][0]['description']

        return (image_url, image_title, image_caption)
    except Exception as e:
        traceback.print_exc()
        err_url = 'https://upload.wikimedia.org/wikipedia/commons/3/3b/Gato_enervado_pola_presencia_dun_can.jpg'
        err_caption = 'Uh-Oh. Something went wrong. Here\'s a picture of a cat instead.'
        return (err_url, err_caption, json_data)

Sending the Image to Telegram

To make a Telegram Bot send an image to a user we need three pieces of information.

  1. The Chat ID : The chat ID is like a serial number that uniquely identifies the chat between a bot and a user.
  2. The Photo : There are a few different formats that telegram accepts the photo in. I chose the simplest option, a string with the URL to the photo.
  3. The Bot API Key : This is a long random looking string that you get when you create a bot. See instructions here to learn how to get your own.

The actual sending of the message is achieved by using more HTTP GET or POST requests. In this case I used the sendPhoto function defined in the API. Again, I encapsulated the functionality to send the photo into a single function.

def sendPhoto(chat_id, url, caption):
    sendPhotoUrl = 'https://api.telegram.org/bot{your-api-key}/sendPhoto'
    data = {}
    data['chat_id'] = chat_id
    data['photo'] = url
    data['caption'] = caption
    data = urllib.parse.urlencode(data)
    data = data.encode('ascii') # data should be bytes
    
    req = urllib.request.Request(sendPhotoUrl, data)
    
    with urllib.request.urlopen(req, timeout=10) as response:
        the_page = response.read()
        
    return the_page

Setting Up a Google Cloud Function

Google Cloud Functions allow you to execute a custom block of code when triggered by some kind of event - like it being a certain time of the day. Apart from Google, companies like Amazon and Microsoft also have their own versions of cloud functions.

Since my application logic was fairly simple, I opted to setup my cloud function from their web interface by following the instructions on this page. I kept all the default settings and opted to use Python 3.7 since that’s the programming language that I’m the most familiar with. The ‘hello_world’ function that they have setup is the function that will be called when the service is triggered.

Inside the function, I need to implement some very simple logic:

  1. Extract the Chat ID from the incoming message.
  2. Get a random NASA photo.
  3. Send the photo (along with its caption) to the incoming message’s Chat ID
  4. Return an HTTP OK response.

Here’s my code for the main function that’s called when an event is triggered. I’ve added some exception handling to the main logic as well.

def hello_world(request):
    """Responds to any HTTP request.
    Args:
        request (flask.Request): HTTP request object.
    Returns:
        The response text or any set of values that can be turned into a
        Response object using
        `make_response <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>`.
    """
    request_json = request.get_json()
    
    doneFlag = False
    try_counter = 0
    try_max = 5
    
    while not doneFlag:
        try:
            # Send back a random nasa photo
            photo, title, caption = get_random_nasa_image()
            print(photo)
            sendPhoto(request_json['message']['chat']['id'],
                      photo, caption)
            doneFlag = True
        except:
            print("Something Went Wrong. Trying again!")
            try_counter = try_counter + 1
            if try_counter > 5:
                doneFlag = True
                traceback.print_exc()
                sendPhoto(request_json['message']['chat']['id'],
                          'https://upload.wikimedia.org/wikipedia/commons/3/3b/Gato_enervado_pola_presencia_dun_can.jpg',
                          'I\'m Sorry, something went wrong. Here\'s a cat picture instead. :P')
            else:
                pass

    
    print(request_json)
    return f'HTTP/1.0 200 OK'

Connecting the Telegram Bot to the Cloud Function.

The final step is to connect the Telegram Bot to the Cloud Function so that the function is triggered every time the bot receives a message from someone. The Telegram API has a function for just that. setWebhook allows you to set a URL that gets called every time the bot gets a new message. All the message data is passed on in JSON format. To connect your bot to the cloud function that you just created, you need to set the webhook to the URL specified in the ‘Trigger’ tab of the function details page.

gcf_image

Demo

And we’re done! If there are no errors in the code, your bot should be triggered every time it receives a message. Here’s a demo of my bot working:

demo_image

Conclusion

Successes like these are the reason that I sometimes revive old projects. In the years that passed between my two attempts, some technologies had become cheap enough that I could use it nearly for free. In my last attempt to build the bot, I used a custom VPS server (basically a linux server) to try and run the bot. This meant that in addition to the logic for the bot, I needed to figure out how to get the bot to run on the server reliably. I often had to go back and restart the server or the script because it had got itself into an unexpected state. For cloud functions, there is no state. Each event invokes a new call of the function and if there is an error, the next function call isn’t affected by it. I also don’t need to worry about reliability and uptime because Google manages the service. Building systems like these are a great way of learning more about the inner workings of the internet and I hope that others who want to build their own Telegram bots (or other web based things) can use this article as a starting point.

Related

comments powered by Disqus