Image credit: Wikipedia

Telegram Chess Bot

The following is a re-upload of a post in my previous blog (which was destroyed by mistake). While I did not get any money from the Telegram Bot Prize, it was a fun weekend.


As some of you know, I recently graduated and am waiting for my job to start. One day while going out with friends, I was told about the Telegram Bot Prize. Having used Telegram for quite some time1, I decided to check it out. The Telegram Bot API looked interesting and so I spent the weekend on learning how to build a Telegram bot. The result is a Telegram Chess Bot @tgchessbot. It’s been a fun journey.

In this post, I will share the experience of building my first Telegram bot while giving an overview of how to build your own! I will try to be framework agnostic as far as possible but my examples will probably be in Python3.

Getting started

Steps:

  1. Register a bot with the BotFather Follow the instructions from BotFather section of the official Telegram bot page. It’s that easy!

  2. Customise your bot (Optional) Customise your bot by running commands like /setname, /setdescription, etc. in the chat with the BotFather. It’s all fully optional but I recommend you revisit this step once you’re done with your bot and have free time. The customisation with the highest impact, in my opinion, is /setuserpic where you can upload a photo to be used as your bot’s icon.

  3. Pick a framework and look at existing bot code examples Personally, I picked Python and studied Telepot. nickoala has done a great job with Telepot and provides an extensive documentation for it. Telepot acts as a Python wrapper over the Telegram Bot API (which interfaces with HTTP requests).

What I will cover:

Reading updates

A bot needs to be able to receive messages from and send messages to chats. Your bot receives messages from chats via getUpdates(). There are 4 types of updates:

  • Message
  • InlineQuery
  • ChosenInlineResult
  • CallbackQuery

I will focus mostly on the first 2 types of updates since you can make your bot do almost everything with these. ChosenInlineResult is an update that lets you know which inline query option was chosen by the user. CallbackQuery are a form of inline query result that comes from inline keyboard inputs.

Update objects are in JSON format that look something like these (‘12345678’ is a placeholder for actual chat and user ID):

Private chat

{'from': {'username': 'SozoS', 'id': 12345678, 'first_name': 'Davin', 'last_name': 'Choo'},'date': 1463382166,'message_id': 1347,'text': 'update from private chat','chat': {'username': 'SozoS', 'id': 12345678, 'type': 'private', 'first_name': 'Davin', 'last_name': 'Choo'}}

Group chat

{'message_id': 1351, 'entities': [{'length': 7, 'type': 'bot_command', 'offset': 0}], 'chat': {'type': 'group', 'id': 12345678, 'title': 'tgchessbot Test Channel'}, 'from': {'username': 'SozoS', 'id': 12345678, 'first_name': 'Davin', 'last_name': 'Choo'}, 'date': 1463382642, 'text': '/update from group chat'}

In the above examples, the message type is text. There are other types such as audio, document, photo, etc. For more, look up the Message object in the Telegram Bot API.

Notes:

  • Not every Telegram user has a registered username.
  • You can extract sender details using/modifying this code snippet that I wrote for @tgchessbot:
def get_sender_details(self, msg):
        '''Extract sender id and name to be used in the match'''
        sender_id = msg["from"]["id"]
        if "username" in msg["from"]:
            sender_username = msg["from"]["username"]
        elif "last_name" in msg["from"]:
            sender_username = msg["from"]["last_name"]
        elif "first_name" in msg["from"]:
            sender_username = msg["from"]["first_name"]
        else:
            sender_username = "Nameless"
        return sender_id, sender_username
  • If chat_id == sender_id, then it’s a human-to-bot 1-on-1 chat.
  • If chat_id != sender_id, then chat_id is the group chat id.
  • Bots have privacy mode automatically turned on. One key implication is that bots do not receive anything from group chats except messages starting with a slash ‘/’. That’s why the examples above had ‘/update from group chat’ as compared to ‘update from private chat’.

Sending replies

There are many forms of replies that your bot may send back upon receiving an update such as text messages, photos, audio files, stickers, locations, etc. For a full list, check out the Available methods section of the Telegram Bot API. For now, let’s just focus on sending text and photos back to the chat.

In Telepot, you can simply do it via

bot.sendMessage(chat_id, 'your text message reply')

There are 5 other optional fields that you may augment your message with: parse_mode, disable_web_page_preview, disable_notification, reply_to_message_id, and reply_markup. They are all pretty self-explanatory (details on Telegram Bot API page).

What I would like to highlight is the parse_mode option. Telegram supports Markdown, a way to stylise your replies with bold, italics, etc. In Telepot,

bot.sendMessage(chat_id, 'your *bolded* text message reply', parse_mode = 'Markdown')

Photos can be sent in a similar fashion in Telepot:

