Repository navigation
Implement UNO game bot with Telegram integration - #45
piojomd86-jpg wants to merge 1 commit into
Conversation
Added a Telegram bot implementation for a UNO game with commands for creating, joining, and playing the game.
📝 WalkthroughWalkthroughThe Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Bot as Telegram Bot
participant Handler as Command Handler
participant GameState as Games State
User->>Bot: /start
Bot->>Handler: start()
Handler->>User: Welcome message
User->>Bot: /crear_juego
Bot->>Handler: crear_juego()
Handler->>GameState: Initialize Juego instance
GameState-->>Handler: Game created
Handler->>User: Game created, share ID
User2->>Bot: /unirse <game_id>
Bot->>Handler: unirse()
Handler->>GameState: Add player to game
GameState-->>Handler: Player added
Handler->>User2: Joined game
User->>Bot: /empezar
Bot->>Handler: empezar()
Handler->>GameState: iniciar_juego(), repartir_cartas()
GameState-->>Handler: Game started
Handler->>User: Game started, your turn
loop Game Loop
User->>Bot: /jugar <card> or /robar
Bot->>Handler: jugar() or robar()
Handler->>GameState: Validate & update game state
GameState-->>Handler: Move completed
Handler->>GameState: pasar_turno()
GameState-->>Handler: Next player's turn
Handler->>User: State update
end
GameState-->>Handler: verificar_ganador()
Handler->>User: Winner announced
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@railway.toml`:
- Around line 345-349: The current lookup sets game_id by finding the first
games[gid] where any(j["user_id"] == user_id for j in games[gid].jugadores),
which ignores the chat and can pick the wrong game if the same user is in
multiple chats; change the search to match both update.effective_chat.id (or
chat_id) and user_id (e.g., require games[gid].chat_id ==
update.effective_chat.id AND any(j["user_id"] == user_id ...)) so the handler
resolves a game by chat+player; apply the same fix in the other occurrence
referenced (the /jugar and /robar handlers at the second block).
- Line 10: Remove the hard-coded Telegram bot token from railway.toml and
rotate/revoke the exposed token immediately; instead add a reference to the
environment variable TELEGRAM_BOT_TOKEN (used in main.py) in the deployment
config so the runtime reads os.getenv('TELEGRAM_BOT_TOKEN'); update any
CI/deploy env settings to store the new secret and ensure no literal token
remains in the repo or configs.
- Around line 315-316: Handlers that call obtener_estado(), jugar_carta(), and
robar must first verify the match has started; currently /estado, /jugar and
/robar run against juego = games[game_id] even in the lobby and crash because
there is no discard pile. Update each handler that uses juego (the places
invoking obtener_estado(), jugar_carta(), or robar) to check a single game-start
flag or state (e.g., juego.partida_empezada or juego.estado === 'in_progress')
and return an appropriate error/message if the match hasn’t started (allow
/empezar to flip the flag); ensure the same guard is added at the other similar
sites referenced (the other two diffs mentioned).
- Around line 75-95: agregar_jugador currently only blocks full tables and will
append duplicate user_id entries; change agregar_jugador to first check
self.jugadores for an existing entry with the same user_id and return False if
found (use self.jugadores and user_id to locate duplicates) instead of mutating
membership during /empezar, and keep the existing capacity check. Also modify
iniciar_juego to guard against restarts by checking self.iniciado at the top and
returning False if the game is already started; only call repartir_cartas, pop
from self.mazo, append to self.descarte and flip self.iniciado when the game was
not started. Ensure the same duplicate-check and iniciado-guard fixes are
applied to the analogous code at the other occurrence mentioned.
- Around line 210-216: The current creation uses a user-scoped key (game_id =
f"{chat_id}_{user_id}") causing multiple lobbies per chat; change the game key
to be chat-scoped (use chat_id or a string derived only from chat_id) when
checking and storing in the games dict so /crear_juego, /unirse, /empezar and
/estado all reference the same chat-level entry; update the creation code that
sets games[game_id] = Juego(...) and any lookups that currently expect
f"{chat_id}_{user_id}" to instead use the chat-scoped key, and ensure the Juego
constructor still receives the creator or owner id if needed (pass user_id
separately rather than in the dict key).
- Around line 1-10: The railway.toml currently contains Python bot code (e.g.,
declarations like TOKEN and games) which breaks the deployment config; move all
Python implementation (imports, TOKEN, games dict and bot handlers) into a
proper .py module (e.g., bot.py or inside the existing main.py) and remove it
from railway.toml, restore railway.toml to valid TOML that declares the project
and start command used by the runtime, and ensure the runtime still starts
main.py (as expected by the Dockerfile) and that TOKEN is read from an
environment variable rather than being hard-coded.
- Around line 291-297: The bot currently attempts to DM each player in the
juego.jugadores loop using context.bot.send_message (used in /empezar and the
turn-notification sites) which will raise "chat not found" for users who haven't
started the bot; wrap those send_message calls in a try/cept that catches
telegram.error.BadRequest (and fall back to a generic Exception), and on failure
send the same content to the group chat (use the game's chat id from juego or
the current context chat) with a mention of the player (username or display name
from jugador) so they still receive their hand/turn notice; update all
occurrences (the mano send in the juego.jugadores loop and the two
turn-notification send_message calls) to use this try/fallback pattern and log
the error for monitoring.
- Around line 4-6: The project imports from python-telegram-bot (e.g., "from
telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup" and uses
Application.run_polling()), but pyproject.toml only includes pytelegrambotapi;
update the pyproject.toml dependencies to include "python-telegram-bot>=20.0"
(and optionally remove "pytelegrambotapi" if you are fully migrating), ensuring
the dependency list matches the imports and runtime usage of Application,
CommandHandler, CallbackQueryHandler, MessageHandler, filters, and ChatAction.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| import os | ||
| import json | ||
| import random | ||
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup | ||
| from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes | ||
| from telegram.constants import ChatAction | ||
|
|
||
| # Variables globales | ||
| games = {} # {game_id: game_object} | ||
| TOKEN = "8688837364:AAHMpSjqLCy7WLYyaDSY7sxBI2nD7n9ZdFw" |
There was a problem hiding this comment.
Move this implementation out of railway.toml.
Railway expects this file to be valid TOML. Replacing it with Python makes the deployment config unreadable, and Dockerfile:34-35 still starts main.py, so this bot code is not wired into the actual runtime path. Restore a real railway.toml and move the bot implementation into a .py module.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 1 - 10, The railway.toml currently contains Python
bot code (e.g., declarations like TOKEN and games) which breaks the deployment
config; move all Python implementation (imports, TOKEN, games dict and bot
handlers) into a proper .py module (e.g., bot.py or inside the existing main.py)
and remove it from railway.toml, restore railway.toml to valid TOML that
declares the project and start command used by the runtime, and ensure the
runtime still starts main.py (as expected by the Dockerfile) and that TOKEN is
read from an environment variable rather than being hard-coded.
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup | ||
| from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes | ||
| from telegram.constants import ChatAction |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In the current python-telegram-bot documentation, which PyPI package provides telegram.ext.Application, and is Application.run_polling() a synchronous method or an async coroutine?
💡 Result:
The PyPI package is python-telegram-bot. telegram.ext.Application is provided by this package, as confirmed by the official PyPI page, GitHub repository, and documentation for v22.7 (latest as of 2026-03-16). Application.run_polling is an async coroutine method. Documentation describes it with parameters and usage in async contexts (e.g., await application.run_polling), source code context shows async methods, GitHub discussions confirm it's a coroutine that blocks the event loop via asyncio.run or similar until shutdown signals, and example code uses it directly in main without explicit await but in asyncio-based library.
Citations:
- 1: https://python-telegram-bot.readthedocs.io/telegram.ext.application.html
- 2: https://github.com/python-telegram-bot/python-telegram-bot
- 3: https://docs.python-telegram-bot.org/en/latest/telegram.ext.application.html
- 4: https://pypi.org/project/python-telegram-bot/
- 5: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/_application.py
- 6: https://docs.python-telegram-bot.org/en/stable/examples.pollbot.html
- 7: Having trouble running Application within another running loop... python-telegram-bot/python-telegram-bot#3310
- 8: http://python-telegram-bot.readthedocs.org/en/latest/telegram.ext.application.html
🏁 Script executed:
# Find pyproject.toml and examine its structure
find . -name "pyproject.toml" -type f | head -5Repository: aeither/telegram-bot-python
Length of output: 87
🏁 Script executed:
# Search for files that might contain the imports mentioned in the review
rg "from telegram import Update" --type pythonRepository: aeither/telegram-bot-python
Length of output: 101
🏁 Script executed:
# Look for Application.run_polling usage
rg "Application.run_polling" --type python -B 2 -A 2Repository: aeither/telegram-bot-python
Length of output: 101
🏁 Script executed:
# Check pyproject.toml to see dependencies
cat ./pyproject.tomlRepository: aeither/telegram-bot-python
Length of output: 497
🏁 Script executed:
# Find Python files in the repo
fd -e py | head -20Repository: aeither/telegram-bot-python
Length of output: 90
🏁 Script executed:
# Check main.py - see full content and line numbers
cat -n main.pyRepository: aeither/telegram-bot-python
Length of output: 1691
🏁 Script executed:
# Check commands.py - see full content and line numbers
cat -n commands.pyRepository: aeither/telegram-bot-python
Length of output: 544
🏁 Script executed:
# Search for railway.toml
find . -name "railway.toml"Repository: aeither/telegram-bot-python
Length of output: 85
🏁 Script executed:
# Search for the exact imports mentioned in the review
rg "from telegram import Update, InlineKeyboardButton"Repository: aeither/telegram-bot-python
Length of output: 155
🏁 Script executed:
# Check if there are files with 400+ lines that might contain lines 424-442
find . -name "*.py" -exec wc -l {} + | sort -rn | head -10Repository: aeither/telegram-bot-python
Length of output: 115
🏁 Script executed:
# View the entire railway.toml file with line numbers
cat -n railway.tomlRepository: aeither/telegram-bot-python
Length of output: 17754
Add python-telegram-bot to pyproject.toml dependencies
The code imports from telegram.* (lines 4-6 and throughout railway.toml), which requires the python-telegram-bot package. However, pyproject.toml only lists pytelegrambotapi as a dependency. This will cause an ImportError at runtime. Add python-telegram-bot to the dependencies:
dependencies = [
"python-telegram-bot>=20.0",
"pytelegrambotapi>=4.30.0,<5",
"python-dotenv>=1.2.1,<2",
]
Also remove the unused pytelegrambotapi dependency if migrating fully to python-telegram-bot.
Note: The code correctly awaits application.run_polling() at line 438, which is the correct usage since Application.run_polling() is an async coroutine method.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 4 - 6, The project imports from
python-telegram-bot (e.g., "from telegram import Update, InlineKeyboardButton,
InlineKeyboardMarkup" and uses Application.run_polling()), but pyproject.toml
only includes pytelegrambotapi; update the pyproject.toml dependencies to
include "python-telegram-bot>=20.0" (and optionally remove "pytelegrambotapi" if
you are fully migrating), ensuring the dependency list matches the imports and
runtime usage of Application, CommandHandler, CallbackQueryHandler,
MessageHandler, filters, and ChatAction.
|
|
||
| # Variables globales | ||
| games = {} # {game_id: game_object} | ||
| TOKEN = "8688837364:AAHMpSjqLCy7WLYyaDSY7sxBI2nD7n9ZdFw" |
There was a problem hiding this comment.
Remove and rotate the committed bot token.
This is a live credential in source control. main.py:1-13 already uses os.getenv('TELEGRAM_BOT_TOKEN'); follow that pattern here and revoke the leaked token before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` at line 10, Remove the hard-coded Telegram bot token from
railway.toml and rotate/revoke the exposed token immediately; instead add a
reference to the environment variable TELEGRAM_BOT_TOKEN (used in main.py) in
the deployment config so the runtime reads os.getenv('TELEGRAM_BOT_TOKEN');
update any CI/deploy env settings to store the new secret and ensure no literal
token remains in the repo or configs.
| def agregar_jugador(self, user_id, nombre): | ||
| """Agrega un jugador al juego""" | ||
| if len(self.jugadores) >= 4: | ||
| return False | ||
| self.jugadores.append({ | ||
| "user_id": user_id, | ||
| "nombre": nombre, | ||
| "mano": [] | ||
| }) | ||
| return True | ||
|
|
||
| def iniciar_juego(self): | ||
| """Inicia el juego""" | ||
| if len(self.jugadores) < 2: | ||
| return False | ||
|
|
||
| self.repartir_cartas() | ||
| # Primera carta del descarte | ||
| self.descarte.append(self.mazo.pop()) | ||
| self.iniciado = True | ||
| return True |
There was a problem hiding this comment.
Don't mutate lobby membership in /empezar, and block restarts.
If an existing player calls /empezar while the table still has free seats, agregar_jugador() adds the same user_id again because it only rejects full lobbies. Also, iniciar_juego() never checks self.iniciado, so /empezar can redeal a live match and corrupt hands and turn order.
Also applies to: 277-282
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 75 - 95, agregar_jugador currently only blocks
full tables and will append duplicate user_id entries; change agregar_jugador to
first check self.jugadores for an existing entry with the same user_id and
return False if found (use self.jugadores and user_id to locate duplicates)
instead of mutating membership during /empezar, and keep the existing capacity
check. Also modify iniciar_juego to guard against restarts by checking
self.iniciado at the top and returning False if the game is already started;
only call repartir_cartas, pop from self.mazo, append to self.descarte and flip
self.iniciado when the game was not started. Ensure the same duplicate-check and
iniciado-guard fixes are applied to the analogous code at the other occurrence
mentioned.
| game_id = f"{chat_id}_{user_id}" | ||
|
|
||
| if game_id in games: | ||
| await update.message.reply_text("❌ Ya hay un juego creado. Usa /unirse para unirte.") | ||
| return | ||
|
|
||
| games[game_id] = Juego(game_id, chat_id) |
There was a problem hiding this comment.
Use one active-game key per chat.
/unirse, /empezar, and /estado all resolve “the game in this chat”, but /crear_juego only checks the creator-specific key. A second user can create another lobby in the same chat, after which chat-scoped commands hit whichever dict entry comes first.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 210 - 216, The current creation uses a user-scoped
key (game_id = f"{chat_id}_{user_id}") causing multiple lobbies per chat; change
the game key to be chat-scoped (use chat_id or a string derived only from
chat_id) when checking and storing in the games dict so /crear_juego, /unirse,
/empezar and /estado all reference the same chat-level entry; update the
creation code that sets games[game_id] = Juego(...) and any lookups that
currently expect f"{chat_id}_{user_id}" to instead use the chat-scoped key, and
ensure the Juego constructor still receives the creator or owner id if needed
(pass user_id separately rather than in the dict key).
| # Enviar manos privadas | ||
| for jugador in juego.jugadores: | ||
| mano_texto = "\n".join([f"{i}: {c}" for i, c in enumerate(jugador["mano"])]) | ||
| await context.bot.send_message( | ||
| chat_id=jugador["user_id"], | ||
| text=f"Tu mano:\n{mano_texto}" | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
According to the Telegram Bot API, can a bot send a private message to a user who has not initiated a chat with the bot first?
💡 Result:
No, according to the Telegram Bot API, a bot cannot send a private message to a user who has not initiated a chat with the bot first. Bots can only respond in private chats after the user has sent a message to the bot, establishing the chat. Attempting to send a message without an existing chat results in errors like "Bad Request: chat not found" or "user not found". This limitation is a core design principle of the Bot API to prevent spam, as stated in official documentation and unchanged in recent updates up to Bot API 9.5 (March 2026).
Citations:
- 1: https://stackoverflow.com/questions/32226096/telegram-api-doesnt-send-message-to-user-until-receive-first-message-from-that
- 2: Bot cannot send message to private chat telegraf/telegraf#812
- 3: https://stackoverflow.com/questions/63852419/telegram-bots-can-i-now-start-a-chat-or-not
- 4: https://josxa.github.io/telegram-bot-api-docs/core.telegram.org/bots/index.html
- 5: https://core.telegram.org/bots/api
- 6: https://core.telegram.org/bots/api-changelog
- 7: https://core.telegram.org/bots/
🏁 Script executed:
# Check the other locations mentioned (381-385, 416-418)
# First, let's see the full context around those lines
rg -n "send_message" railway.toml | head -20Repository: aeither/telegram-bot-python
Length of output: 394
🏁 Script executed:
# Get line counts to understand the file better
wc -l railway.tomlRepository: aeither/telegram-bot-python
Length of output: 87
🏁 Script executed:
# Look at lines 381-385 and 416-418 for context
sed -n '375,390p' railway.tomlRepository: aeither/telegram-bot-python
Length of output: 757
🏁 Script executed:
# Also check 416-418
sed -n '410,425p' railway.tomlRepository: aeither/telegram-bot-python
Length of output: 657
🏁 Script executed:
# Search for any try-catch or error handling around send_message calls
rg -B 3 -A 3 "send_message" railway.tomlRepository: aeither/telegram-bot-python
Length of output: 2083
Telegram bots cannot send private messages to users who haven't started a chat with the bot first.
According to the Telegram Bot API, attempting to send a private message to a user who hasn't initiated a chat results in errors like "Bad Request: chat not found". This prevents /empezar from working for first-time players, as it sends private hands to all players (line 294) without verifying they've started the bot. The same issue affects turn notifications (lines 383 and 416) that send private messages to the current player.
Wrap these send_message calls with error handling or implement a fallback mechanism (e.g., send hands in the group chat instead, or collect acknowledgments before sending private messages).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 291 - 297, The bot currently attempts to DM each
player in the juego.jugadores loop using context.bot.send_message (used in
/empezar and the turn-notification sites) which will raise "chat not found" for
users who haven't started the bot; wrap those send_message calls in a try/cept
that catches telegram.error.BadRequest (and fall back to a generic Exception),
and on failure send the same content to the group chat (use the game's chat id
from juego or the current context chat) with a mention of the player (username
or display name from jugador) so they still receive their hand/turn notice;
update all occurrences (the mano send in the juego.jugadores loop and the two
turn-notification send_message calls) to use this try/fallback pattern and log
the error for monitoring.
| juego = games[game_id] | ||
| estado = juego.obtener_estado() |
There was a problem hiding this comment.
Guard gameplay commands until the match has started.
obtener_estado() and jugar_carta() assume there is already a discard pile, so /estado and /jugar can fail before /empezar. /robar is also callable in the lobby phase, which lets players mutate hands before the game even starts.
Also applies to: 355-356, 404-405
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@railway.toml` around lines 315 - 316, Handlers that call obtener_estado(),
jugar_carta(), and robar must first verify the match has started; currently
/estado, /jugar and /robar run against juego = games[game_id] even in the lobby
and crash because there is no discard pile. Update each handler that uses juego
(the places invoking obtener_estado(), jugar_carta(), or robar) to check a
single game-start flag or state (e.g., juego.partida_empezada or juego.estado
=== 'in_progress') and return an appropriate error/message if the match hasn’t
started (allow /empezar to flip the flag); ensure the same guard is added at the
other similar sites referenced (the other two diffs mentioned).
Added a Telegram bot implementation for a UNO game with commands for creating, joining, and playing the game.
Summary by CodeRabbit
Release Notes