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

text

str

Label shown on the button.

data

str (optional)

Callback data (for callback buttons). Used when no url is given.

url

str (optional)

URL the button opens (for link buttons).

web_app

str (optional)

Web App (Mini App) URL. Launches the Telegram Mini App inline (supported in private chats / bot dialogues).

style

str (optional)

Visual style. One of "primary" (default), "secondary", "success", "danger".

Button Styles:

Style

When to Use

"primary"

Main action (blue). Default.

"secondary"

Less important action (grey).

"success"

Positive/confirm action (green).

"danger"

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

.title(text)

Adds a bold header line.

.paragraph(text)

Adds a plain paragraph of text.

.quote(text, expandable=False)

Adds a blockquote. Set expandable=True for a collapsible section.

.code(code, language="")

Adds a formatted code block. Pass language for syntax highlighting (e.g. "python").

.button_row(*buttons)

Adds a horizontal row of in-message buttons.

.build()

Compiles everything into an InputRichMessage.

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

chat_id

int | str

Target chat ID or username.

rich_message

InputRichMessage

The rich message object (from RichMessageBuilder().build()).

disable_notification

bool (optional)

Send silently.

message_thread_id

int (optional)

Topic/thread ID for forum groups.

effect_id

int (optional)

Message effect ID (private chats only).

reply_parameters

ReplyParameters (optional)

Reply to a specific message.

protect_content

bool (optional)

Prevent forwarding/saving.

business_connection_id

str (optional)

Business account connection ID.

allow_paid_broadcast

bool (optional)

Allow sending up to 1000 msg/sec (costs 0.1 Stars/msg).

reply_markup

InlineKeyboardMarkup etc. (optional)

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

html

str (optional)

Raw HTML content with <tg-button-row> tags.

markdown

str (optional)

Markdown-formatted content (no in-message buttons).

HTML Tags Supported:

Tag

Effect

<b>text</b>

Bold text

<i>text</i>

Italic text

<code>text</code>

Inline code

<pre>text</pre>

Code block

<pre><code class="language-python">...</code></pre>

Syntax-highlighted code block

<blockquote>text</blockquote>

Blockquote

<blockquote expandable>text</blockquote>

Expandable / collapsible blockquote

<tg-button-row align="center">...</tg-button-row>

Row of in-message buttons

<tg-button type="url" style="primary" url="...">Label</tg-button>

URL button inside a row

<tg-button type="callback_data" style="success" data="...">Label</tg-button>

Callback button inside a row

<tg-thinking>text</tg-thinking>

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

client

Client

The FTMGram client instance.

chat_id

int | str

Target chat.

stream

AsyncIterable[str]

Async generator yielding text chunks.

chunk_interval

float

Seconds between draft updates. Default: 0.15.

placeholder

str

Initial thinking text. Default: "Thinking...".

can_stop

bool

Show Stop button. Default: True.

reply_to_message_id

int (optional)

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

client

Client

The FTMGram client instance.

chat_id

int | str

Target chat.

text

str

Displayed text. Default: "Thinking...".

can_stop

bool

Show Stop button to user. Default: True.

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()