← Back to portfolio
Published on 18th September 2019

Python Tutorial: Adapting a Telegram Client for ChatWars

The other day, I found myself coding a client for the instant messaging program Telegram—in particular, for the multiplayer game ChatWars.

ChatWars? What's That?

ChatWars is a text-based fantasy role-playing game that you can connect to via Telegram. 

Each ChatWars player belongs to one of seven "castles." Every eight hours, the castles attack each other. You want your castle to be on the winning side.

In between the skirmishes, you can go on quests for treasure, craft different kinds of weapons, potions and armor, and, of course, chat with other players. It's fun stuff.

Technically, because it runs on a chat program, ChatWars isn't an app. It's actually a collection of bots, which is pretty cool. To play the game, you just send messages to them.

It looks like this.

There's one event in ChatWars called a "foray," when an enemy player attacks one of your castle's villages. If you happen to have your phone on you when this happens, you'll get a notification and have the chance to stop them.

Tragically, I proved too weak to prevent this foray.

The problem, of course, is that a lot of the time, you won't have your phone on you. Or you'll be asleep—ChatWars has players from all over the globe. 

Or maybe you'll get the notification at work. Somehow I've never quite been able to bring myself to whip out my phone in a meeting to stop someone from Dragonclaw Castle from raiding my village.

That's why a couple days ago I found myself writing a Python script to do it for me.

Building a Telegram Client

Telegram is pretty friendly to developers. They offer not one but two well-documented APIs, one for building bots and one for building chat clients. Not having any familiarity with either API, I was gearing up to spend the better part of the afternoon poring through documentation—everybody's favorite way to spend their Saturday.

Fortunately, I found someone who'd done a lot of it for me, and I was happy to profit off of his hard work. As one of my professors liked to say, good artists borrow, but great artists steal.

Jiayu Yi has a very useful article on Medium that explains how to write an autoresponder for Telegram. His client is written with a very different purpose—sending your friends an automated message when you're away for a few days on vacation. But I was able to adapt it pretty easily, with only a few tweaks. 

It ended up being intuitive enough that, even if you only have a passing familiarity with Python, you'll be able to add to this script, setting it up to interact with ChatWars in more sophisticated ways and do basically anything that you can do as a player.

Credit Where It's Due

Obviously, this post is indebted to Jiayu's. Before you read any further, please check out his article (and consider reading some of the other posts on his blog). 

I'm not going to spend much time talking about his code; I'm going to focus on the places where mine differs from his.

If nothing else, you need to at least skim his article to learn how to authenticate yourself to the Telegram API and obtain an API ID and hash.

From Autoreplier to Autodefender

Let's dive right in.

# autodefender.py
# automatically respond to forays on ChatWars

import time
from telethon import TelegramClient, events
import emoji

# add your own details here ...
api_id = 111111
api_hash = 'ca6ee9913bc76ceba71e1749dd4338b4'

# ... and here
phone = '+111111111111'
session_file = 'username'
password = ''

if __name__ == '__main__':
    client = TelegramClient(session_file, api_id, api_hash, sequential_updates=True)


    async def handle_new_message(event):

        from_ = await event.client.get_entity(event.from_id)
        if from_.bot:  # ONLY respond to bots
            e = event.message.message # the contents of the bot's message to you

            if "You were strolling around on your horse" in e:
                print(time.asctime(), '-', 'You tried to stop a foray.')
                await event.respond("/go")
            elif "Not enough stamina" in e:
                print(time.asctime(), '-', 'You are tired. Time to defend.')
                await event.respond(emoji.emojize(":shield:Defend"))
            elif "Your body hurts, but" in e:
                print ('You failed to prevent the foray. Back to defending.')
                await event.respond(emoji.emojize(":shield:Defend"))
            elif "was crawling away" in e:
                print ('You prevented the foray. Back to defending.')
                await event.respond(emoji.emojize(":shield:Defend"))

    print(time.asctime(), '-', 'Auto-defending ...')
    client.start(phone, password)
    print(time.asctime(), '-', 'Stopped!')

