Event payloads
Events lists which events exist and which permissions they need. This page lists what is inside each event. Every event is a CBOR map; in Python it is a dict. The shapes of ids, entities and messages are described in Data shapes.
Conventions used below:
- int is a signed 64-bit integer, text is a UTF-8 string, bool a boolean, list an array.
- “always” means the key is present; “optional” means the key is missing (not
None) when there is no value, so read it withevent.get("key"). - Chat, user and message ids are plain integers; see id conventions.
- Offsets and lengths in
entitiesare counted in UTF-16 code units, not in Python characters or bytes.
What the host removes from an event
Section titled “What the host removes from an event”The app builds the full event, then the host reduces it for your plugin:
| Event kind | Rule |
|---|---|
Chat-bound subscription events (message.*, chat.*, call.*, story.new) |
Dropped when the chat is a secret chat, Saved Messages, the Telegram service chat (777000) or outside the chats the user granted. secret and self are removed from every one of them except message.* (there both are always false). |
message.deleted without messages.read_deleted |
text and old_text (if the app sent it) are removed; entities stay. message.edited and the other message.* events are never reduced: they already need messages.read. |
Targeted events (command, menu.click, badge.click) |
chat, topic, reply_to are removed when the chat is out of your scope. message, from, ids, text and old_text are also removed unless you have messages.read (and the chat is in scope). secret and self are always removed, so they never appear in the menu event tables either. Read these keys with event.get(...). |
Hooks, timer, plugin.message, settings.*, dialog.*, window.*, toast.*, translate.request, ai.request, call.result |
Delivered as described, with no reduction. |
The value passed to @freshgram.on_init (Wasm: the fg_init argument). It is not the freshgram.info() result.
| Key | Type | Presence | Meaning |
|---|---|---|---|
account |
text | always | Id of the account this instance runs for (a decimal string such as "3"). A plugin runs once per account. |
plugin |
text | always | The plugin id from the manifest. |
version |
text | always | The installed plugin version. |
api |
int | always | The host API level (currently 7). |
@freshgram.on_initdef start(info): freshgram.log("account %s, api %d" % (info["account"], info["api"]))freshgram.info() (meta.info) answers {api, plugin, granted: [permission...], chats: "all" | [chat id...]}.
Message events
Section titled “Message events”message.new, message.edited, message.deleted, message.reaction and message.pinned share one payload. Permission: messages.read. Service messages (a pin notice, a joined member), messages in Saved Messages and messages without a server id are never reported.
| Key | Type | Presence | Meaning |
|---|---|---|---|
type |
text | always | The event name. |
id |
int | always | Message id in the chat (server id). |
chat |
int | always | Chat id. |
chat_title |
text | always | Chat title or the user’s name. |
chat_type |
text | always | user, bot, self, group, supergroup, forum, channel. |
from |
int | always | Id of the sender peer. For channel posts it is the channel id; for an anonymous admin it is the chat id. |
from_kind |
text | always | Kind of the sender, same values as chat_type. |
sender_name |
text | always | Display name of the sender. |
date |
int | always | Send time, unix seconds. |
text |
text | always | Message text or media caption, empty if none. This is the real text, not what a message hook changed for display. |
out |
bool | always | true for your own messages. |
forwarded |
bool | always | true if the message is a forward. The origin of the forward is not reported. |
secret |
bool | always | Always false here. |
self |
bool | always | Always false here. |
entities |
list | optional | Entities of the text; missing when there are none. |
reply_to |
int | optional | Id of the message this one replies to. Missing when it is not a reply. |
topic |
int | optional | Id of the topic root. 1 (the General topic) for every message outside a real topic, including ordinary chats. |
grouped |
int | optional | Album id; messages of one album share it. |
edit_date |
int | optional | Time of the last edit, unix seconds. |
media |
text | optional | Media kind: photo, video, audio, voice, round, gif, sticker, file, poll, contact, location, webpage, other. Missing for a text-only message. |
sticker |
map | optional | For a sticker: {id, emoji, set, animated, video}. |
emoji_only |
bool | optional | true (only ever true) when the text consists of emoji only. |
reactions |
list | optional | Current reactions: [{emoji | custom_emoji | paid, count, my}]. |
via_bot |
text | optional | Username of the inline bot the message was sent through. |
local_id |
int | optional | Only on message.new for a message you sent from this app: the temporary id it had before the server confirmed it. |
by_plugin |
bool | optional | Only true, only on message.new: the message was sent by a plugin (any plugin). |
pinned |
bool | optional | Only on message.pinned: true after pinning, false after unpinning. |
Differences per event:
| Event | When it arrives |
|---|---|
message.new |
A message with a server id appeared: an incoming one, or your own one once the server confirmed it (then local_id is set and out is true). |
message.edited |
The edit date or the text changed. An edit the app never saw the original of is reported only if it happened in the last 5 minutes. |
message.deleted |
The message was deleted; reported once per message. Without messages.read_deleted there is no text. |
message.reaction |
A new unread reaction appeared on a message (someone reacted to yours). reactions holds the full current summary; the reacting user is not in the event, ask messages.reactions. |
message.pinned |
A pin or unpin that the app heard about while running; pinned tells which. |
import freshgram
@freshgram.on("message.new")def on_message(event): if event["out"] or event["chat_type"] not in ("user", "group", "supergroup"): return if event.get("media") == "photo": freshgram.log("photo from %s in %s" % (event["sender_name"], event["chat_title"])) for entity in event.get("entities", []): if entity["type"] == "url": # offsets are UTF-16 units raw = event["text"].encode("utf-16-le") url = raw[entity["offset"] * 2:(entity["offset"] + entity["length"]) * 2].decode("utf-16-le") freshgram.log("link: " + url)
freshgram.run()Message hooks
Section titled “Message hooks”Declared in contributes.outgoing / contributes.incoming; see Message hooks. They are delivered to the same @freshgram.on(...) handlers. Inside the handler only the hook calls (set_outgoing, drop_outgoing, set_incoming, hide_incoming), info, log and time work.
message.outgoing
Section titled “message.outgoing”Permission: messages.transform_outgoing. The text is what is about to be sent (after earlier plugins in the chain changed it).
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Target chat id. |
chat_type |
text | always | user, bot, group, supergroup, forum, channel (never self: those sends are not offered to hooks). |
text |
text | always | The text, or the caption of a file send. |
entities |
list | always | Entities of the text; an empty list when there are none. |
silent |
bool | always | Send without sound. |
topic |
int | optional | Topic root id, missing when the send is not into a topic. |
reply_to |
int | optional | Id of the replied message (a reply to the topic root itself is not reported). |
scheduled |
int | optional | Scheduled send time, unix seconds. |
media |
list | optional | Only for a file send and only if the hook declares media: [{kind, name, size}], kind is photo, video, audio or file, size in bytes. |
@freshgram.on("message.outgoing")def outgoing(event): if "password" in event["text"].lower(): freshgram.messages.drop_outgoing("Looks like a password") elif event["chat_type"] == "channel": freshgram.messages.set_outgoing(silent=True)message.incoming
Section titled “message.incoming”Permission: messages.transform_incoming. The map is the message object (full form: id, chat, from, from_kind, date, out, reply_to, topic, media, sticker, emoji_only, reactions, buttons, …) plus:
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat_type |
text | always | Kind of the chat. |
edited |
bool | always | true if the message was edited and is shown again. |
service |
bool | optional | Only true: a service message (hooks with service: true only); text is then the service text, entities empty, and the message can only be hidden. |
text |
text | always | Text as the previous hooks left it. |
entities |
list | always | Entities, an empty list when none. |
The event has no secret, chat_title, sender_name, forwarded or edit_date.
@freshgram.on("message.incoming")def incoming(event): if event["from_kind"] == "bot" and "casino" in event["text"].lower(): freshgram.messages.hide_incoming("spam")Chat, user, story, call and app events
Section titled “Chat, user, story, call and app events”All of these are subscription events and need the listed permission.
chat.opened
Section titled “chat.opened”Permission chats.active. The user switched the active chat.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Chat id. |
chat_title |
text | always | Chat title. |
topic |
int | optional | Topic root id when a topic of a forum was opened. |
chat.typing
Section titled “chat.typing”Permission chats.activity. Repeats of the same action are merged: at most one event per 3 seconds for the same chat, user and action. Chats of blocked users are skipped.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Chat id. |
user |
int | always | Who is doing it. |
action |
text | always | typing, record_voice, upload_voice, record_video, upload_video, record_round, upload_photo, upload_file, choose_sticker, other. |
cancelled |
bool | always | true when the user stopped (action is typing then). |
topic |
int | optional | Topic root id. |
chat.read
Section titled “chat.read”Permission chats.activity. Only fires when the read marker moves forward. Inbox reads (outbox: false) are not reported while ghost mode is active.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Chat id. |
read_till |
int | always | Id of the last message read. |
outbox |
bool | always | true: the other side read your messages; false: you read theirs. |
chat.member
Section titled “chat.member”Permission chats.activity.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Group or channel id. |
user |
int | always | Who joined or left. |
joined |
bool | always | true joined, false left or was removed. |
chat.title_changed
Section titled “chat.title_changed”Permission chats.activity. Not sent for users.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | Chat id. |
title |
text | always | The new title. |
old_title |
text | optional | Missing when the app had not seen the earlier title. |
user.status
Section titled “user.status”Permission users.presence. Not chat-bound. Your own status, bots, blocked users and hidden statuses are not reported; repeats of the same status are merged.
| Key | Type | Presence | Meaning |
|---|---|---|---|
user |
int | always | User id. |
status |
text | always | online, recently, week, month, long_ago, offline. |
last_seen |
int | optional | Unix time the user was last online (or is online till); missing when unknown. |
@freshgram.on_user_statusdef status(event): if event["status"] == "online": freshgram.log("user %d is online" % event["user"])story.new
Section titled “story.new”Permission stories.read. The story itself is not passed.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | The poster (a user or a channel). |
story_id |
int | always | Story id. |
date |
int | always | Time the app noticed it, unix seconds (not the post time). |
call.incoming and call.ended
Section titled “call.incoming and call.ended”Permission calls.activity. Only one-to-one calls with a user.
| Key | Type | Presence | Meaning |
|---|---|---|---|
chat |
int | always | The other user’s id. |
user |
int | always | The same id as chat. |
incoming |
bool | always | true for an incoming call. |
reason |
text | call.ended only |
ended, busy, failed, other_device. |
duration |
int | call.ended only |
Seconds since the call was established, 0 if it never was. |
call.incoming arrives only for incoming calls. call.ended arrives for both directions, once per call.
app.focus and app.blur
Section titled “app.focus and app.blur”Permission app.activity. Only {type}. Not sent while ghost mode is active.
Events from the user interface
Section titled “Events from the user interface”These need no subscription but need the matching UI permission.
command
Section titled “command”Permission ui.command; the command must be declared in contributes.commands. Sent when the user types /name args in a chat and presses send (the text is not sent to the chat).
| Key | Type | Presence | Meaning |
|---|---|---|---|
name |
text | always | The command name without the slash. |
args |
text | always | The rest of the line, trimmed; empty if none. |
chat |
int | usually | Chat id (removed when out of your scope). |
topic |
int | optional | Topic root id when typed inside a topic. |
reply_to |
int | optional | Id of the message the composer was replying to. |
menu.click
Section titled “menu.click”Permission ui.menu (ui.composer_button for composer); the item must be declared in contributes.menus. surface is one of message, chat, profile, composer, main_menu, tray, selection, shared_media. Composer buttons are menu items with surface: "composer"; there are no other composer events.
| Key | Type | Presence | Meaning |
|---|---|---|---|
surface |
text | always | Where the item was clicked. |
id |
text | always | The id from the manifest. |
button |
text | optional | "right" for a right click on a composer button. |
chat |
int | message, chat, profile, composer, selection, shared_media |
Chat id. |
topic |
int | optional | Topic root id (for message it is the message’s topic, 1 outside topics). |
reply_to |
int | composer |
Id of the message being replied to, if any. |
message |
int | message |
Id of the clicked message (needs messages.read). |
from |
int | message |
Sender id of that message (needs messages.read). |
text |
text | message |
Text of that message (needs messages.read). |
ids |
list of int | selection, shared_media |
Ids of the selected messages, at most 100 (needs messages.read). |
main_menu and tray clicks carry only surface and id.
@freshgram.on_menu("copy-link", surface="message")def copy_link(event): if "message" in event: freshgram.ui.toast("message %d: %d chars" % (event["message"], len(event["text"])))badge.click
Section titled “badge.click”Permission ui.badges. The user clicked a badge set with badges.set.
| Key | Type | Presence | Meaning |
|---|---|---|---|
id |
text | always | Badge id. |
user |
int | always | The peer the badge belongs to. |
surface |
text | always | message, chat (chat header), profile, chats (chat list). |
chat |
int | message only |
Chat of the message. |
message |
int | message only |
Id of the message (needs messages.read). |
Set by timers.set. Only {id: text}. Fires even if the plugin was unloaded.
plugin.message
Section titled “plugin.message”Permission plugins.message.
| Key | Type | Presence | Meaning |
|---|---|---|---|
from |
text | always | Id of the sending plugin. |
data |
any | always | Whatever the sender passed to plugins.send. |
settings., dialog. and window.*
Section titled “settings., dialog. and window.*”settings.* needs ui.settings_page, dialog.* and window.* need ui.dialog.
| Event | Payload | Meaning |
|---|---|---|
settings.open |
{} |
The user opened the plugin’s settings. Send the page here with ui.settings_page. |
settings.change / dialog.change / window.change |
id, value, window* |
An input item changed. |
settings.click / dialog.click / window.click |
id, window* |
A button or list entry was pressed. |
settings.long_click / dialog.long_click / window.long_click |
id, window* |
Long press or right click on an item that has long_click: true. |
dialog.close |
{} |
The dialog was closed. |
window.close |
window |
The window was closed. |
* window (text, the window id) is present on all window.* events and absent on settings.* and dialog.*.
id is the item id from your page (text). The type of value depends on the item:
| Item | value |
|---|---|
toggle |
bool |
slider |
int |
input |
text |
select |
text, the chosen option value |
tabs |
text, the id of the opened tab |
chat |
int, the chosen chat id |
@freshgram.on_setting("enabled")def toggled(event): freshgram.storage.set("enabled", event["value"])toast.click and toast.close
Section titled “toast.click and toast.close”Permission ui.toast, for a toast shown with action.
| Event | Keys | Meaning |
|---|---|---|
toast.click |
id (text) |
The button was pressed. |
toast.close |
id (text), reason (text) |
The toast went away; reason is "action" after a press, "timeout" otherwise. |
translate.request
Section titled “translate.request”Permission translate.provide; the plugin must declare contributes.translator. Answer with translate.reply (the @on_translate decorator does it).
| Key | Type | Presence | Meaning |
|---|---|---|---|
ticket |
int | always | Pass it back in translate.reply. |
to |
text | always | Two-letter target language code. |
format |
text | always | Always "plain". |
items |
list of text | always | Up to 20 texts; answer with the same number of items. |
ai.request
Section titled “ai.request”Permission ai.provide; the plugin must declare contributes.aiProvider. Answer with ai.reply (@on_ai).
| Key | Type | Presence | Meaning |
|---|---|---|---|
ticket |
int | always | Pass it back in ai.reply. |
text |
text | always | The text to process, formatting removed. |
format |
text | always | Always "plain". |
proofread |
bool | always | Fix errors. |
emojify |
bool | always | Add emoji. |
translate_to |
text | optional | Two-letter language code to translate into. |
tone |
text | optional | Requested tone, such as formal. |
prompt |
text | optional | A free instruction from the user. |
@on_ai hands your function the same dict without type, ticket and format.
call.result
Section titled “call.result”The result of a call made with "async": true (all of freshgram.aio). The immediate reply to such a call is {ok: true, v: {ticket: N}}; later the event arrives. The Python SDK resolves the future for you; with the raw protocol match on ticket.
| Key | Type | Presence | Meaning |
|---|---|---|---|
type |
text | always | "call.result". |
ticket |
int | always | The ticket from the immediate reply. |
ok |
bool | always | true on success. |
v |
any | when ok |
The same value the synchronous call would return. |
e |
text | when not ok |
Error code: denied, invalid, not_found, quota, limit, unavailable, failed. |
m |
text | when not ok |
A short explanation. |
So the shape of v for each operation is exactly the result described in Data shapes; only the delivery differs. Operations whose v is empty (time.sleep, messages.edit, messages.delete, ui.toast, …) answer v: None. If the plugin was stopped before the answer, ok is false with e: "unavailable". A result larger than the message limit becomes e: "limit".
from freshgram import aio
async def last_text(chat): history = await aio.messages.history(chat, limit=1) # call.result with v = list of messages return history[0]["text"] if history else ""