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.
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.
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:
- WebhookUpdate Class in line 54
- webhook_update function in line 88
- Handling bot updates like /start command in line 78 and webhook updates in line 88
- /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
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.
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]
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.
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.
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.
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.
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.
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.