Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MohammadHosseinGhorbani/f3eb9ca2763511012ef09fa5c2a058c2 to your computer and use it in GitHub Desktop.
Save MohammadHosseinGhorbani/f3eb9ca2763511012ef09fa5c2a058c2 to your computer and use it in GitHub Desktop.
Making a Telegram game with Godot

Telegram Games

Telegram is a powerful messenger app with many features; such as games! You can use a Telegram bot and an HTML5 page to share your game in Telegram. The main part of this thing is the HTML5 web page that is your game, Telegram is used to share your game and record the players' score. We will use Godot for the HTML5 part, and Python for the Telegram bot.

Telegram bots can be created using any programming language, therefore Python is not a requirement.

You can read more about Telegram games here.

Let's get started!

The Telegram Bot

First things first. Creating a Telegram bot is super easy. Open @botfather in your Telegram app and start it. It should respond with a general help message. Now send the /newbot command and follow the instructions to create a new bot. We will use the bot token you just got in our code. Then send the /setinline command. Game bots are required to have inline query enabled, because most of the times the game is shared by inline messages. Once you enabled it, it's time to create your game. Send the /newgame command and follow the instructions. The game short name is also used in the code.

image

Bot Code

As I mentioned, we use Python. In my opinion, the best Python framework for Telegram bots is python-telegram-bot. They have an example of implementing a Telegram bot with a custom webhook (customwebhook.py). This is the exact thing that we need. Let me explain it a little bit. These are the main parts of the code that we need to change:

  1. WebhookUpdate Class in line 54
  2. webhook_update function in line 88
  3. Handling bot updates like /start command in line 78 and webhook updates in line 88
  4. /submitpayload path in line 126

As you can see there are three paths in our service, telegram, submitpayload and healthcheck. The telegram path should be set as the webhook URL for our bot. All Telegram updates will be sent to this. The healtcheck path is clear. It just has a small text as a response to know if the service is on. And the submitpayload that handles our custom updates. Currently, its update has two attributes, user_id and payload, but we need none of them. So let's change them.

@dataclass
class WebhookUpdate:
    """Simple dataclass to wrap a custom update type"""

    hash: str
    score: int

hash is a unique identifier that we store in a database to use for making changes and updating scores. score is the score the player has gained. We need to modify the custom_updates function to align with the WebhookUpdate.

    @flask_app.route("/submitpayload", methods=["GET", "POST"])  # type: ignore[misc]
    async def custom_updates() -> Response:
        """
        Handle incoming webhook updates by also putting them into the `update_queue` if
        the required parameters were passed correctly.
        """
        try:
            score = int(request.args["score"])
            hash= request.args["hash"]
        except KeyError:
            abort(
                HTTPStatus.BAD_REQUEST,
                "Please pass both `hash` and `score` as query parameters.",
            )
        except ValueError:
            abort(HTTPStatus.BAD_REQUEST, "The `score` must be a string!")

        await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload))
        return Response(status=HTTPStatus.OK)

The default start command responds with a piece of certain information about the webhook. Well we don't want our users to know about it; So let's replace it with a simple response.

async def start(update: Update, context: CustomContext) -> None:
    await update.message.reply_html("Hello!")

The bot should have a message handler or an inline query handler to send the game. Inline queries are suitable for this because they can be used in chats that the bot is not a member of.

async def inline_query(update, context):
    query_id = update.inline_query.id
    results = [
        InlineQueryResultGame(
            id='1',
            game_short_name='YOUR GAME SHORTNAME'
        )
    ]
    await context.bot.answer_inline_query(inline_query_id=query_id, results=results)

The game always has a play button that we also need to handle with Callback Query Handlers

image

async def callback_query(update, context):
    query = update.callback_query
    url = 'https://YOUR-GAME-HOST/?hash='

    if query.game_short_name == "YOURGAMESHORTNAME"
         uuid = str(uuid4())
         conn = sqlite3.connect('players.db')
         cur = conn.cursor()
         cur.execute("INSERT INTO games (user_id, hash, game_name, inline_id) VALUES (?, ?, ?, ?) ON CONFLICT (user_id) DO UPDATE SET hash=?, game_name=?, inline_id=?", (query.from_user.id, uuid, "YOUR GAME SHORT NAME", query.inline_message_id, uuid, "YOUR GAME SHORTNAME", query.inline_message_id))
         conn.commit()
         conn.close()

        await context.bot.answer_callback_query(callback_query_id=query.id, url=url+uuid)
    else:
        await context.bot.answer_callback_query(callback_query_id=query.id, text="This does nothing.")

Let me explain. When the user clicks a button, an update will be passed to this function. If the query data is your game shortname, a unique ID gets created and stored in a database along with the Telegram ID of the user, the game shortname, and the inline_message_id. You will see what we are going to do with these values.

To use uuid4, you need to import it first. from uuid import uuid4 You can also use any other unique text generator you want.

This is the SQL code to create the games table which is used.

CREATE TABLE "games" (
  "user_id"	BIGINT,
  "hash"	STRING,
  "game_name"	STRING,
  "inline_id"	BIGINT,
  PRIMARY KEY("user_id")
);

At the end, register the new handlers. (in the original code, it's in line 110)

    # register handlers
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CallbackQueryHandler(callback_query))
    application.add_handler(InlineQueryHandler(inline_query))
    application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update))

It's time to change the webhook_update function. It needs to be completely changed.