bot.sendPhoto(chat_id, open(filename, "rb"), caption = "This is some optional text")

Inline queries

So far, it’s been pretty straightforward - You get an update, you send a reply. What about inline queries? Below, I annotated some pictures from Telegram’s “inline bots” post.

Inline queries are not active by default in all bots. To activate this option, talk to the BotFather. Tell him /setinline, click your bot and give him a “placeholder” message. You may also activate /setinlinefeedback if you wish to know which option was selected by the user.

To respond to an inline query, use the answerInlineQuery() method. In Telepot:

bot.answerInlineQuery(query_id, <array of query results>)

The array may contain up to 50 items. Right now, Telegram supports 19 query result types including articles, photos, GIFs, audio files, etc. For a normal text reply, you may use “article”. Note that for photos, you cannot reply with an actual photo but must provide a photo_url. To learn more, read the Imgur section in Going further.

If you are using Telepot, I recommend using the Answerer helper class. It frees you from having to call bot.answerInlineQuery() every time and ensures at most one active inline-query-processing thread per user. It also simplifies inline query handling where you can handle everything in a compute_answer() function.

Here is my code snippet for @tgchessbot to handle inline queries dynamically:

query_id, from_id, query_string = telepot.glance(msg, flavor = "inline_query")
def compute_answer():
    bank = [{"type": "article", "id": "/start", "title": "/start", "description": "Starts the bot in this chat", "message_text": "/start"},
            {"type": "article", "id": "/help", "title": "/help", "description": "Displays help sheet for @tgchessbot", "message_text": "/help"},
            {"type": "article", "id": "/stats", "title": "/stats", "description": "Displays your match statistics with @tgchessbot", "message_text": "/stats"}]
    ans = [opt for opt in bank if query_string in opt["id"]]
    for opt in bank:
        print(query_string, opt["id"], query_string in opt["id"])
    return ans

self._answerer.answer(msg, compute_answer)

Going further

While developing, I always have a browser tab opened for the Telegram Bot API.

Besides /setuserpic, /setinline and /setinlinefeedback, one other neat thing to do with BotFather is to /setcommands for your bot. This gives you the same experience as chatting with the BotFather. After typing ‘/’, a list of commands will pop up. To get a feel of it, activate it then type the following (Don’t worry, you can overwrite this later):

newbot - create a new bot
token - generate authorization token
revoke - revoke bot access token
setname - change a bot's name
setdescription - change bot description
setabouttext - change bot about info
setuserpic - change bot profile photo
setinline - change inline settings
setinlinegeo - toggle inline location requests
setinlinefeedback - change inline feedback settings
setcommands - change bot commands list
setjoingroups - can your bot be added to groups?
setprivacy - what messages does your bot see in groups?
deletebot - delete a bot
cancel - cancel the current operation

Your bot should now have the same interaction capability as the BotFather. This is somewhat similar to the inline query mode except that the response is fixed. I would recommend having at least a help command for your bot.

If you are intending to develop your Telegram bot in Python, I strongly recommend checking out Telepot. The documentation covers more bot capabilities, such as custom keyboards, and provides clear examples of how to use Telepot. Just be patient and read through the whole README.md.

For a chess related program, consider python-chess by niklasf. It’s a solid and comprehensive chess resources written in Python.

To handle graphics in Python, I recommend checking out Pillow, a Python Imaging Library (PIL) fork2. I used it to draw dynamically generate chessboards in @tgchessbot.

To hook up your bot with an image uploading webpage, consider the imgur API. It has a Python API imgurpython available as well. You might need to use this if you wish to dynamically create/upload images, then reply that image to an inline query (without exposing your server URL). This is required since photo results for inline queries only take in URLs and not actual photos, as compared to sendPhoto in a normal reply. Do note that it takes time for your photo to be uploaded to the imgur server hence it may slow down your application3.

Hosting @tgchessbot on your own server

  • Register a bot with the BotFather
  • After installing Python3 and pip on a server, perform the following:
sudo pip3 install telepot
sudo pip3 install python-chess
sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms1-dev libwebp-dev tcl8.5-dev tk8.5-dev
sudo pip3 install Pillow
  • Download the code from my Github repo
  • Replace the telegram_bot_token variable (near the bottom of tgchessbot.py) with your own bot token from BotFather
  • Shoot up a screen and run python3 tgchessbot.py. Detach using Ctrl + A + D. The bot will continue running and handle messages in the background as long as your server is up.

References and Footnotes


  1. I really like it compared to Whatsapp although sadly many family and friends still use it, so I end up using both ^
  2. Why a fork? ^
  3. I originally tried displaying chessboards in inline queries but the uploading was slowing down my bot performance so I scrapped it. ^