byteforge-telegram
A generic, reusable Python library for Telegram bot notifications and webhook management.
Features
-
TelegramBotController: Send notifications via Telegram Bot API
- Plain text messages
- Formatted messages with title, fields, and footer
- Rich Messages (Bot API 10.1): tables, lists, headings, formulas, media — see docs/rich-messages.md
- Inline keyboards: attach buttons to sends, answer callback queries, edit sent messages
- Both sync and async support
- Automatic event loop handling
- Session cleanup to prevent leaks
-
WebhookManager: Manage Telegram webhooks
- Set webhook URL,
allowed_updates, andsecret_token - Get webhook information
- Delete webhook
- CLI tool included
- Set webhook URL,
Installation
pip install byteforge-telegram
Or install from source:
git clone https://github.com/jmazzahacks/byteforge-telegram.git
cd byteforge-telegram
pip install -e .
Quick Start
Sending Notifications
from byteforge_telegram import TelegramBotController, ParseMode
# Initialize with your bot token
bot = TelegramBotController("YOUR_BOT_TOKEN")
# Send a simple message
bot.send_message_sync(
text="Hello from byteforge-telegram!",
chat_ids=["CHAT_ID_1", "CHAT_ID_2"]
)
# Send a formatted message
bot.send_formatted_sync(
title="Deployment Complete",
fields={
"Environment": "production",
"Version": "1.2.3",
"Status": "Success"
},
chat_ids=["YOUR_CHAT_ID"],
emoji="✅",
footer="Deployed at 2025-01-03 12:00:00 UTC"
)
Sending to a Supergroup Topic
Telegram supergroups with topics enabled require a message_thread_id to post into
a specific topic. Use send_to_chat (single-target) rather than send_message
(fan-out) — a thread id only applies to one supergroup, so it can't compose with a
mixed list of chat ids:
bot.send_to_chat_sync(
chat_id="-1001234567890", # the supergroup
text="<b>New ticket filed</b>",
message_thread_id=42, # the topic within it
)
Inline Keyboards and Callback Queries
Attach an inline keyboard to a single-chat send, then react to button taps and
edit the delivered message. send_to_chat_sync returns the sent message_id
(or None on failure), which you store to edit the message later:
keyboard = {
"inline_keyboard": [[
{"text": "✅ Approve", "callback_data": "approve:abc123"},
{"text": "✖ Reject", "callback_data": "reject:abc123"},
]]
}
message_id = bot.send_to_chat_sync(
chat_id="YOUR_CHAT_ID",
text="<b>Escalation:</b> agent needs approval",
reply_markup=keyboard,
)
# Later, in your webhook handler for the callback_query update:
bot.answer_callback_query_sync(callback_query_id, text="Approved!")
# Rewrite the message so the buttons disappear and the outcome is shown
bot.edit_message_text_sync(
chat_id="YOUR_CHAT_ID",
message_id=message_id,
text="<b>Escalation:</b> approved by Jason",
)
The reply_markup dict is passed to the Bot API untouched. If the text is long
enough to be split into multiple messages, the keyboard attaches to the last
chunk, and the returned message_id is that last chunk's — so it is always the
right target for edit_message_text_sync. Editing without reply_markup
removes any existing keyboard; pass the keyboard again to keep it.
To change only the buttons — e.g. swapping to a confirm/cancel keyboard and
back — use edit_message_reply_markup_sync, which leaves the message text and
its formatting untouched:
# Tap "Reject" → ask for confirmation without touching the text
bot.edit_message_reply_markup_sync(
chat_id="YOUR_CHAT_ID",
message_id=message_id,
reply_markup={"inline_keyboard": [[
{"text": "⚠️ Yes, reject", "callback_data": "confirm_reject:abc123"},
{"text": "Cancel", "callback_data": "cancel:abc123"},
]]},
)
Avoid rebuilding text from callback_query.message.text for keyboard swaps: that
field is plain text (Telegram strips HTML formatting into a separate entities
array) and it reflects the message as already edited, not the original. If you
only need different buttons, edit only the buttons.
Buttons do nothing when tapped? If your webhook was registered before the bot used inline keyboards, Telegram may not be delivering
callback_queryupdates at all — see theallowed_updatesnote under Managing Webhooks below.
Sending a Rich Message
Rich Messages (Bot API 10.1) support structured content — headings, lists, tables,
formulas, media, collapsible blocks — expressed as an extended-HTML or Markdown string.
Use an InputRichMessage with send_rich_message / send_rich_message_sync:
from byteforge_telegram import InputRichMessage
bot.send_rich_message_sync(
chat_id="123456789",
rich_message=InputRichMessage(html=(
"<h2>Daily report</h2>"
"<ul><li>All systems green</li><li>3 deploys</li></ul>"
"<table><tr><th>Metric</th><th>Value</th></tr>"
"<tr><td>Uptime</td><td>99.98%</td></tr></table>"
)),
)
Pass exactly one of html or markdown. Unlike send_message, rich text is sent
as-is (no escaping/repair/splitting), so escape literal <, >, & yourself. See
docs/rich-messages.md for the full list of supported tags,
attributes, entities, and limits.
Managing Webhooks
Programmatic API
from byteforge_telegram import WebhookManager
# Initialize manager
manager = WebhookManager("YOUR_BOT_TOKEN")
# Set webhook, declaring which update types to receive
result = manager.set_webhook(
"https://example.com/telegram/webhook",
allowed_updates=["message", "callback_query"],
secret_token="MY_WEBHOOK_SECRET", # optional; validate it in your handler
)
if result['success']:
print(f"Webhook set: {result['description']}")
# Get webhook info
info = manager.get_webhook_info()
if info:
print(f"Current webhook: {info.get('url')}")
print(f"Pending updates: {info.get('pending_update_count')}")
# Delete webhook
result = manager.delete_webhook()
if result['success']:
print("Webhook deleted")
The allowed_updates trap. Telegram treats an omitted allowed_updates
as "keep the previous setting", not "use the default". If the webhook was ever
registered with a narrow list (say ["message"]), every later set_webhook
call that omits the parameter reports success while silently continuing to
discard other update types — inline keyboard taps (callback_query) never
reach your endpoint, with no error anywhere. When updates you expect aren't
arriving, get_webhook_info() is the diagnostic: check its allowed_updates
field before suspecting your handler. Pass allowed_updates explicitly to
widen the set, or [] to reset to Telegram's default.
secret_token has the opposite behavior — it is not sticky. Omitting it on
set_webhook clears any existing secret, so if your endpoint validates the
X-Telegram-Bot-Api-Secret-Token header, re-supply the secret on every call.
Command-Line Interface
The package includes a setup-telegram-webhook CLI tool:
# Set webhook
setup-telegram-webhook --token YOUR_BOT_TOKEN --url https://example.com/telegram/webhook
# Set webhook and declare update types (omitting --allowed-updates KEEPS the
# previously registered set — see the allowed_updates trap above)
setup-telegram-webhook --token YOUR_BOT_TOKEN \
--url https://example.com/telegram/webhook \
--allowed-updates message callback_query \
--secret-token MY_WEBHOOK_SECRET
# Or use environment variable
export TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN
setup-telegram-webhook --url https://example.com/telegram/webhook
# Get webhook info
setup-telegram-webhook --token YOUR_BOT_TOKEN --info
# Delete webhook
setup-telegram-webhook --token YOUR_BOT_TOKEN --delete
API Reference
TelegramBotController
Methods
send_message_sync(text, chat_ids, parse_mode=ParseMode.HTML, ...)
- Send a plain text message (synchronous)
- Returns:
Dict[str, bool]- success status for each chat
send_to_chat_sync(chat_id, text, *, message_thread_id=None, reply_markup=None, ...)
- Send a message to a single chat, optionally targeting a supergroup topic
- Returns:
Optional[int]- the sent message'smessage_id, orNoneif the send did not fully succeed (truthy on success, so existing boolean-style checks keep working; on a multi-chunk send,Nonecan mean earlier chunks were already delivered, so retrying may duplicate them) reply_markuptakes a Bot API dict, e.g.{"inline_keyboard": [[{"text": ..., "callback_data": ...}]]}; when the text is split into chunks it attaches to the last chunk, whosemessage_idis the one returned- Use this instead of
send_message_syncwhen you needmessage_thread_id, since a thread id is only meaningful for one specific supergroup.
send_formatted_sync(title, fields, chat_ids, emoji=None, footer=None)
- Send a formatted message with title, fields, and footer (synchronous)
- Returns:
Dict[str, bool]- success status for each chat
send_rich_message_sync(chat_id, rich_message, *, message_thread_id=None, disable_notification=False, protect_content=False)
- Send a Rich Message (Bot API 10.1) to a single chat;
rich_messageis anInputRichMessage - Returns:
bool- success status - Content is sent as-is (no escaping/repair/splitting). See docs/rich-messages.md
edit_message_text_sync(chat_id, message_id, text, *, parse_mode=ParseMode.HTML, reply_markup=None, ...)
- Edit the text (and inline keyboard) of a previously sent message
- Returns:
bool- success status - Omitting
reply_markupremoves any existing keyboard; text is not split, so it must fit in one message (4096 chars)
edit_message_reply_markup_sync(chat_id, message_id, *, reply_markup=None)
- Edit only the inline keyboard of a previously sent message; the text and its formatting are left untouched
- Returns:
bool- success status - Omitting
reply_markupremoves the keyboard (same convention asedit_message_text_sync)
answer_callback_query_sync(callback_query_id, *, text=None, show_alert=False)
- Answer an inline keyboard button tap (clears the spinner Telegram shows on the button)
- Returns:
bool- success status textappears as a toast, or a modal alert withshow_alert=True
send_message(...) / send_to_chat(...) / send_formatted(...) / send_rich_message(...) / edit_message_text(...) / edit_message_reply_markup(...) / answer_callback_query(...)
- Async versions of the above methods
- Use with
awaitin async contexts
test_connection_sync(chat_id)
- Send a test message to verify bot is working
- Returns:
bool
Parse Modes
from byteforge_telegram import ParseMode
ParseMode.HTML # HTML formatting (default)
ParseMode.MARKDOWN # Markdown formatting
ParseMode.MARKDOWN_V2 # MarkdownV2 formatting
ParseMode.NONE # Plain text, no formatting
WebhookManager
Methods
set_webhook(webhook_url, timeout=10, allowed_updates=None, secret_token=None)
- Set the webhook URL for the bot
- Args:
webhook_url: HTTPS URL (required)timeout: Request timeout in secondsallowed_updates: Update types to receive, e.g.["message", "callback_query"]. Omitted means "keep the previous setting" (see the trap above);[]resets to Telegram's defaultsecret_token: Value echoed back in theX-Telegram-Bot-Api-Secret-Tokenheader. Not sticky — omitting it clears any existing secret
- Returns:
Dict[str, Any]withsuccessanddescription - Raises:
ValueErrorif URL is not HTTPS - On success, logs the effective
allowed_updates(fetched viaget_webhook_info)
get_webhook_info(timeout=10)
- Get current webhook configuration
- Returns:
Dict[str, Any]with webhook details, orNoneon error - The first place to look when expected updates aren't arriving — check the
allowed_updatesfield
delete_webhook(timeout=10)
- Delete the current webhook
- Returns:
Dict[str, Any]withsuccessanddescription
TelegramResponse
Type-safe dataclass for constructing webhook responses.
Fields
method: API method name (usually "sendMessage")chat_id: Target chat IDtext: Message textparse_mode: Format type (default: "HTML")reply_markup: Optional keyboard markupdisable_web_page_preview: Disable link previews (default: False)disable_notification: Send silently (default: False)
Methods
to_dict()
- Convert to JSON-serializable dictionary
- Returns:
Dict[str, Any]
Example
from byteforge_telegram import TelegramResponse
response = TelegramResponse(
method='sendMessage',
chat_id=12345,
text='<b>Hello!</b>',
parse_mode='HTML',
disable_web_page_preview=True
)
# Use in Flask webhook
return jsonify(response.to_dict()), 200
Examples
Integration with Flask (Simple)
import os
from flask import Flask, request, jsonify
from byteforge_telegram import TelegramBotController
app = Flask(__name__)
bot = TelegramBotController(os.getenv('TELEGRAM_BOT_TOKEN'))
@app.route('/telegram/webhook', methods=['POST'])
def telegram_webhook():
update = request.get_json()
# Process the update
message = update.get('message', {})
text = message.get('text', '')
chat_id = str(message.get('chat', {}).get('id'))
if text == '/start':
bot.send_message_sync(
text="Welcome! I'm your bot.",
chat_ids=[chat_id]
)
return jsonify({'ok': True}), 200
Integration with Flask (Using TelegramResponse)
For more complex webhooks, use TelegramResponse for type-safe responses:
from flask import Flask, request, jsonify
from byteforge_telegram import TelegramResponse
app = Flask(__name__)
@app.route('/telegram/webhook', methods=['POST'])
def telegram_webhook():
update = request.get_json()
# Extract message details
message = update.get('message', {})
text = message.get('text', '')
chat_id = message.get('chat', {}).get('id')
# Handle command
if text == '/start':
response = TelegramResponse(
method='sendMessage',
chat_id=chat_id,
text='<b>Welcome!</b> Type /help for commands.',
parse_mode='HTML'
)
return jsonify(response.to_dict()), 200
return jsonify({'ok': True}), 200
Async Usage
import asyncio
from byteforge_telegram import TelegramBotController, ParseMode
async def send_notifications():
bot = TelegramBotController("YOUR_BOT_TOKEN")
# Send multiple messages concurrently
results = await bot.send_message(
text="Async notification",
chat_ids=["CHAT_1", "CHAT_2", "CHAT_3"],
parse_mode=ParseMode.HTML
)
for chat_id, success in results.items():
if success:
print(f"Sent to {chat_id}")
else:
print(f"Failed to send to {chat_id}")
asyncio.run(send_notifications())
Error Handling
from byteforge_telegram import TelegramBotController
bot = TelegramBotController("YOUR_BOT_TOKEN")
results = bot.send_message_sync(
text="Important notification",
chat_ids=["CHAT_ID"]
)
for chat_id, success in results.items():
if not success:
print(f"Failed to send to {chat_id}")
# Implement retry logic, logging, etc.
Design Philosophy
Sync/Async Compatibility
The library handles both synchronous and asynchronous contexts automatically:
*_sync()methods work in regular Python code (like Flask apps)asyncmethods work in async contexts (like FastAPI, async scripts)- Automatically detects running event loops
- Creates fresh Bot instances per call to avoid loop conflicts
Session Management
Each message send creates a new Bot instance and properly cleans up the HTTP session afterward. This prevents connection leaks and event loop conflicts.
Error Handling
- Network errors are caught and logged
- Results dict shows success/failure per chat ID
- Graceful degradation when services are unavailable
Requirements
- Python 3.9+
- python-telegram-bot >= 20.0
- requests >= 2.31.0
Development
Setup
# Clone repository
git clone https://github.com/jmazzahacks/byteforge-telegram.git
cd byteforge-telegram
# Create and activate virtual environment
python3 -m venv .
source bin/activate
# Install development dependencies
pip install -r dev-requirements.txt
# Install package in development mode
pip install -e .
# Run tests
pytest
# Format code
black src/
Running Tests
# Run all tests
source bin/activate && pytest
# Run with coverage
source bin/activate && pytest --cov=byteforge_telegram
# Run specific test file
source bin/activate && pytest tests/test_models.py
License
MIT License - see LICENSE file for details
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Author
Jason Byteforge (@jmazzahacks)
Links
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file byteforge_telegram-0.4.1.tar.gz.
File metadata
- Download URL: byteforge_telegram-0.4.1.tar.gz
- Upload date:
- Size: 43.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
995ce98103cb040d0392fea37c515cc5d41030f2cf2607af4af8ab1dc1fcc442
|
|
| MD5 |
fd0581e9d8f806721bd3e0ad7cb30bdd
|
|
| BLAKE2b-256 |
0588aee2cfd2907449e78a9465964d66cf712a04435ea8f8f818824350f41dad
|
File details
Details for the file byteforge_telegram-0.4.1-py3-none-any.whl.
File metadata
- Download URL: byteforge_telegram-0.4.1-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
186941aef5c7bec1d57e8dcc277747e90343a2c07e810d88d8f186c20a073cd9
|
|
| MD5 |
a84e8a4f591ad8e30ef7fc5c1f594537
|
|
| BLAKE2b-256 |
28b30dc113a212f0f73382e481193a807fb836b5e50dddea5318e7f64e371112
|