async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None:
    user_hash = update.payload
    score = update.score
    bot = context.bot

    conn = sqlite3.connect('players.db')
    cur = conn.cursor()
    cur.execute('SELECT user_id, game_name, inline_id FROM games WHERE hash=?', (user_hash,))
    info = cur.fetchone()
    if info:
        user_id, game_name, inline_id = info
        await bot.set_game_score(user_id=user_id, score=score, inline_message_id=inline_id)
    conn.close()

As you can see, the information we stored before has gotten from the database and with the method bot.set_game_score we update the score of the player. If the player beats the high score, Telegram sends a service message to the chat. image

If you have some experience of developing Flask websites and Telegram bots, you know the rest! Set the TOKEN variable and other hard code things. Also don't forget to set the bot webhook to your API url. Install the requirements, save the file, and run your code on a server!

python3 main.py # in some operating systems like Windows, use `python` instead.

Requirements of this code:

  • python-telegram-bot
  • uvicorn
  • asgiref
  • flask[async]

The Game Code

Same as Godot docs, I recommend Godot 3.x for web games. They provided these reasons and my experience with Godot 4 confirms them.

Projects written in C# using Godot 4 currently cannot be exported to the web. To use C# on web platforms, use Godot 3 instead.

Godot 4's HTML5 exports currently cannot run on macOS and iOS due to upstream bugs with SharedArrayBuffer and WebGL 2.0. We recommend using macOS and iOS native export functionality instead, as it will also result in better performance.

Godot 3's HTML5 exports are more compatible with various browsers in general, especially when using the GLES2 rendering backend (which only requires WebGL 1.0).

Do you remember your first 2D game, Dodge The Creeps? I found a Godot 3 version of it on GitHub. We use it as an example for this project. Clone the repository to get started. Dodge The Creeps for Godot 3.x

To update the player's score, we use the API we just made. You can use JavaScript.eval method anywhere to evaluate JavaScript code. In the file Main.gd, add this line at the end of the game_over function.

JavaScript.eval("fetch('https://YOUR-BOT-HOST/submitpayload?hash' + window.location.search.substring(3) + '&score=%s');" % score)

This line calls the fetch function in JavaScript, which sends an HTTPRequest to our API. window.location.search.substring(3) is the generated hash for this specific user. The URL would look like this:

https://YOUR-BOT-HOST/submitpayload?hash=6c1e7595-c19f-44b6-a6dd-7bb0c5f1d1c2&score=10

You can add a Button and set this code to its pressed signal. When players click the button, a page will be opened to share the game and the score.

func _on_Button_pressed():
    JavaScript.eval("TelegramGameProxy.shareScore();")

TelegramGameProxy will get added later.

image

Exporting The Game

Open the Editor settings and enable General>Input Devices>Pointing>Emulate Touch From Mouse. This will cause your touches to be recognized as mouse clicks. image

Head to Project>Export to open the Export menu. Add an HTML5 preset and enable VRAM Texture Compression/For Mobile. Make other changes you need, then click Export Project... to export your project as WebAssembly and HTML. image

In the export files, you should have an HTML file. rename it to index.html and add this script to the end of the <body> tag.

<script src="https://telegram.org/js/games.js"></script>

It adds TelegramGameProxy to share the game and the score. The last thing is running the server. You can use this Python script to host a local webserver to test your exported file. Save it as serve.py in the export folder and execute this command to run the server.

python3 server.py -n --root . # in some operating systems like Windows, use `python` instead.

Congratulations! You successfully created a fully working Telegram game. You can send it to your friends and enjoy playing it.

Summary

In this article, we created an API that handles Telegram API updates and custom updates. The API sends a Telegram game to other users and updates the players' scores whenever the game sends an HTTPRequest to it.

I would be happy to receive your questions and suggestions as a comment.

Read this article in Godot Forum

@hethon
Copy link

hethon commented Jul 9, 2024

Thank you for this amazing information. I have a question, though. How can we verify that the data we receive is genuinely from Telegram? How can we prevent advanced users from cheating? With the current setup, it seems like anyone could become the highest scorer simply by sending requests to our /submitpayload endpoint.

@MohammadHosseinGhorbani
Copy link
Author

MohammadHosseinGhorbani commented Jul 10, 2024

Thank you for this amazing information. I have a question, though. How can we verify that the data we receive is genuinely from Telegram? How can we prevent advanced users from cheating? With the current setup, it seems like anyone could become the highest scorer simply by sending requests to our /submitpayload endpoint.

@hethon

Thank you very much for sharing your opinion. Yes, it is true. The only security issue I covered was using an API to register the score so that the user does not have direct access to the Telegram bot, but I did not go further. There can be various methods for enhancing security. Two come to mind for me:

  1. Using Godot's HttpRequest instead of fetch in JS. I think this would cause the request to be sent server-side, making it difficult for the user to easily access the API. However, I am not sure if HttpRequest in Godot works server-side, but if it does, it would greatly facilitate our work!

  2. If HttpRequest in Godot does not solve the problem or if the API address is still exposed, the main and better method that comes to my mind is to create a database that both the game code and the bot have access to. Every time a user's score is supposed to be registered, a specific code or password is assigned to it in that database, and it is used in the link sent to the API. On the other hand, the bot code makes sure by checking the database that this request is actually sent from the game, and if not, it does not register the score. This way, a password is always needed to register a score, which a user cannot write on their own.

If you have any other questions, I would be happy to hear them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment