Messages¶
Sending¶
await app.send_message("me", "**hello**")
await app.send_message("@durov", "hi", silent=True)
await app.send_message(chat_id, "see above", reply_to=message.id)
The first argument is any peer: a username, a phone number, an id, "me", or
a peer out of an earlier answer. The answer is the Message the server made of it, not a
raw TL object.
Text is markdown by default. no_webpage=True stops the link preview, silent=True sends
without a notification, and reply_markup= puts buttons under it, which
only a bot may do.
From a message¶
A message knows the client it arrived on, so it can answer:
@app.on_message(filters.text)
async def handler(client, message):
await message.reply("as a reply") # replies to this message
await message.respond("in the same chat") # no reply
await message.edit("changed") # if it is ours
await message.edit_markup(keyboard([...])) # the buttons only
await message.delete()
await message.forward_to("@somewhere")
data = await message.download() # if it has media
What is on one¶
id |
the message id, per chat |
text |
the text, without formatting |
markdown / html |
the text with the formatting written back in |
entities |
the formatting itself, as Telegram spells it |
chat |
a Chat: id, title, username, is_private, is_group, is_channel |
sender |
a User: id, username, full_name, mention, is_bot, is_premium |
date |
when it was sent, as a datetime |
media |
the attachment, if any; has_media is the short question |
reply_to_id |
the id of the message this replies to |
reply_to_message |
that message itself, when it was known without asking |
partial |
whether this is an outline of a message rather than the message |
buttons |
the rows of inline buttons under it, if any |
file_ref |
its file as one string that can be written down |
outgoing |
whether we sent it |
service |
whether Telegram wrote it rather than a person |
raw |
the TL object it was built from, always |
command, arguments and match are filled in by the filters that work them out. See
Filters.
reply_to_message is usually already there, because the message being answered went past
this client a moment ago. await message.get_reply() is the same thing and fetches when
it is not. See Handling updates.
Formatting¶
await app.send_message("me", "**bold** __italic__ `code` [link](https://example.com)")
await app.send_message("me", "<b>bold</b> <i>italic</i>", parse_mode="html")
await app.send_message("me", "literally **this**", parse_mode=None)
parse_mode on the client sets the default for every call:
Offsets are counted in UTF-16 code units, which is how Telegram counts them and why formatting stays where you put it once someone sends an emoji. Getting that wrong is the classic way for bold text to drift a character to the left halfway down a message.
To send formatting you already have, pass entities and skip parsing altogether:
Editing, deleting, forwarding¶
await app.edit_message(peer, message_id, "new text")
await app.delete_messages(peer, [id1, id2]) # for everyone
await app.delete_messages(peer, [id1], everywhere=False) # only for us
await app.forward_messages(target, source, [id1, id2])
delete_messages answers with how many the server owned up to. Deleting for everyone is
allowed for a while after sending, and always in a chat you administer.
Reading history¶
Telegram answers history a page at a time and expects the client to keep asking with the
id it got to. get_history does that bookkeeping: you say how many you want and read them.
batch controls how many come per call, offset_id starts partway down.
Fetching and searching¶
found = await app.get_messages("@durov", [1234, 1235])
async for message in app.search_messages("@durov", "hello", limit=50):
print(message.id, message.text)
get_messages leaves out anything that is not there, not returning a hole, so the answer
can be shorter than what you asked for. search_messages pages itself the same way
get_history does.
An empty query with a filter asks for everything of one kind:
from sunnygram.raw import types
async for photo in app.search_messages(
"@durov", filter=types.InputMessagesFilterPhotos(), limit=20
):
await photo.download(into=f"{photo.id}.jpg")
Pinning, reading, typing¶
await app.pin_message(peer, message.id) # quietly
await app.pin_message(peer, message.id, silent=False) # and notify the chat
await app.unpin_message(peer, message.id)
await app.unpin_all_messages(peer)
await app.read_history(peer) # everything
await app.read_history(peer, max_id=1234) # up to one message
await app.send_action(peer) # "typing..."
Pinning is quiet by default, which is the opposite of Telegram's own default and the
kinder one: the noisy version notifies everybody in the chat. In a private chat it pins
only on your side unless you pass both_sides=True.
A typing action is forgotten after about six seconds, so anything slower has to say it
again. Other actions live in sunnygram.raw.types:
from sunnygram.raw import types
await app.send_action(peer, types.SendMessageUploadPhotoAction(progress=0))
Sending twice¶
Every send carries a random id, and Telegram deduplicates on it. That is what makes the invoker's retry-after-a-dropped-connection safe: a message that went out, whose answer was lost, is not sent again when the call is.
Field by field: Types reference.
Sending later¶
schedule_date queues a message instead of sending it now. A datetime or a unix
timestamp, on send_message and on everything that sends a file.
from datetime import datetime, timedelta
await app.send_message(chat, "Happy new year", schedule_date=datetime(2031, 1, 1))
await app.send_photo(chat, "cake.jpg", schedule_date=datetime.now() + timedelta(hours=2))
A naive datetime is read as local time, which is what someone writing a wall-clock time
means by it. Pass one with a tzinfo to be explicit.
sunnygram.WHEN_ONLINE is the one special value: send it the moment the recipient next
appears, instead of at a time.
from sunnygram import WHEN_ONLINE
await app.send_message(user, "Morning", schedule_date=WHEN_ONLINE)
What comes back is the queued message. It is not in the chat yet and it has its own numbering, so its id is only good for the other scheduled calls:
queued = await app.get_scheduled(chat)
await app.send_scheduled(chat, [queued[0].id]) # send it now instead
await app.delete_scheduled(chat, [queued[1].id]) # or never
A message being queued arrives as its own kind of event, not as a message, because no one has received it:
The moment it actually goes out it arrives again as an ordinary message, because by then it is one.
Replacing the file on a message¶
await app.edit_media(chat, message_id, "better.jpg")
await message.edit_media("better.jpg", caption="Fixed")
Telegram will not put a file on a message that has none and will not take one off, so this swaps a photo for another photo instead of turning text into an image. Passing no caption leaves the existing one alone.