Skip to content

Emoji Callbacks

How Does It Work?

Matrix doesn't have inline buttons like Telegram. But it has reactions (emojis). And they work as buttons.

You send a message, the bot adds reactions. The user clicks a reaction — the bot fires your callback.

Example:

Message: "What color is Hatsune Miku's hair?"
Reactions:  🔵 🟢 🟤

User clicks 🔵
  → callback catches the reaction
  → Calls callback with payload="blue"

Message changes to: "✅ Correct! Teal-blue!"


EmojiKeyBoard

from mxc.types import EmojiButton
from mxc.utils.keyboard import EmojiKeyBoard

markup = EmojiKeyBoard(
    rows=[
        [
            EmojiButton(emoji="✅", data="yes"),
            EmojiButton(emoji="❌", data="no"),
        ],
    ],
    callback=my_handler,
)

await utils.answer(
    mx,
    "Confirm action:",
    event=event,
    reply_markup=markup,
)

All EmojiKeyBoard Parameters

Parameter What it does Default
rows Buttons arranged in rows required
callback Your handler function required
ttl Time to live in seconds (0 = forever) 0
allowed_senders Who can click. Default: command sender + sudo users Sender only + sudo
remove_clicked Remove reaction after click True
keep_reactions Restore if deleted True
data State dict, passed to callback {}

EmojiCallbackContext

What you get in the callback:

Field Type What it contains
ctx.payload Any data from EmojiButton
ctx.data dict mutable session dict from the data parameter
ctx.key str The emoji that was clicked
ctx.sender str Who clicked (@user:server)
ctx.message_id str Message ID
ctx.room_id str Room ID

Methods: - await ctx.edit("text") — edit the message - await ctx.react("👍") — add a reaction - await ctx.close() — close + remove all reactions - await ctx.refresh() — restore deleted reactions


PATTERNS

1. Confirm

async def on_confirm(ctx):
    if ctx.payload == "yes":
        await ctx.edit("✅ Ok, done!")
    else:
        await ctx.edit("❌ Cancelled")
    await ctx.close()

markup = EmojiKeyBoard(
    rows=[[
        EmojiButton(emoji="✅", data="yes"),
        EmojiButton(emoji="❌", data="no"),
    ]],
    callback=on_confirm,
)

await utils.answer(
    mx,
    "Confirm action:",
    event=event,
    reply_markup=markup,
)

2. Pagination

PAGES = ["page 1", "page 2", "page 3"]

async def on_page(ctx):
    page = (ctx.data.get("page", 0) + 1) % len(PAGES)
    ctx.data["page"] = page
    await ctx.edit(PAGES[page])

markup = EmojiKeyBoard(
    rows=[[EmojiButton(emoji="➡️", data="next")]],
    callback=on_page,
    data={"page": 0},
    remove_clicked=False,
)

await utils.answer(
    mx,
    PAGES[0],
    event=event,
    reply_markup=markup,
)

3. Rating

async def on_rate(ctx):
    await ctx.edit(f"⭐ Rating: {ctx.payload}/5")
    await ctx.close()

markup = EmojiKeyBoard(
    rows=[[
        EmojiButton(emoji="⭐", data=1),
        EmojiButton(emoji="⭐⭐", data=2),
        EmojiButton(emoji="⭐⭐⭐", data=3),
    ]],
    callback=on_rate,
)

await utils.answer(
    mx,
    "Rate:",
    event=event,
    reply_markup=markup,
)

4. Action Menu (dict in data)

async def on_action(ctx):
    action = ctx.payload.get("action")

    if action == "refresh":
        ctx.data["count"] = ctx.data.get("count", 0) + 1
        await ctx.edit(
            f"Counter: {ctx.data['count']}",
        )
    elif action == "close":
        await ctx.close()

markup = EmojiKeyBoard(
    rows=[
        [EmojiButton(emoji="🔄", data={"action": "refresh"})],
        [EmojiButton(emoji="🧹", data={"action": "close"})],
    ],
    callback=on_action,
    data={"count": 0},
)

5. Editing in callback via ctx.edit()

When the user clicks a reaction — use ctx.edit() to change the message:

async def on_confirm(ctx):
    if ctx.payload == "yes":
        await ctx.edit("✅ You chose YES")
    else:
        await ctx.edit("❌ You chose NO")

    await ctx.close()

ctx.edit() accepts the same parameters as utils.answer: - ctx.edit("text") — plain text - ctx.edit("text", emoji_map=...) — with custom emoji - BUT: ctx.edit() does NOT accept reply_markup (if you need a new keyboard — use utils.answer with edit_id)


6. Custom Emoji via emoji_map

You can use custom (server) emoji via shortcodes %name%:

# Send with custom emoji
await utils.answer(
    mx,
    "Petted %aam%",
    event=event,
    emoji_map={
        "aam": "mxc://your-server.org/abc123",
    },
)

# Edit with custom emoji in callback
async def handler(ctx):
    await ctx.edit(
        "You clicked %custom_emoji%",
        emoji_map={
            "custom_emoji": "mxc://server.org/xyz",
        },
    )

Format: - In text, write %shortcode% - In emoji_map, pass a dict: {"shortcode": "mxc://..."}


7. Editing with Media

When using utils.answer() with media (Image, Video, ...) and reply_markup — always pass edit_id explicitly so that reactions attach to the visible event:

# ❌ Wrong — reactions will go to an invisible edit-event
await utils.answer(mx, media=Image(url=url), reply_markup=markup)

# ✅ Correct — pass edit_id of the message being edited
await utils.answer(mx, media=Image(url=url), edit_id=message_id, reply_markup=markup)

Otherwise, EmojiKeyBoard reactions will end up on a hidden internal event and won't be visible on the message.


For regular folks: What emoji callbacks are in simple terms — see Key Concepts.