Repository navigation
Update commands.py - #57
mahangames57-droid wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThe PR completely replaces a synchronous telebot command registration system with an async python-telegram-bot application. It implements a multi-step conversation to collect channel ID, message text, and schedule time; stores configuration in users.json; schedules daily cron jobs via AsyncIOScheduler; restores persisted jobs on startup; and provides /stop and /cancel controls for disabling and cleanup. ChangesAsync Telegram Bot Scheduler
Sequence DiagramsequenceDiagram
participant User
participant Bot as python-telegram-bot
participant Scheduler as AsyncIOScheduler
participant Storage as users.json
participant Channel as Telegram Channel
User->>Bot: /start
Bot->>Bot: check_admin_callback
Bot->>User: Prompt channel ID
User->>Bot: Send channel ID
Bot->>Bot: receive_channel (verify bot admin status)
Bot->>User: Prompt message text
User->>Bot: Send message
Bot->>Bot: receive_message
Bot->>User: Prompt schedule time (HH:MM)
User->>Bot: Send time (HH:MM)
Bot->>Bot: receive_hour (validate timezone)
Bot->>Scheduler: Create daily cron job
Bot->>Storage: Save user config (active=true)
Bot->>User: Conversation ended
Note over Scheduler: Daily at scheduled time
Scheduler->>Bot: Trigger send_daily_message
Bot->>Storage: Load user config
Bot->>Channel: Send message
User->>Bot: /stop
Bot->>Storage: Mark active=false
Bot->>Scheduler: Remove job
Bot->>User: Acknowledged
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ 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: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@commands.py`:
- Around line 32-40: Handle corrupted or partially-written users.json by
updating load_data to catch json.JSONDecodeError and return {} (and optionally
log a warning) so restore_jobs() won't crash on startup, and change save_data to
write to a temporary file and then atomically replace DATA_FILE (e.g., write to
DATA_FILE + ".tmp" and use os.replace) to avoid in-place partial writes;
reference the load_data and save_data functions and DATA_FILE constant when
making these changes.
- Around line 21-23: The module currently resolves TOKEN at import time (TOKEN =
os.environ.get(...)) which runs before load_dotenv() in main; remove the
top-level TOKEN assignment and instead read the BOT_TOKEN inside main() after
calling load_dotenv(), validate it and exit/fail fast if missing, or provide a
commands.get_token() accessor that reads env at call-time; update any code that
relied on the module-level TOKEN to accept the token as an argument or call the
new accessor so the real value from .env is used.
- Around line 243-274: The repo's old entrypoint expects register_commands
(imported by main.py) but commands.py removed it and now only exposes main();
add a compatibility shim named register_commands in commands.py that either (a)
wraps/forwards to the new main() startup flow or (b) performs the previous
minimal bootstrap (creates/returns whatever the old main.py expects, e.g., a
TeleBot-like object or a non-blocking startup handle) so imports don't
fail—implement register_commands() near the top-level of commands.py and ensure
it references the same Application.builder()/scheduler setup or simply delegates
to main(), preserving the old import contract.
- Around line 163-170: The confirmation reply inserts user-provided
context.user_data["message_text"] (message_text) into update.message.reply_text
with parse_mode="Markdown", which can break for Telegram legacy Markdown; escape
Markdown special characters in message_text before calling reply_text (or switch
to parse_mode="HTML" and escape accordingly) so the confirmation cannot fail —
locate where message_text is read/used and apply a Markdown-escaping function to
message_text prior to passing it to update.message.reply_text (or remove
parse_mode and send plain text).
- Around line 8-18: The file currently mismatches the project entrypoint and
deps: remove PTB-specific main() and export a register_commands(bot:
telebot.TeleBot) function that main.py expects (reference symbol:
register_commands) so main.py can register handlers on the telebot instance;
align the token env var by replacing TOKEN = os.environ.get("BOT_TOKEN", ...)
with reading TELEGRAM_BOT_TOKEN (or centralize env var usage to
TELEGRAM_BOT_TOKEN) so the token name matches README/main.py; eliminate imports
on unused PTB/apscheduler/pytz or move those features behind optional flags and
list required packages in pyproject.toml so clean installs succeed; in
receive_hour(message_text) stop passing raw user text into parse_mode="Markdown"
— escape or sanitize message_text before sending (reference symbol:
receive_hour); and make load_data()/save_data() robust by adding JSON parse
error handling and atomic writes (read with try/except JSONDecodeError and on
save write to temp file then os.replace) to avoid crashes or partial writes
(reference symbols: load_data, save_data).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup | ||
| from telegram.ext import ( | ||
| Application, | ||
| CommandHandler, | ||
| MessageHandler, | ||
| CallbackQueryHandler, | ||
| ConversationHandler, | ||
| ContextTypes, | ||
| filters, | ||
| ) | ||
| from apscheduler.schedulers.asyncio import AsyncIOScheduler |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
import tomllib
from pathlib import Path
deps = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["dependencies"]
needed = ["python-telegram-bot", "apscheduler", "pytz"]
print("declared dependencies:")
for dep in deps:
print(" -", dep)
print("\nrequired by commands.py:")
for name in needed:
present = any(dep.lower().startswith(name) for dep in deps)
print(f"{name}: {'present' if present else 'MISSING'}")
PYRepository: aeither/telegram-bot-python
Length of output: 242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print relevant dependency block
echo "=== pyproject.toml dependencies ==="
python - <<'PY'
import tomllib
from pathlib import Path
p = Path("pyproject.toml")
data = tomllib.loads(p.read_text(encoding="utf-8"))
deps = data.get("project", {}).get("dependencies", [])
print("\n".join(deps))
PY
# Inspect commands.py sections (imports, token loading, persistence, receive_hour)
echo
echo "=== commands.py (lines 1-260) ==="
nl -ba commands.py | sed -n '1,260p'
echo
echo "=== commands.py (search tokens/persistence/receive_hour) ==="
rg -n --hidden --no-ignore-vcs "(BOT_TOKEN|load_data|save_data|receive_hour|markdown|Markdown|parse_mode|message_text)" commands.py
# Inspect main.py for register_commands / load_dotenv order
if [ -f main.py ]; then
echo
echo "=== main.py (lines 1-220) ==="
nl -ba main.py | sed -n '1,220p'
else
echo
echo "main.py not found"
fi
echo
echo "=== main.py imports from commands.py ==="
rg -n --hidden --no-ignore-vcs "(from commands import|import commands|register_commands|load_dotenv|BOT_TOKEN)" main.py || true
echo
echo "=== repo-wide references to register_commands / load_data / receive_hour ==="
rg -n --hidden --no-ignore-vcs "(register_commands|load_data\\(|save_data\\(|receive_hour|BOT_TOKEN|BOT_TOKEN|BOT_TOKEN|BOT_TOKEN)" .Repository: aeither/telegram-bot-python
Length of output: 256
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== pyproject.toml dependencies ==="
python - <<'PY'
import tomllib
from pathlib import Path
data = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
deps = data.get("project", {}).get("dependencies", [])
print("\n".join(deps))
PY
echo
echo "=== commands.py (lines 1-260) ==="
if [ -f commands.py ]; then
cat -n commands.py | sed -n '1,260p'
else
echo "commands.py not found"
fi
echo
echo "=== commands.py (search tokens/persistence/receive_hour) ==="
rg -n --hidden --no-ignore-vcs "(BOT_TOKEN|load_data|save_data|receive_hour|markdown|Markdown|parse_mode|message_text)" commands.py || true
echo
echo "=== main.py (lines 1-220) ==="
if [ -f main.py ]; then
cat -n main.py | sed -n '1,220p'
else
echo "main.py not found"
fi
echo
echo "=== main.py imports from commands.py ==="
rg -n --hidden --no-ignore-vcs "(from commands import|import commands|register_commands|load_dotenv|BOT_TOKEN)" main.py || true
echo
echo "=== repo-wide references to register_commands / load_data / receive_hour ==="
rg -n --hidden --no-ignore-vcs "(register_commands|load_data\\(|save_data\\(|receive_hour|BOT_TOKEN)" . || trueRepository: aeither/telegram-bot-python
Length of output: 15078
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== commands.py: search for register_commands ==="
rg -n --hidden --no-ignore-vcs "def register_commands|register_commands\s*\(" commands.py || true
echo
echo "=== commands.py: tail (rest of file) ==="
tail -n 80 commands.py
echo
echo "=== commands.py: show full file length ==="
python - <<'PY'
from pathlib import Path
p=Path("commands.py")
print("commands.py lines:", sum(1 for _ in p.open("r", encoding="utf-8")))
PY
echo
echo "=== repo-wide references to python-telegram-bot/AsyncIOScheduler/restore_jobs ==="
rg -n --hidden --no-ignore-vcs "(python-telegram-bot|AsyncIOScheduler|restore_jobs|send_daily_message|ConversationHandler\\()" . || trueRepository: aeither/telegram-bot-python
Length of output: 3386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== .env.example ==="
cat .env.example
echo
echo "=== README.md (token-related lines) ==="
rg -n --hidden --no-ignore-vcs "TELEGRAM_BOT_TOKEN|BOT_TOKEN" README.md || true
echo
echo "=== pyproject.toml (project.dependencies block) ==="
python - <<'PY'
import tomllib
from pathlib import Path
p=Path("pyproject.toml")
data=tomllib.loads(p.read_text(encoding="utf-8"))
print(data.get("project", {}).get("dependencies", []))
PY
echo
echo "=== commands.py token env var ==="
rg -n --hidden --no-ignore-vcs "TOKEN = os\.environ\.get\\(|BOT_TOKEN" commands.pyRepository: aeither/telegram-bot-python
Length of output: 658
Fix critical startup breaks: missing deps, entrypoint mismatch, and token env var divergence
-
commands.pyimportspython-telegram-bot,apscheduler, andpytz, butpyproject.tomlonly declarespytelegrambotapiandpython-dotenv; clean installs will fail before the bot can run. -
main.pyimportsregister_commandsfromcommands.pyand builds atelebot.TeleBot, butcommands.pyhas noregister_commandsexport (it defines its own PTBmain()), so initialization will break. -
Token env var mismatch:
commands.pyusesBOT_TOKEN(TOKEN = os.environ.get("BOT_TOKEN", ...)), whilemain.py/README/.env.example useTELEGRAM_BOT_TOKEN. -
Additionally:
receive_hour()injects usermessage_textintoparse_mode="Markdown"without escaping, andload_data()/save_data()do raw JSON reads/writes without recovery/atomicity—corruptedusers.jsoncan crash handlers or leave partial writes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@commands.py` around lines 8 - 18, The file currently mismatches the project
entrypoint and deps: remove PTB-specific main() and export a
register_commands(bot: telebot.TeleBot) function that main.py expects (reference
symbol: register_commands) so main.py can register handlers on the telebot
instance; align the token env var by replacing TOKEN =
os.environ.get("BOT_TOKEN", ...) with reading TELEGRAM_BOT_TOKEN (or centralize
env var usage to TELEGRAM_BOT_TOKEN) so the token name matches README/main.py;
eliminate imports on unused PTB/apscheduler/pytz or move those features behind
optional flags and list required packages in pyproject.toml so clean installs
succeed; in receive_hour(message_text) stop passing raw user text into
parse_mode="Markdown" — escape or sanitize message_text before sending
(reference symbol: receive_hour); and make load_data()/save_data() robust by
adding JSON parse error handling and atomic writes (read with try/except
JSONDecodeError and on save write to temp file then os.replace) to avoid crashes
or partial writes (reference symbols: load_data, save_data).
| TOKEN = os.environ.get("BOT_TOKEN", "YOUR_BOT_TOKEN_HERE") | ||
| DATA_FILE = "users.json" | ||
| TIMEZONE = "Asia/Tehran" |
There was a problem hiding this comment.
Avoid resolving the bot token at import time.
main.py:1-39 calls load_dotenv() after importing commands, so this module reads the token before the .env file is loaded. Combined with the new BOT_TOKEN name, TOKEN can be frozen to the placeholder for the whole process. Resolve it inside main() and fail fast if it is missing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@commands.py` around lines 21 - 23, The module currently resolves TOKEN at
import time (TOKEN = os.environ.get(...)) which runs before load_dotenv() in
main; remove the top-level TOKEN assignment and instead read the BOT_TOKEN
inside main() after calling load_dotenv(), validate it and exit/fail fast if
missing, or provide a commands.get_token() accessor that reads env at call-time;
update any code that relied on the module-level TOKEN to accept the token as an
argument or call the new accessor so the real value from .env is used.
| def load_data(): | ||
| if os.path.exists(DATA_FILE): | ||
| with open(DATA_FILE, "r", encoding="utf-8") as f: | ||
| return json.load(f) | ||
| return {} | ||
|
|
||
| def save_data(data): | ||
| with open(DATA_FILE, "w", encoding="utf-8") as f: | ||
| json.dump(data, f, ensure_ascii=False, indent=2) |
There was a problem hiding this comment.
Harden users.json reads and writes against corruption.
restore_jobs() loads this file during startup, so a truncated or manually edited users.json will raise JSONDecodeError and brick the bot until someone repairs the file. save_data() also writes in place, which makes partial-file corruption possible on crash. Handle decode failures and write via a temp file + replace.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@commands.py` around lines 32 - 40, Handle corrupted or partially-written
users.json by updating load_data to catch json.JSONDecodeError and return {}
(and optionally log a warning) so restore_jobs() won't crash on startup, and
change save_data to write to a temporary file and then atomically replace
DATA_FILE (e.g., write to DATA_FILE + ".tmp" and use os.replace) to avoid
in-place partial writes; reference the load_data and save_data functions and
DATA_FILE constant when making these changes.
| await update.message.reply_text( | ||
| f"🎉 همه چیز تنظیم شد!\n\n" | ||
| f"📢 کانال/گروه: `{channel_id}`\n" | ||
| f"⏰ ساعت ارسال: `{hour:02d}:{minute:02d}` (تهران)\n" | ||
| f"📝 پیام: {message_text}\n\n" | ||
| f"هر روز ساعت {hour:02d}:{minute:02d} پیام ارسال میشه.\n" | ||
| f"برای توقف بنویس: /stop", | ||
| parse_mode="Markdown" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Telegram Bot API parse_mode="Markdown"require escaping user-provided text containing_, [, ], or backticks before sending?
💡 Result:
Yes, if you use parse_mode="Markdown" (the legacy mode), you must escape specific characters to prevent them from being interpreted as formatting markers [1]. Specifically, in the legacy Markdown mode, the characters _, *, , and [ must be escaped with a backslash if they are intended to be displayed as literal text [1]. If you are using the modern, more robust MarkdownV2 mode (parse_mode="MarkdownV2"), the requirements are much stricter. MarkdownV2 requires escaping a larger set of 18 special characters (including _, *, [, ], , etc.) whenever they appear outside of code blocks or specific formatting contexts [2][3]. Because manual escaping is error-prone, it is highly recommended to either: 1. Use HTML mode (parse_mode="HTML") instead, which only requires escaping <, >, and & [2]. 2. Send the message without a parse_mode and provide an entities array instead, which allows for precise, programmatic formatting without the need for escaping special characters at all [4].
Citations:
- 1: https://github.com/python-telegram-bot/python-telegram-bot/blob/v21.0/telegram/helpers.py
- 2: https://botnamefinder.com/blog/telegram-markdownv2-escape-characters
- 3: https://www.go2share.net/article/telegram-bot-api-markdownv2-string
- 4: https://github.com/bakaut/telegram-markdown-entities
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and display the relevant section of commands.py
if [ -f commands.py ]; then
echo "== commands.py =="
nl -ba commands.py | sed -n '130,200p'
else
echo "commands.py not found at repo root"
fd -a -t f 'commands.py' . | head -n 20
fiRepository: aeither/telegram-bot-python
Length of output: 150
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find commands.py (exactly) and print the relevant section with line numbers
paths="$(fd -a -t f '^commands\.py$' . || true)"
if [ -z "$paths" ]; then
echo "Could not find commands.py"
exit 0
fi
echo "Found commands.py at:"
echo "$paths"
# For each match, print 130-200 with line numbers
while IFS= read -r p; do
echo "== $p =="
cat -n "$p" | sed -n '130,200p'
done <<< "$paths"Repository: aeither/telegram-bot-python
Length of output: 2865
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a -t f 'commands.py$' . | xargs -I{} rg -n 'user_data\["message_text"\]' {}Repository: aeither/telegram-bot-python
Length of output: 239
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a -t f 'commands.py$' . | xargs -I{} rg -n 'message_text' {}Repository: aeither/telegram-bot-python
Length of output: 362
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a -t f 'commands.py$' . | xargs -I{} rg -n 'user_data\["channel_id"\]' {}Repository: aeither/telegram-bot-python
Length of output: 225
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a -t f 'commands.py$' . | xargs -I{} rg -n 'channel_id' {}Repository: aeither/telegram-bot-python
Length of output: 500
Escape user-provided message_text when using parse_mode="Markdown"
In commands.py (context.user_data["message_text"] = update.message.text and later reply_text(..., parse_mode="Markdown")), message_text is inserted into a legacy Markdown-formatted message without escaping. Telegram legacy Markdown requires escaping special characters (e.g., _, *, `, [), otherwise the confirmation can fail—despite the schedule/job already being saved. Escape message_text (or send the confirmation without Markdown / using entities or HTML).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@commands.py` around lines 163 - 170, The confirmation reply inserts
user-provided context.user_data["message_text"] (message_text) into
update.message.reply_text with parse_mode="Markdown", which can break for
Telegram legacy Markdown; escape Markdown special characters in message_text
before calling reply_text (or switch to parse_mode="HTML" and escape
accordingly) so the confirmation cannot fail — locate where message_text is
read/used and apply a Markdown-escaping function to message_text prior to
passing it to update.message.reply_text (or remove parse_mode and send plain
text).
| # ─── اجرای اصلی ──────────────────────────────────────────── | ||
| def main(): | ||
| app = Application.builder().token(TOKEN).build() | ||
|
|
||
| # Scheduler | ||
| scheduler = AsyncIOScheduler() | ||
| app.bot_data["scheduler"] = scheduler | ||
|
|
||
| # ConversationHandler | ||
| conv_handler = ConversationHandler( | ||
| entry_points=[CallbackQueryHandler(check_admin_callback, pattern="^check_admin$")], | ||
| states={ | ||
| WAITING_CHANNEL: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_channel)], | ||
| WAITING_MESSAGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_message)], | ||
| WAITING_HOUR: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_hour)], | ||
| }, | ||
| fallbacks=[CommandHandler("cancel", cancel)], | ||
| ) | ||
|
|
||
| app.add_handler(CommandHandler("start", start)) | ||
| app.add_handler(CommandHandler("stop", stop)) | ||
| app.add_handler(conv_handler) | ||
|
|
||
| # بازیابی jobها و شروع scheduler | ||
| scheduler.start() | ||
| restore_jobs(scheduler, app) | ||
|
|
||
| logger.info("ربات شروع به کار کرد...") | ||
| app.run_polling() | ||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
Update the existing entrypoint in the same PR or keep a compatibility shim.
main.py:1-39 still does from commands import register_commands and boots telebot.TeleBot. Since this rewrite removes register_commands and moves startup into commands.main(), the current app either fails on import or never reaches this async flow.
🧰 Tools
🪛 Ruff (0.15.15)
[warning] 266-266: Comment contains ambiguous ه (ARABIC LETTER HEH). Did you mean o (LATIN SMALL LETTER O)?
(RUF003)
[warning] 266-266: Comment contains ambiguous ا (ARABIC LETTER ALEF). Did you mean l (LATIN SMALL LETTER L)?
(RUF003)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@commands.py` around lines 243 - 274, The repo's old entrypoint expects
register_commands (imported by main.py) but commands.py removed it and now only
exposes main(); add a compatibility shim named register_commands in commands.py
that either (a) wraps/forwards to the new main() startup flow or (b) performs
the previous minimal bootstrap (creates/returns whatever the old main.py
expects, e.g., a TeleBot-like object or a non-blocking startup handle) so
imports don't fail—implement register_commands() near the top-level of
commands.py and ensure it references the same Application.builder()/scheduler
setup or simply delegates to main(), preserving the old import contract.
Summary by CodeRabbit
/startto begin setup,/stopto disable scheduling, and/cancelto exit configuration