The first half of this script doesn't need much explanation. It connects to Telegram, logs into your account, and starts listening for events to reply to. (Each incoming message is a separate Telegram event.)

The first significant change from Jiayu's script comes on line 27. His code reads:

if not from_.bot:

That line tells his script to only respond to real people, not bots. That's necessary in his program, so that his autoresponder can't get trapped in an infinite loop wherein his robot keeps messaging another robot and they go back and forth forever. (Also, he's presumably not worried about telling bots that he's on vacation.)

This program is different. To the contrary, we only want our client to respond to bots, which is what the ChatWars program technically is. So we want to write the opposite: if from_.bot:

This means that we can also do away with the if statement that comes immediately before that.

if event.is_private:

Again, in Jiayu's code, this is necessary. He doesn't want his autoresponder spamming public group chats. But this program won't be doing that, because now it's ignoring everything that's not a bot, and moreover, as we'll see, it only replies to messages that contain particular strings (which in practice only the ChatWars bot will send). That means this would be a wholly unnecessary check.

Watching for Forays

That's the other big difference between Jiayu's autoresponder and this program: unlike his, ours actually needs to know what the message that we're replying to says

We don't want to respond to every message that ChatWars sends us, just the ones that indicate someone is raiding one of our villages.

That's what line 29 does: 

e = event.message.message

When we receive a message from Telegram, this pulls the contents. Then with those four if/elif statements a few lines below, we can check to see if it contains certain strings like "You were strolling around on your horse", which is a phrase that all of Telegram's "you detected a foray" messages feature. 

if "You were strolling around on your horse" in e:
    print(time.asctime(), '-', 'You tried to stop a foray.')
    await event.respond("/go")

When our script detects one of those key phrases, it responds with a message that tells the ChatWars bot that we want to defend the village. The syntax is unchanged from the original program (await event.respond(STRING)), but obviously the contents of the reply are entirely different. For instance, to attempt to stop the foray, we send the command "/go".

The other three if statements check for similarly reliable key phrases that indicate whether our intervention was successful or not. Regardless of whether it was, we need to reply with ":shield:Defend", which tells ChatWars that we want to return to defending our castle. (That's basically what you want to be doing at all times in ChatWars, unless you're actively on a quest.)

Getting Emotional

But wait. What's this business with those lines that contain the method emojize()?

Many of the messages that you as the user send to ChatWars, the ones that give it commands, feature emojis. For instance, to return to defending our castle, the message that we need to send is actually:

Similarly, if we wanted to go to our workshop and start crafting items, we'd have to send:

You get the idea.

Emojis are Unicode characters, which are a bit of a pain to handle in Python. We could just directly plunk the Unicode into our strings, but that would mean looking up (and cluttering up our strings with) a bunch of stuff that looks like \U0001f600. Euch. 

Let's pretend we're not interested in doing that. Right now, this script only uses one emoji, but if we want to add more features later, that much Unicode could quickly become unmanageable.

The easiest workaround is to just install Python's emoji library, which makes it very simple to add emojis to strings. We can use the method emoji.emojize() to handle the conversion for us. (If we forgot to plug our string into that method, we'd literally be sending ChatWars text that said ":shield:Defend", and it would get confused.)

What Next?

By adding more if statements, watching for more key phrases, it would be easy to turn this script into a useful little automated client that could run in the background, handling some of the grindier aspects of ChatWars while you did other things.

For instance, you could program it to wait until your stamina was full, then do quests in the forest until you ran out again. Or to go to your workshop, craft a certain item until you ran out of materials, and then go buy more.

Earlier today I had the idea of having it record my fights in the arena. It probably wouldn't be too hard to identify patterns in ChatWars' rock-paper-scissors-style combat system and have the program formulate and execute an optimal strategy.

Although this article might suggest otherwise, I don't really spend enough time playing ChatWars to do that myself. But if there are any former stats majors out there looking for a side project to keep your hand in, let me know and I'll cheerfully profit off of your work too. Like I said, good artists borrow, great artists steal.