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.
- The Chat ID : The chat ID is like a serial number that uniquely identifies the chat between a bot and a user.
- 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.
- 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:
- Extract the Chat ID from the incoming message.
- Get a random NASA photo.
- Send the photo (along with its caption) to the incoming message’s Chat ID
- 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.
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:
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.