Rich Messages & Inline Buttons¶
Note
Rich Messages are a FTMGram-exclusive feature powered by Telegram’s native Bot API 10.3 Rich Message layer (Layer 230+). They require FTMGram ≥ 3.5.0 and a bot token (not a user account).
—
What Are Rich Messages?¶
Rich Messages are a completely different message type than plain text messages. They support:
Styled inline buttons embedded directly inside the message body (not below)
Formatted content — bold, italic, code blocks, blockquotes
Expandable blockquotes — collapsible sections
Thinking / streaming states — animated AI loading indicators
Stop button — let users cancel long-running operations
They are sent via send_rich_message() instead of
send_message().
—
Quick Start¶
from ftmgram import Client
from ftmgram.helpers import RichMessageBuilder, Button
app = Client("my_bot", bot_token="TOKEN")
async with app:
rich = (
RichMessageBuilder()
.title("👋 Welcome to FTMGram!")
.paragraph("Choose an action below:")
.button_row(
Button("📖 Docs", url="https://ftmgram.ftmbotzx.dev", style="primary"),
Button("💬 Support", url="https://t.me/ftmdeveloperz", style="success"),
)
.build()
)
await app.send_rich_message(chat_id, rich)
—
Button¶
- class ftmgram.helpers.Button(text: str, data: str | None = None, url: str | None = None, web_app: str | None = None, style: str = 'primary')¶
Represents a button in a Rich Message layout.
- Parameters:
text (
str) – The text to display on the button.data (
str, optional) – Callback data for callback buttons.url (
str, optional) – URL for link buttons.web_app (
str, optional) – Web App URL — opens a Telegram Mini App inline inside the message.style (
str, optional) – Style of button:"primary","secondary","success","danger". Defaults to"primary".
- to_rich_button() → RichMessageButton¶
Button is a helper class for building in-message styled buttons.
Constructor Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Label shown on the button. |
|
|
Callback data (for callback buttons). Used when no |
|
|
URL the button opens (for link buttons). |
|
|
Web App (Mini App) URL. Launches the Telegram Mini App inline (supported in private chats / bot dialogues). |
|
|
Visual style. One of |
Button Styles:
Style |
When to Use |
|---|---|
|
Main action (blue). Default. |
|
Less important action (grey). |
|
Positive/confirm action (green). |
|
Destructive/cancel action (red). |
Example — Callback Button:
btn = Button("✅ Confirm", data="confirm_action", style="success")
Example — URL Button:
btn = Button("🌐 Open Docs", url="https://ftmgram.ftmbotzx.dev", style="primary")
Example — Web App (Mini App) Button:
btn = Button("🚀 Open Mini App", web_app="https://tera-dl.ftmbotzx.dev/", style="success")
Note
Telegram restricts inline Mini App buttons (type="web_app") to private chats / 1-on-1 bot conversations. For public channels, use standard URL buttons (url="...").
—
RichMessageBuilder¶
RichMessageBuilder is a fluent DSL for building Rich Messages.
Chain methods together and call .build() at the end to get an
InputRichMessage ready to send.
Import:
from ftmgram.helpers import RichMessageBuilder, Button
Methods¶
Method |
Description |
|---|---|
|
Adds a bold header line. |
|
Adds a plain paragraph of text. |
|
Adds a blockquote. Set |
|
Adds a formatted code block. Pass |
|
Adds a horizontal row of in-message buttons. |
|
Compiles everything into an |
Full Example¶
from ftmgram.helpers import RichMessageBuilder, Button
rich = (
RichMessageBuilder()
.title("🚀 Bot Status")
.paragraph("Everything is running smoothly.")
.quote("Server uptime: 99.9%", expandable=True)
.code("{'status': 'ok', 'latency_ms': 12}", language="json")
.button_row(
Button("🔄 Refresh", data="refresh", style="primary"),
Button("⛔ Stop Bot", data="stop", style="danger"),
)
.button_row(
Button("📖 Docs", url="https://ftmgram.ftmbotzx.dev", style="secondary"),
)
.build()
)
await app.send_rich_message(chat_id, rich)
Multiple Button Rows¶
You can call .button_row() multiple times to create separate rows:
rich = (
RichMessageBuilder()
.title("📋 Menu")
.button_row(
Button("Option A", data="opt_a"),
Button("Option B", data="opt_b"),
)
.button_row(
Button("🔙 Back", data="back", style="secondary"),
)
.build()
)
—
send_rich_message¶
- Client.send_rich_message(chat_id: int | str, rich_message: InputRichMessage, disable_notification: bool | None = None, message_thread_id: int | None = None, direct_messages_topic_id: int | None = None, effect_id: int | None = None, reply_parameters: ReplyParameters | None = None, protect_content: bool | None = None, business_connection_id: str | None = None, allow_paid_broadcast: bool | None = None, suggested_post_parameters: SuggestedPostParameters | None = None, reply_markup: InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply | None = None) → Message¶
Send text messages.
Usable by ❌ Users ✅ Bots- Parameters:
chat_id (
int|str) – Unique identifier (int) or username (str) of the target chat. For your personal cloud (Saved Messages) you can simply use “me” or “self”. For a contact that exists in your Telegram address book you can use his phone number (str).rich_message (
InputRichMessage) – The message to be sent.disable_notification (
bool, optional) – Sends the message silently. Users will receive a notification with no sound.message_thread_id (
int, optional) – Unique identifier for the target message thread (topic) of the forum. For forums only.direct_messages_topic_id (
int, optional) – Unique identifier of the topic in a channel direct messages chat administered by the current user. For directs only only.effect_id (
int, optional) – Unique identifier of the message effect. For private chats only.reply_parameters (
ReplyParameters, optional) – Describes reply parameters for the message that is being sent.protect_content (
bool, optional) – Protects the contents of the sent message from forwarding and saving.business_connection_id (
str, optional) – Unique identifier of the business connection on behalf of which the message will be sent.allow_paid_broadcast (
bool, optional) – If True, you will be allowed to send up to 1000 messages per second. Ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot’s balance. For bots only.suggested_post_parameters (
SuggestedPostParameters, optional) – Information about the suggested post.reply_markup (
InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, optional) – Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
- Returns:
Message– On success, the sent text message is returned.
Example
from ftmgram import types await app.send_rich_message( chat_id=chat_id, rich_message=types.InputRichMessage(html="Hello <b>World</b>"), reply_markup=types.InlineKeyboardMarkup( [ [types.InlineKeyboardButton("Data", callback_data="callback_data")], [types.InlineKeyboardButton("Docs", url="https://docs.ftmgram.org")], ] ), )
Sends a Rich Message to a chat.
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
|
Target chat ID or username. |
|
The rich message object (from |
|
|
|
Send silently. |
|
|
Topic/thread ID for forum groups. |
|
|
Message effect ID (private chats only). |
|
|
Reply to a specific message. |
|
|
Prevent forwarding/saving. |
|
|
Business account connection ID. |
|
|
Allow sending up to 1000 msg/sec (costs 0.1 Stars/msg). |
|
|
Standard Telegram inline keyboard (separate from in-message buttons). |
Returns: Message
Examples:
Minimal:
from ftmgram.types import InputRichMessage
await app.send_rich_message(
chat_id=chat_id,
rich_message=InputRichMessage(html="Hello <b>World</b>!")
)
With RichMessageBuilder + buttons:
from ftmgram.helpers import RichMessageBuilder, Button
await app.send_rich_message(
chat_id=chat_id,
rich_message=(
RichMessageBuilder()
.title("📣 Announcement")
.paragraph("New update is live!")
.button_row(Button("🔗 Read More", url="https://t.me/ftmdeveloperz"))
.build()
),
)
With reply:
from ftmgram.types import ReplyParameters
await app.send_rich_message(
chat_id=chat_id,
rich_message=InputRichMessage(html="<b>Reply sent!</b>"),
reply_parameters=ReplyParameters(message_id=message.id),
)
—
InputRichMessage¶
InputRichMessage is the raw input type for rich messages.
You can use it directly (with raw HTML/Markdown) or let RichMessageBuilder
produce it for you.
Parameter |
Type |
Description |
|---|---|---|
|
|
Raw HTML content with |
|
|
Markdown-formatted content (no in-message buttons). |
HTML Tags Supported:
Tag |
Effect |
|---|---|
|
Bold text |
|
Italic text |
|
Inline code |
|
Code block |
|
Syntax-highlighted code block |
|
Blockquote |
|
Expandable / collapsible blockquote |
|
Row of in-message buttons |
|
URL button inside a row |
|
Callback button inside a row |
|
Animated thinking/loading indicator |
Direct HTML Example:
from ftmgram.types import InputRichMessage
raw_html = '''
<b>⚡ Live Status</b>
Server is online.
<tg-button-row align="center">
<tg-button type="url" style="primary" url="https://ftmgram.ftmbotzx.dev">Docs</tg-button>
<tg-button type="callback_data" style="danger" data="shutdown">Shutdown</tg-button>
</tg-button-row>
'''
await app.send_rich_message(chat_id, InputRichMessage(html=raw_html))
—
Streaming & Thinking¶
stream_text¶
stream_text streams AI-generated text token-by-token directly to Telegram
using native draft updates — no message spam.
from ftmgram.helpers import stream_text
# Works with any async generator (OpenAI, Gemini, Groq, Ollama, etc.)
async def my_llm_stream():
words = ["Hello", " from", " AI!", " How", " can", " I", " help?"]
for word in words:
await asyncio.sleep(0.1)
yield word
await stream_text(
client=app,
chat_id=chat_id,
stream=my_llm_stream(),
chunk_interval=0.15, # Update draft every 150ms max
placeholder="Thinking...", # Initial loading text
reply_to_message_id=message.id,
)
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
The FTMGram client instance. |
|
|
|
Target chat. |
|
|
Async generator yielding text chunks. |
|
|
Seconds between draft updates. Default: |
|
|
Initial thinking text. Default: |
|
|
Show Stop button. Default: |
|
|
Reply to a specific message. |
thinking¶
thinking is an async context manager that shows an animated “Thinking…”
indicator while your code runs in the background.
from ftmgram.helpers import thinking
async with thinking(app, chat_id, text="Searching knowledge base..."):
results = await fetch_from_database(query)
await app.send_message(chat_id, f"Found {len(results)} results!")
# Custom text + hide Stop button
async with thinking(app, chat_id, text="⚙️ Processing...", can_stop=False):
await heavy_computation()
Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
The FTMGram client instance. |
|
|
|
Target chat. |
|
|
Displayed text. Default: |
|
|
Show Stop button to user. Default: |
—
Handling Callback Buttons¶
In-message callback buttons work exactly like standard inline keyboard callbacks.
Register a handler with @app.on_callback_query():
from ftmgram import Client, filters
from ftmgram.helpers import RichMessageBuilder, Button
app = Client("my_bot", bot_token="TOKEN")
@app.on_message(filters.command("start"))
async def start(client, message):
rich = (
RichMessageBuilder()
.title("🎛️ Control Panel")
.paragraph("What would you like to do?")
.button_row(
Button("✅ Enable", data="toggle:on", style="success"),
Button("❌ Disable", data="toggle:off", style="danger"),
)
.build()
)
await client.send_rich_message(message.chat.id, rich)
@app.on_callback_query(filters.regex(r"toggle:(.+)"))
async def on_toggle(client, callback_query):
state = callback_query.matches[0].group(1)
await callback_query.answer(f"Turned {state}!", show_alert=True)
app.run()
—
Complete Real-World Example¶
import asyncio
from ftmgram import Client, filters
from ftmgram.helpers import RichMessageBuilder, Button, thinking, stream_text
from ftmgram.types import InputRichMessage
app = Client("my_bot", bot_token="BOT_TOKEN")
@app.on_message(filters.command("menu"))
async def menu(client, message):
rich = (
RichMessageBuilder()
.title("🤖 FTMGram Bot")
.paragraph("Powered by FTMGram v3.5+ — the fastest MTProto library.")
.quote("Ultra-fast transfers, Rich Messages, AI streaming built-in!", expandable=True)
.button_row(
Button("⚡ Fast Download", data="action:download", style="primary"),
Button("📤 Fast Upload", data="action:upload", style="primary"),
)
.button_row(
Button("🧠 AI Chat", data="action:ai", style="success"),
Button("📖 Docs", url="https://ftmgram.ftmbotzx.dev", style="secondary"),
)
.build()
)
await client.send_rich_message(message.chat.id, rich)
@app.on_callback_query(filters.regex(r"action:(.+)"))
async def on_action(client, cq):
action = cq.matches[0].group(1)
if action == "ai":
await cq.answer()
async def fake_ai():
for token in "Hello! I am an AI assistant. How can I help you today?".split():
await asyncio.sleep(0.1)
yield token + " "
await stream_text(client, cq.message.chat.id, fake_ai())
elif action == "download":
async with thinking(client, cq.message.chat.id, "Downloading file..."):
await asyncio.sleep(2) # your download logic here
await client.send_message(cq.message.chat.id, "✅ Download complete!")
await cq.answer()
else:
await cq.answer(f"Action '{action}' triggered!", show_alert=True)
app.run()