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

Installation

Install Shingram using pip:

pip install shingram
Requires Python 3.10+

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:

  1. Create a Bot instance with your token
  2. Register an event handler using the @bot.on() decorator
  3. Access event data through the event parameter
  4. Call Telegram API methods using Python-style naming
  5. 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)
event.raw
The full Telegram update is in event.raw; use it when you need fields that aren’t on Event.

Event Fields Explained

Required Fields

Optional Convenience Fields

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:

Best Practice
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"}
Zero Hardcoding
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:

  1. Wildcard handler ("*") runs first, if registered. Useful for logging or global processing.
  2. Specific handler ("command:start") runs if the event name matches exactly.
  3. Type handler ("command") runs as a fallback for all events of that type.
Handler Execution
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

Yes, Markdown is Supported!
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"
    )
Formatting Tips
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

Good: Use convenience fields
if event.chat_type == "private":
    bot.send_message(chat_id=event.chat_id, text="Hey! I'm using shingram")
Avoid: Diving into raw for simple things
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

Good: Use raw for advanced features
entities = event.raw["message"].get("entities", [])
date = event.raw["message"]["date"]
Avoid: Trying to extract everything into Event
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

Good: Check if field exists
if event.username:
    print(f"User: @{event.username}")

if event.reply_to:
    print(f"Replied to: {event.reply_to}")
Avoid: Assuming fields always exist
print(f"User: @{event.username}")  # Might be None!

4. Filter by Chat Type

Good: Use chat_type for filtering
@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
Avoid: Checking raw every time
if event.raw["message"]["chat"]["type"] == "private":
    pass

5. Combine Event and Raw

Good: Use both Event fields 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