Messages¶
This guide shows how a bot reads the messages it gets, and how it sends, edits and deletes its own.
What a message looks like¶
A handler for messages gets a Message:
@bot.on_message()
async def show(message):
print(message.id) # 1834, unique inside the chat
print(message.chat.id) # 7781
print(message.chat.kind) # ChatKind.PRIVATE, GROUP or CHANNEL
print(message.from_user.username) # "anna", or None if she has no handle
print(message.text) # what she wrote, or None
print(message.date) # a datetime in UTC
Field |
Type |
Meaning |
|---|---|---|
|
|
the message ID, unique inside its chat |
|
|
|
|
|
who wrote it. |
|
|
when it was sent, in UTC |
|
|
the ID of the message it answers |
|
|
the text, already decrypted in an encrypted chat |
|
tuple of |
pictures, clips and files attached |
|
|
for a signed channel post with no |
|
|
|
|
|
the original OpenPGP message, in an encrypted chat |
|
|
the exact decrypted body (the app’s internal format), in an encrypted chat |
|
|
the result of the signature check, in an encrypted chat |
|
|
the JSON the server sent, untouched |
Some fields are shortcuts: message.chat_id is message.chat.id, and message.is_command
is true when the text starts with /.
Text in readable chats¶
In a readable chat the server hands the text over slightly cleaned: spaces at the start and at the end are removed, runs of empty lines become one, and the text is cut at 4000 characters. In an encrypted chat you get the exact text the person wrote.
Stickers, calls, payment requests¶
For a sticker, a call notice or a payment request, message.text is None. In an encrypted
chat, message.plaintext still holds the app’s internal format if you need it.
Media¶
message.media lists what came with the message:
from pathlib import Path
from pgpcbot import filters as f
@bot.on_message(f.has_media)
async def save_photo(message):
for item in message.media:
print(item.kind, item.mime, item.width, item.height, item.duration_ms)
data = await bot.download_media(item) # bytes, decrypted if needed
Path(f"{item.id}.bin").write_bytes(data)
download_media refuses files bigger than max_bytes (64 MiB by default). In an encrypted
chat the file is decrypted for you. In an encrypted group, phones seal attachments only to
the chat’s members, so a bot in privacy mode can read the text but not the attachments: the
download then fails with NotARecipientError.
Sending¶
Replying¶
await message.reply("Got it") # quotes the message
await message.reply("Got it", quote=False) # same chat, no quote
await message.answer("Got it") # the same as quote=False
Sending into any chat¶
await bot.send_message(7781, "Last night's backup finished fine.")
await bot.send_message(message.chat, "Hello") # a chat object works too
await bot.send_message(7781, "Done", reply_to=1834)
The bot can only write into chats it is in. In a private chat, the person must have started the chat with the bot first.
Encryption is automatic¶
You never choose between “text” and “encrypted” yourself. Before sending, the library checks the chat’s mode:
readable chat: the text is sent as it is;
encrypted chat: the text is encrypted for every member and for the bot, signed with the bot’s key, and sent as an OpenPGP message.
If the chat is encrypted and the message cannot be encrypted (no keys, no private key, a bad
key in a private chat), nothing is sent and you get an EncryptionError. The library never
falls back to plain text. The OpenPGP guide explains the details.
You can state what you expect for one message:
await message.reply("Your code is 4471", encrypt=True) # EncryptionRequiredError in a readable chat
await message.reply("Public notice", encrypt=False) # ValidationError in an encrypted chat
Limits¶
What |
Limit |
|---|---|
text |
1 to 4096 characters, counted in UTF-16 units like JavaScript does: most emoji count 2 |
characters |
no control characters except new lines and tabs. |
encrypted message |
131072 characters of armored text |
The library checks these before sending and raises ValidationError with the field name.
The numbers come from the server (GET /me), so they follow the server you are on.
Silent posts¶
await bot.send_message(channel_id, "Maintenance tonight", silent=True)
silent only matters in channels: the post goes out without a notification.
Retries never write a message twice¶
Every send gets a sendId, a UUID made once and reused for every attempt. If an answer gets
lost on the network and the library tries again, the server recognises the sendId and gives
back the message it already wrote. You can pass your own:
import uuid
send_id = uuid.uuid4() # save it with your job, before sending
await bot.send_message(7781, "Invoice 2026-118 is ready", send_id=send_id)
If your own program crashes and runs the job again with the same send_id, the person still
gets one message. A sendId belongs to one chat: using it in another chat raises
SendIdConflictError.
Showing “typing…”¶
await message.typing() # or: await bot.send_typing(chat_id)
result = await slow_search(message.text)
await message.reply(result)
The indicator goes away after a few seconds or when the message arrives. The server sends at
most one every 3 seconds per chat; extra calls return False and do nothing.
Editing¶
reply and send_message return a SentMessage, which you can edit or delete later:
sent = await message.reply("Working on it...")
await asyncio.sleep(2)
await sent.edit("Done: 14 files checked.")
Or by ID:
await bot.edit_message(chat_id, message_id, "New text")
The bot can edit only its own ordinary messages, at any age.
In an encrypted chat the new text is encrypted like a send.
Leave
markupout to keep the buttons;markup=Noneremoves them.Edits have their own rate limits: one per second in a private chat, twenty per minute in a group.
Deleting¶
await message.delete() # a message someone else sent
await sent.delete() # one of the bot's own
await bot.delete_message(chat_id, message_id)
The bot can delete its own messages anywhere, and other people’s messages in a group or
channel where it has the “delete messages” right. A message that is already gone raises
MessageNotFoundError. After a timeout the library treats that error as “done”, so a retry is
safe.
Edited and deleted messages¶
@bot.on_edited_message()
async def edited(message):
print("now it says:", message.text)
@bot.on_deleted_messages()
async def deleted(event):
print("gone from", event.chat.id, ":", event.message_ids)
Sending without handlers¶
For a script that only sends, use the client directly. It does not encrypt anything, so it fits readable chats:
from pgpcbot import Client
with Client() as api:
api.send_message(7781, "Backup finished.")
For encrypted chats outside a handler, use the bot’s methods, which encrypt:
import asyncio
from pgpcbot import Bot
async def main():
async with Bot(pgp_private_key="bot-key.asc", pgp_passphrase=...) as bot:
await bot.send_message(7781, "Backup finished.")
asyncio.run(main())