Shingram Documentation
Introduction
Minimal Python wrapper for the Telegram Bot API. Future-proof: new API methods work without library updates. Dual mode: same code works sync or async. All updates normalized to a single Event object — no dozens of classes to learn.
Features
- Zero hardcoding — Call any Telegram method; new API features work immediately
- One Event type — Every update (message, callback, inline, etc.) has the same shape
- Sync & Async — Use
bot.run()orbot.run_async()with the same handlers - ~1,200 lines — Lightweight, readable, easy to contribute
Installation
Install Shingram using pip:
pip install shingram
Quick Start
Minimal example:
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("command:start")
def handle_start(event):
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
bot.run()
Core concepts:
- Create a
Botinstance with your token - Register an event handler using the
@bot.on()decorator - Access event data through the
eventparameter - Call Telegram API methods using Python-style naming
- Start the bot with
bot.run()
Using async
Use async def handlers and start the bot with bot.run_async(). For API calls
use await bot.async_client.send_message(...) (and the other methods on
bot.async_client).
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("command:start")
async def handle_start(event):
await bot.async_client.send_message(chat_id=event.chat_id, text="Hello!")
@bot.on("message")
async def handle_message(event):
if event.text:
await bot.async_client.send_message(chat_id=event.chat_id, text=f"You said: {event.text}")
bot.run_async()
run() and run_async() accept optional timeout (default 30),
limit (default 100), allowed_updates (list of update types, or
None for all), and on_error (callback for polling-loop errors, e.g.
bot.run(on_error=logger.exception)).
Event Object Explained
What is Event?
Every update (message, callback, inline, etc.) is turned into the same Event shape. You get
a single type to handle; the original JSON is still in event.raw.
Event Structure
The Event dataclass contains the following fields:
@dataclass
class Event:
# Required fields (always present)
type: str # Event type: "command", "message", "callback", etc.
name: str # Event name: "start" for commands, "" for others
chat_id: int # Chat ID (0 if not applicable)
user_id: int # User ID (0 if not applicable)
text: str # Message text or callback data
raw: dict # Complete raw JSON from Telegram API
# Optional convenience fields (extracted for common use)
reply_to: Optional[int] # ID of replied message
chat_type: Optional[str] # "private", "group", "supergroup", "channel"
inline_query_id: Optional[str] # For inline_query events
callback_query_id: Optional[str] # For callback_query events
message_id: Optional[int] # Message ID (when available)
username: Optional[str] # User username (when available)
first_name: Optional[str] # User first name (when available)
chat_title: Optional[str] # Chat title (for groups/channels)
last_name: Optional[str] # User last name (when available)
language_code: Optional[str] # User language code (when available)
content_type: Optional[str] # "text", "photo", "document", etc. (for messages)
The full Telegram update is in
event.raw; use it when you need fields that aren’t on
Event.
Event Fields Explained
Required Fields
type: The type of event ("command", "message", "callback", "inline_query", etc.)name: The specific name of the event. For commands, this is the command name without the "/" prefix. For other events, this is typically an empty string or a specific identifier.chat_id: The unique identifier of the chat where the event occurred. Set to 0 for events that don't have a chat context (like inline queries).user_id: The unique identifier of the user who triggered the event. Set to 0 for events without a user context.text: The text content of the event. For messages, this is the message text. For callback queries, this is the callback data. For inline queries, this is the query text.raw: The complete, unmodified JSON object from the Telegram Bot API. This contains all fields and nested structures from the original update.
Optional Convenience Fields
reply_to: The message ID that this event is replying to, if applicable.chat_type: The type of chat ("private", "group", "supergroup", or "channel"). Useful for filtering and conditional logic.inline_query_id: The unique identifier for inline query events, used when answering inline queries.callback_query_id: The unique identifier for callback query events, used when answering callback queries.message_id: The unique identifier of the message, when applicable.username: The username of the user who triggered the event, if available.first_name: The first name of the user who triggered the event, if available.chat_title: The title or name of the group or channel, if applicable.last_name: The last name of the user, if available.language_code: The user's language code, if available.content_type: For messages, the content type ("text", "photo", "document", etc.).
Using Event Fields
Basic Fields (Always Available)
These fields are always present in every Event object, regardless of the update type:
@bot.on("message")
def handle_message(event):
# Always available
print(event.type) # "message", "command", "callback", etc.
print(event.chat_id) # Chat ID
print(event.user_id) # User ID
print(event.text) # Message text
print(event.raw) # Complete JSON
Convenience Fields (When Available)
These fields are extracted from the raw data when available, making common operations easier:
@bot.on("message")
def handle_message(event):
# Filter by chat type (very common)
if event.chat_type == "private":
bot.send_message(
chat_id=event.chat_id,
text=f"Hello {event.first_name}! This is a private chat."
)
elif event.chat_type == "group":
bot.send_message(
chat_id=event.chat_id,
text=f"@{event.username} wrote in {event.chat_title}"
)
# Check if it's a reply
if event.reply_to:
bot.send_message(
chat_id=event.chat_id,
text=f"You replied to message {event.reply_to}",
reply_to_message_id=event.message_id
)
# Use username directly
if event.username:
print(f"Message from @{event.username}")
Event Fields by Type
Message Events
For regular text messages, all convenience fields are typically available:
@bot.on("message")
def handle_message(event):
# Available fields:
event.type # "message"
event.chat_id # Chat ID
event.user_id # User ID
event.text # Message text
event.chat_type # "private", "group", "supergroup", "channel"
event.username # User username
event.first_name # User first name
event.chat_title # Group/channel title
event.message_id # Message ID
event.reply_to # Replied message ID (if reply)
event.raw # Complete JSON
Command Events
Commands are a special type of message that start with "/". The name field contains the
command name:
@bot.on("command:start")
def handle_start(event):
# Available fields:
event.type # "command"
event.name # "start" (command name without /)
event.chat_id # Chat ID
event.user_id # User ID
event.text # "/start" (full command text)
event.chat_type # Chat type
event.username # User username
event.first_name # User first name
event.message_id # Message ID
event.raw # Complete JSON
Callback Query Events
Callback queries are triggered when users click inline buttons. The text field contains the
callback data:
@bot.on("callback")
def handle_callback(event):
# Available fields:
event.type # "callback"
event.name # First part of callback_data (if contains ":")
event.text # Full callback_data
event.chat_id # Chat ID
event.user_id # User ID
event.callback_query_id # Callback query ID (for answering)
event.message_id # Message ID (if callback from message)
event.chat_type # Chat type
event.username # User username
event.raw # Complete JSON
# Answer callback
bot.answer_callback_query(
callback_query_id=event.callback_query_id,
text="Done!"
)
Inline Query Events
Inline queries are triggered when users use your bot in inline mode. Note that inline queries don't have
a chat_id:
@bot.on("inline_query")
def handle_inline(event):
# Available fields:
event.type # "inline_query"
event.user_id # User ID
event.text # Query text
event.inline_query_id # Inline query ID (for answering)
event.username # User username
event.first_name # User first name
event.raw # Complete JSON
# Answer inline query
bot.answer_inline_query(
inline_query_id=event.inline_query_id,
results=[...]
)
Using event.raw
When to Use event.raw
While Event provides convenient access to common fields, you should use event.raw when you
need:
- Advanced data not extracted by Event (entities, dates, media metadata)
- Complete structures (like the full
reply_to_messageobject) - Media information (photos, videos, documents, voice messages)
- Telegram-specific fields (update_id, forward information, etc.)
- Nested data that requires deep access into the JSON structure
Use Event fields for common operations, and
event.raw for advanced use cases.
Examples
1. Accessing Message Entities
Message entities provide information about formatting, mentions, links, and other special elements in the text:
@bot.on("message")
def handle_message(event):
# Entities (bold, italic, links, mentions, etc.)
entities = event.raw["message"].get("entities", [])
for entity in entities:
if entity["type"] == "bold":
offset = entity["offset"]
length = entity["length"]
bold_text = event.text[offset:offset+length]
print(f"Bold text: {bold_text}")
elif entity["type"] == "mention":
offset = entity["offset"]
length = entity["length"]
mention = event.text[offset:offset+length]
print(f"Mentioned: {mention}")
2. Getting Timestamp
Access the exact timestamp when the message was sent:
@bot.on("message")
def handle_message(event):
# Date/timestamp (Unix timestamp)
date = event.raw["message"]["date"]
from datetime import datetime
message_time = datetime.fromtimestamp(date)
print(f"Message sent at: {message_time}")
# Calculate time difference
now = datetime.now()
time_diff = now - message_time
print(f"Time since message: {time_diff}")
3. Accessing Media
Handle different types of media sent in messages:
@bot.on("message")
def handle_message(event):
message = event.raw["message"]
# Photo (array of photo sizes, last is largest)
if "photo" in message:
photos = message["photo"]
largest_photo = photos[-1] # Last is largest
file_id = largest_photo["file_id"]
file_size = largest_photo.get("file_size", 0)
print(f"Photo file_id: {file_id}, size: {file_size} bytes")
# Document
if "document" in message:
doc = message["document"]
file_name = doc.get("file_name", "")
file_size = doc.get("file_size", 0)
mime_type = doc.get("mime_type", "")
file_id = doc.get("file_id", "")
print(f"Document: {file_name} ({file_size} bytes, {mime_type})")
print(f"File ID: {file_id}")
# Voice message
if "voice" in message:
voice = message["voice"]
duration = voice.get("duration", 0)
file_id = voice.get("file_id", "")
file_size = voice.get("file_size", 0)
print(f"Voice message: {duration} seconds, {file_size} bytes")
print(f"File ID: {file_id}")
# Video
if "video" in message:
video = message["video"]
duration = video.get("duration", 0)
width = video.get("width", 0)
height = video.get("height", 0)
file_id = video.get("file_id", "")
print(f"Video: {width}x{height}, {duration} seconds")
4. Complete Reply Information
Get full details about the message being replied to:
@bot.on("message")
def handle_message(event):
# event.reply_to gives only the ID
if event.reply_to:
# Get complete reply information from raw
reply_msg = event.raw["message"].get("reply_to_message", {})
original_text = reply_msg.get("text", "")
original_user = reply_msg.get("from", {}).get("username", "")
original_date = reply_msg.get("date", 0)
original_chat = reply_msg.get("chat", {}).get("title", "")
print(f"Replied to: {original_text}")
print(f"From: @{original_user}")
print(f"In chat: {original_chat}")
print(f"Date: {datetime.fromtimestamp(original_date)}")
# Check if original message has media
if "photo" in reply_msg:
print("Original message had a photo")
if "document" in reply_msg:
print("Original message had a document")
5. Forward Information
Detect and handle forwarded messages:
@bot.on("message")
def handle_message(event):
message = event.raw["message"]
# Forwarded from user
if "forward_from" in message:
original_user = message["forward_from"]
user_id = original_user.get("id")
username = original_user.get("username", "")
first_name = original_user.get("first_name", "")
print(f"Forwarded from user: {first_name} (@{username}, ID: {user_id})")
# Forwarded from channel
if "forward_from_chat" in message:
chat = message["forward_from_chat"]
chat_type = chat.get("type")
if chat_type == "channel":
channel_name = chat.get("title", "")
channel_id = chat.get("id")
channel_username = chat.get("username", "")
print(f"Forwarded from channel: {channel_name} (@{channel_username}, ID: {channel_id})")
# Forward signature (for channels)
if "forward_signature" in message:
signature = message["forward_signature"]
print(f"Forward signature: {signature}")
# Forward date
if "forward_date" in message:
forward_date = datetime.fromtimestamp(message["forward_date"])
print(f"Originally sent: {forward_date}")
6. User Information
Access detailed user information:
@bot.on("message")
def handle_message(event):
user_info = event.raw["message"]["from"]
# Advanced user info
user_id = user_info.get("id")
is_bot = user_info.get("is_bot", False)
is_premium = user_info.get("is_premium", False)
language = user_info.get("language_code", "unknown")
last_name = user_info.get("last_name", "")
print(f"User ID: {user_id}")
print(f"Is bot: {is_bot}")
print(f"Premium: {is_premium}")
print(f"Language: {language}")
print(f"Full name: {event.first_name} {last_name}")
# Handle premium users differently
if is_premium:
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
7. Update ID
Access the update ID for tracking and deduplication:
@bot.on("*")
def handle_all(event):
# Update ID (not in Event, but in raw)
update_id = event.raw["update_id"]
print(f"Update ID: {update_id}")
# Use for tracking processed updates
# Shingram handles this automatically
Dynamic Methods
How It Works
Shingram uses Python's __getattr__ to automatically convert snake_case method
names to Telegram's camelCase API format.
# You write (Python style):
bot.send_message(chat_id=123, text="Hello")
# Shingram automatically converts to:
# POST https://api.telegram.org/bot<token>/sendMessage
# With JSON: {"chat_id": 123, "text": "Hello"}
No Telegram API methods are hardcoded in Shingram. If Telegram adds a new method tomorrow, it works immediately without any code changes. This makes Shingram future-proof and eliminates the need for library updates when new methods are added.
Examples
All Telegram API methods work automatically with Python naming:
# All of these work automatically:
bot.send_message(chat_id=123, text="Hey! I'm using shingram")
bot.send_photo(chat_id=123, photo="https://example.com/image.jpg")
bot.send_document(chat_id=123, document="file_id")
bot.forward_message(chat_id=123, from_chat_id=456, message_id=789)
bot.ban_chat_member(chat_id=123, user_id=456)
bot.get_chat_member(chat_id=123, user_id=456)
bot.answer_callback_query(callback_query_id="123", text="Hey! I'm using shingram")
bot.answer_inline_query(inline_query_id="123", results=[])
bot.edit_message_text(chat_id=123, message_id=456, text="Hey! I'm using shingram")
bot.delete_message(chat_id=123, message_id=456)
# ... any other Telegram API method!
Conversion Table
Common method name conversions:
| Python (snake_case) | Telegram API (camelCase) |
|---|---|
send_message |
sendMessage |
get_updates |
getUpdates |
ban_chat_member |
banChatMember |
forward_message |
forwardMessage |
get_chat_member |
getChatMember |
edit_message_text |
editMessageText |
answer_callback_query |
answerCallbackQuery |
answer_inline_query |
answerInlineQuery |
Event Handlers
Handler Registration
You can register event handlers in two ways:
Method 1: Decorator (Recommended)
@bot.on("command:start")
def handle_start(event):
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
Method 2: Direct Method
def handle_start(event):
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
bot.on("command:start", handle_start)
Handler Patterns
Shingram supports flexible pattern matching for event handlers:
# Specific command (most specific)
@bot.on("command:start") # Only /start
# All commands (less specific)
@bot.on("command") # All commands (/start, /help, /info, etc.)
# All messages (even less specific)
@bot.on("message") # All text messages
# Wildcard (catches everything, least specific)
@bot.on("*") # ALL events (messages, commands, callbacks, etc.)
Handler Priority
When multiple handlers could match an event, Shingram uses the following priority order:
- Wildcard handler (
"*") runs first, if registered. Useful for logging or global processing. - Specific handler (
"command:start") runs if the event name matches exactly. - Type handler (
"command") runs as a fallback for all events of that type.
Only one handler is executed per event. Once a specific handler matches, the type handler is not called. The wildcard handler always runs first (if present) but doesn't prevent other handlers from running.
Examples
You can find plenty of examples in the examples/ directory: sync (e.g.
echo_bot.py, inline_bot.py, keyboard_bot.py,
webhook_flask.py, webhook_fastapi.py) and async (e.g.
echo_bot_async.py, inline_bot_async.py, keyboard_bot_async.py).
Set your bot token in the file and run it.
Example 1: Simple Echo Bot
A basic bot that echoes back whatever the user sends:
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("message")
def echo(event):
if event.text:
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
bot.run()
Example 2: Private Chat vs Group Chat
Different behavior based on chat type:
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("command:hello")
def hello(event):
# Different responses based on chat type
if event.chat_type == "private":
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
elif event.chat_type in ["group", "supergroup"]:
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
bot.run()
Example 3: Inline Bot
Handle inline queries for inline mode:
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("inline_query")
def handle_inline(event):
query = event.text
# Build results based on query
results = [
{
"type": "article",
"id": "1",
"title": f"Result for: {query}",
"description": f"Search result for '{query}'",
"input_message_content": {
"message_text": "Hey! I'm using shingram"
}
}
]
# Answer the inline query
bot.answer_inline_query(
inline_query_id=event.inline_query_id,
results=results
)
bot.run()
Example 4: Callback Buttons
Create interactive buttons and handle button clicks:
from shingram import Bot
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("command:buttons")
def show_buttons(event):
# Create inline keyboard
keyboard = {
"inline_keyboard": [
[
{"text": "Button 1", "callback_data": "btn1"},
{"text": "Button 2", "callback_data": "btn2"}
],
[
{"text": "Button 3", "callback_data": "btn3"}
]
]
}
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram",
reply_markup=keyboard
)
@bot.on("callback")
def handle_callback(event):
# Answer callback (required to stop loading indicator)
bot.answer_callback_query(
callback_query_id=event.callback_query_id,
text="Hey! I'm using shingram"
)
# Handle button based on callback data
if event.text == "btn1":
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
elif event.text == "btn2":
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
elif event.text == "btn3":
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
bot.run()
Example 5: Using event.raw for Advanced Features
Combine Event convenience fields with raw data access:
from shingram import Bot
from datetime import datetime
bot = Bot("YOUR_BOT_TOKEN")
@bot.on("message")
def handle_message(event):
# Use convenience fields for common tasks
if event.chat_type == "private":
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
# Use raw for advanced data
message = event.raw["message"]
# Check for media
if "photo" in message:
photos = message["photo"]
file_id = photos[-1]["file_id"]
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
# Get timestamp
date = message["date"]
msg_time = datetime.fromtimestamp(date)
# Check entities (mentions, hashtags, etc.)
entities = message.get("entities", [])
for entity in entities:
if entity["type"] == "mention":
offset = entity["offset"]
length = entity["length"]
mention = event.text[offset:offset+length]
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
bot.run()
Markdown Support
Shingram supports all Telegram formatting options through the
parse_mode parameter. You can
use HTML, Markdown, or MarkdownV2 formatting.
HTML Formatting
HTML is the most flexible and widely supported format:
bot.send_message(
chat_id=event.chat_id,
text="<b>Bold</b> <i>Italic</i> <code>Code</code>",
parse_mode="HTML"
)
Markdown Formatting
Classic Markdown syntax (simpler but less flexible):
bot.send_message(
chat_id=event.chat_id,
text="*Bold* _Italic_ `Code`",
parse_mode="Markdown"
)
MarkdownV2 Formatting
Enhanced Markdown with more features (requires escaping special characters):
bot.send_message(
chat_id=event.chat_id,
text="*Bold* _Italic_ `Code`",
parse_mode="MarkdownV2"
)
HTML Example
Complete HTML formatting example:
@bot.on("command:format")
def format_example(event):
bot.send_message(
chat_id=event.chat_id,
text="""
<b>Bold text</b>
<i>Italic text</i>
<u>Underlined text</u>
<s>Strikethrough text</s>
<code>Monospace code</code>
<pre>Preformatted code block</pre>
<a href="https://example.com">Link</a>
""",
parse_mode="HTML"
)
Markdown Example
Complete Markdown formatting example:
@bot.on("command:format")
def format_example(event):
bot.send_message(
chat_id=event.chat_id,
text="""
*Bold text*
_Italic text_
`Monospace code`
[Link](https://example.com)
""",
parse_mode="Markdown"
)
HTML is generally recommended for its flexibility and better support for complex formatting. MarkdownV2 requires escaping special characters, which can be cumbersome. Use HTML for most cases, and Markdown only if you prefer its syntax.
Best Practices
1. Use Event Fields for Common Tasks
if event.chat_type == "private":
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
if event.raw["message"]["chat"]["type"] == "private":
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
2. Use event.raw for Advanced Data
entities = event.raw["message"].get("entities", [])
date = event.raw["message"]["date"]
Event is designed to have only common fields. Don't try to add every possible field - use
event.raw for advanced data.
3. Always Check Availability
if event.username:
print(f"User: @{event.username}")
if event.reply_to:
print(f"Replied to: {event.reply_to}")
print(f"User: @{event.username}") # Might be None!
4. Filter by Chat Type
@bot.on("message")
def handle_message(event):
if event.chat_type == "private":
# Private chat logic
pass
elif event.chat_type == "group":
# Group chat logic
pass
if event.raw["message"]["chat"]["type"] == "private":
pass
5. Combine Event and Raw
@bot.on("message")
def handle_message(event):
# Simple filtering with Event
if event.chat_type == "private":
# Advanced data from raw
user_info = event.raw["message"]["from"]
is_premium = user_info.get("is_premium", False)
if is_premium:
bot.send_message(
chat_id=event.chat_id,
text="Hey! I'm using shingram"
)
6. Error Handling
Always handle potential errors when making API calls:
from shingram import Bot, TelegramAPIError
@bot.on("message")
def handle_message(event):
try:
bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
except TelegramAPIError as e:
print(f"Telegram API error: {e}")
print(f"Error code: {e.error_code}")
print(f"Description: {e.description}")
except Exception as e:
print(f"Unexpected error: {e}")
Summary
Event is Simple, Not Limited
- Event has 14 fields (6 required + 8 optional)
- Extracts only common fields for convenience
- Everything else is in
event.raw- you never lose data - 80% of use cases can use Event fields directly
- 20% advanced cases use
event.raw
You Never Struggle
- ✅ Common tasks: Use Event fields (
event.chat_type,event.username) - ✅ Advanced tasks: Use
event.raw(entities, media, dates) - ✅ Always available: Complete JSON in
event.raw - ✅ Best of both: Simple when possible, powerful when needed
Markdown Support
- ✅ HTML formatting:
parse_mode="HTML" - ✅ Markdown formatting:
parse_mode="Markdown" - ✅ MarkdownV2 formatting:
parse_mode="MarkdownV2"
Key Takeaways
- Shingram provides a simple, normalized Event object for easy development
- All Telegram API methods work automatically with Python naming conventions
- Full access to raw data ensures you never lose functionality
- The library is future-proof - new Telegram methods work immediately
- Use Event fields for common operations, raw data for advanced features