Skip to content

Types

The hand-written wrappers a handler actually receives. Each keeps the TL object it was built from on .raw, so nothing is lost by taking the friendly one.

sunnygram.types.message.Message dataclass

One message, and what can be done about it.

markdown property

markdown: str

The text with its formatting written back in as markdown.

html property

html: str

The text with its formatting written back in as HTML.

payment property

payment: Any

The payment this service message reports, if it reports one.

A successful payment arrives as a service message instead of as an update of its own, so this is where a bot finds out it was paid. Only the bot that sold the thing gets the reading carrying the charge id, which a refund needs.

chat_id property

chat_id: int | None

Which chat this is in, by id, even when nothing named the chat.

A message that arrives alongside an answer comes with the users and chats that answer carried, and chat is the friendly form of that. A message the server made of something we just sent often arrives with none of them, so there is an id inside it and nothing to build a Chat out of. Everything that only needs the id works either way.

buttons property

buttons: list[list[Any]]

The rows of inline buttons under this message, if it has any.

file_ref property

file_ref: str

This message's file, as one string that can be written down.

The portable form: put it in a database, hand it to another process, and sending the same file later costs one call with no upload and no download. It names where the message came from as well, so the token inside it can be renewed when it goes stale.

album_id property

album_id: int | None

Which album this message is part of, if it is part of one.

An album is not one message carrying several files. It is several messages that share this id, which the clients then draw as one block, so a photo in an album arrives here on its own like any other.

react async

react(reaction: Any = None, **options: Any) -> None

React to this message, or clear this account's reactions on it.

copy_to async

copy_to(target: Any, **options: Any) -> Message

Send this message on without saying where it came from.

The difference from forward_to is what the other side sees: a forward carries the original author's name, and this does not.

vote async

vote(*options: int) -> Any

Answer the poll this message carries, by answer position.

reply async

reply(text: str, **options: Any) -> Message

Answer this message in its own chat, as a reply to it.

respond async

respond(text: str, **options: Any) -> Message

Say something in the same chat, without replying to anything.

reply_file async

reply_file(file: Any, **options: Any) -> Message

Answer this message with a file, as a reply to it.

The kind is worked out from the name, so a .mp4 arrives playable and a .jpg as a photo. Pass kind= to overrule that.

reply_album async

reply_album(files: Any, **options: Any) -> list[Message]

Answer this message with several files as one group.

reply_media async

reply_media(media: Any, **options: Any) -> Message

Answer with a file Telegram already holds, uploading nothing.

reply_file is for bytes that have to go up. This is for a file that is already there: what another message carries, or what a program wrote down after sending it once. Passing one of those to reply_file would try to upload the reference itself.

edit async

edit(text: str, **options: Any) -> Message

Rewrite this message, which only works on one of ours.

edit_media async

edit_media(media: Any, **options: Any) -> Message

Replace the file this message carries with another one.

edit_markup async

edit_markup(markup: Any = None) -> Message

Change the buttons under this message, or take them away.

get_reply async

get_reply() -> Message | None

The message this one answers, fetched only if it has to be.

Nearly always it does not: the message being replied to came past this client a moment ago and is already on reply_to_message, and the quoted kind of reply carries an outline of it in the update itself. Both are answered from here without a call. What is left is an old message someone scrolled up to, and that costs one.

An outline is not enough for this, so a quote is exchanged here for the message it was taken from.

delete async

delete(*, everywhere: bool = True) -> None

Take this message back.

forward_to async

forward_to(target: Any) -> None

Send this message on to someone else.

download async

download(**options: Any) -> Any

Fetch whatever file this message carries.

The message goes down instead of the media off it, so that a file reference which has gone stale can be renewed: the message is what renews it, and the media on its own does not say which message that was.

from_raw classmethod

from_raw(message: Any, *, users: dict[int, Any] | None = None, chats: dict[int, Any] | None = None, replies: dict[int, Any] | None = None, client: Any = None) -> Message | None

Wrap a message off the wire, with whatever came alongside it.

The users and chats are the ones the same answer carried. They are what turns the ids inside a message into people and places, which is why every call that returns messages returns them too.

replies is the same idea one step further: the other messages in the same answer, so that a reply to one of them is tied to it here rather than fetched later. A page of history usually contains both halves of a conversation, and pairing them up costs a dict lookup.

sunnygram.types.callback.CallbackQuery dataclass

One press of an inline button.

text property

text: str

The payload as text, which is how nearly every bot writes one.

Payloads are bytes on the wire and a program almost always puts a short string in them, so this is the form handlers and filters read. A payload that is not text at all reads as empty instead of raising, since a filter asking about it is not the place to find that out.

chat_id property

chat_id: int | None

Which chat the press came from, even when nothing named the chat.

The same shape as Message.chat_id and for the same reason: chat is built out of the users and chats an update carried, and an update that carried none of them still has the peer inside it. A press on an inline message has no chat at all and answers None, which is the honest answer instead of a zero that would compare equal to something.

is_inline property

is_inline: bool

Whether the message this was pressed on came from an inline query.

answer async

answer(text: str = '', *, alert: bool = False, url: str | None = None, cache_time: int = 0) -> None

Stop the spinner, and optionally say something while doing it.

Nothing at all is a valid answer and is what a bot sends when the real reply is an edit to the message. text puts a notice along the top of the screen; alert makes it a box they have to dismiss instead.

cache_time lets the client answer the same press itself for that many seconds without asking again, which is worth setting for a button whose answer cannot change.

edit async

edit(text: str, **options: Any) -> Message | None

Rewrite the message this button is under.

The usual way to answer a press: the message becomes the new state and the buttons change with it. An inline message answers with nothing, because Telegram says only whether the edit went through.

edit_markup async

edit_markup(markup: Any = None) -> Message | None

Change the buttons and leave the text alone, or take them away.

Passing nothing removes the keyboard, which a bot does with a one-shot menu once it has been used.

get_message async

get_message() -> Message

Fetch the message this button is under.

Costs a call. An inline message cannot be fetched at all: it has no chat to fetch it from, which is why editing one takes the opaque id rather than a message.

reply async

reply(text: str, **options: Any) -> Message

Say something new in the chat, as a reply to the message pressed.

from_raw classmethod

from_raw(update: Any, *, users: dict[int, Any] | None = None, chats: dict[int, Any] | None = None, client: Any = None) -> CallbackQuery | None

Wrap a press off the wire, with whatever came alongside it.

sunnygram.types.user.User dataclass

Someone with an account.

full_name property

full_name: str

First and last together, or whichever of them there is.

marked_id property

marked_id: int

The same id, for code that stores peers without knowing the kind.

A person's id is already unambiguous, so this is the id itself. It is here so that anything holding either a Chat or a User can write down one number without asking which it is holding.

mention property

mention: str

A markdown link that names this person even without a username.

from_raw classmethod

from_raw(user: Any) -> User | None

Wrap a user off the wire, or answer None for one with nothing in it.

sunnygram.types.chat.Chat dataclass

A conversation, whoever or whatever is on the other side of it.

marked_id property

marked_id: int

This chat's id in the spelling that says what it is on its own.

id is the id the protocol uses, which is the right one for a raw call and the wrong one to write down on its own: the number does not say which of the three id spaces it came from, so a stored 3003 could be a person or a small group. This pairs it back with the kind, which is what anything keeping a chat id in a database or a config wants.

is_private property

is_private: bool

Whether this is one person talking to another.

is_group property

is_group: bool

Whether people talk here, small group or supergroup alike.

is_channel property

is_channel: bool

Whether this is a broadcast, where only admins post.

of_peer classmethod

of_peer(peer: Any, users: dict[int, Any], chats: dict[int, Any]) -> Chat | None

The chat a Peer names, out of what came alongside it.

A Peer is an id and which of the three id spaces it is in, and nothing else, so it only becomes a chat with the users and chats the same answer carried.

from_raw classmethod

from_raw(peer: Any) -> Chat | None

Wrap whatever a chat arrived as.

sunnygram.types.dialog.Dialog dataclass

A conversation as it appears in the list of them.

send async

send(text: str, **options: Any) -> Message

Say something here.

read async

read() -> None

Mark everything in it as read.

from_raw classmethod

from_raw(dialog: Any, *, users: dict[int, Any] | None = None, chats: dict[int, Any] | None = None, messages: dict[int, Any] | None = None, client: Any = None) -> Dialog | None

Wrap a dialog, with everything the same page carried.

The last message is looked up by id among the messages that came with it instead of fetched, which is why this takes them: the answer that carries the dialogs carries their last messages too, and going back for one would be a round trip per row.

sunnygram.types.topic.Topic dataclass

A thread in a forum, as someone reading it sees it.

send async

send(text: str, **options: Any) -> Message

Say something in this topic.

close async

close() -> Any

Stop anybody but an administrator posting here.

reopen async

reopen() -> Any

Let people post here again.

delete async

delete() -> int

Delete this topic and everything in it.

from_raw classmethod

from_raw(topic: Any, *, chat_id: int = 0, users: dict[int, Any] | None = None, chats: dict[int, Any] | None = None, messages: dict[int, Any] | None = None, client: Any = None) -> Topic | None

Wrap a topic, with the messages that came on the same page.

A deleted topic arrives as a different constructor carrying only an id, and there is nothing to wrap in that, so it comes back as nothing.

Inline mode

Both halves of it: the query that arrived, the result that goes back, and what was picked. See Inline mode.

sunnygram.types.inline.InlineQuery dataclass

What someone has typed after the bot's name, so far.

answer async

answer(results: list[InlineResult | Any], *, cache_time: int = CACHE_TIME, gallery: bool = False, private: bool = False, next_offset: str = '', switch_pm: str = '', start_parameter: str = '', parse_mode: str | None = '') -> bool

Offer these results, which stops the client loading.

Answering is not optional. Telegram holds the query open until the bot says something about it, and until then the person sees a panel that never finishes. An empty list is a complete answer and is what a bot sends when it has nothing for that query.

gallery draws the results as a grid of pictures rather than as a list of rows. private means the answer was built for this one person and must not be cached for anybody else, which matters the moment a result depends on who asked. next_offset is the cursor the next query in the same session arrives with, and is how a long list is paged: hand back where this page ended and the client asks for the rest by scrolling.

switch_pm puts a button above the results that takes the person into the bot's own chat, carrying start_parameter with them. That is how a bot that needs setting up first says so, instead of answering with an apology it has no way to act on.

from_raw classmethod

from_raw(update: Any, *, users: dict[int, Any] | None = None, client: Any = None) -> InlineQuery | None

Wrap a query off the wire, with whoever came alongside it.

sunnygram.types.inline.InlineResult dataclass

One thing a bot offers in answer to an inline query.

Built by the factories below instead of by hand: each of them knows which of Telegram's four result constructors it needs and what has to travel with it. What they have in common is the message, which gets sent if this result is the one picked, and which is not built until the answer goes out because the parse mode belongs to the client rather than to the result.

article classmethod

article(title: str, text: str, *, description: str = '', url: str = '', thumb: str = '', id: str = '', no_webpage: bool = False, reply_markup: Any = None) -> InlineResult

A row of text, which is what most bots answer with.

The title and description are what the person reads in the list; text is the message they send by picking it, and the two have no reason to be the same. A url is shown as where the article came from and is not opened by picking it.

photo classmethod

photo(photo: Any, *, caption: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A photo, either one Telegram holds or one on the web.

Anything send_media takes is a photo it already holds: a Photo off a message, an InputPhoto, a portable file reference, a foreign file id. A http link is the other case, and Telegram fetches it itself when the result is picked, so the link has to still work then.

A photo Telegram already holds carries no title or description, because its own constructor has nowhere to put them.

animation classmethod

animation(animation: Any, *, caption: str = '', title: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A soundless looping video, which is what a gif is here.

Telegram tells the two web forms apart by what is being served: a real gif is a gif, and an mp4 with no sound is what every client actually wants, so a link ending in mp4 is offered as that.

video classmethod

video(video: Any, *, caption: str = '', title: str = '', description: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A video, held or on the web.

audio classmethod

audio(audio: Any, *, caption: str = '', title: str = '', description: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A song or any other audio file.

voice classmethod

voice(voice: Any, *, caption: str = '', title: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A voice note, which is an audio file drawn as a waveform.

document classmethod

document(document: Any, *, caption: str = '', title: str = '', description: str = '', thumb: str = '', mime: str = 'application/octet-stream', id: str = '', reply_markup: Any = None) -> InlineResult

Any file at all, which is what everything that is not a photo is.

The mime type only matters for the web form, where Telegram has nothing but the link to go on. Telegram allows pdf and zip there and nothing else, which is its rule instead of this one.

sticker classmethod

sticker(sticker: Any, *, id: str = '', reply_markup: Any = None) -> InlineResult

A sticker, which has to be one Telegram already holds.

There is no web form for this one: a sticker is a document in a set and a link to an image is not one.

location classmethod

location(latitude: float, longitude: float, title: str, *, live_period: int = 0, heading: int = 0, thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A point on the map.

live_period turns it into a location that keeps updating for that many seconds, which is what sharing your position looks like.

venue classmethod

venue(latitude: float, longitude: float, title: str, address: str, *, provider: str = '', venue_id: str = '', venue_type: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

A place, which is a point on the map with a name and a street.

contact classmethod

contact(phone: str, first_name: str, *, last_name: str = '', vcard: str = '', thumb: str = '', id: str = '', reply_markup: Any = None) -> InlineResult

Someone's phone number, as a contact card.

game classmethod

game(short_name: str, *, id: str = '', reply_markup: Any = None) -> InlineResult

One of the bot's games, by the short name it was registered with.

to_raw

to_raw(style: Style | None = None) -> base.InputBotInlineResult

This result as the protocol spells it.

Four constructors, and which one is right follows from what the result carries: a game is its own, a photo Telegram holds is its own, any other file it holds is the document one, and everything else, web forms included, is the general one.

sunnygram.types.inline.ChosenResult dataclass

Which result someone picked, and what they had typed to find it.

Only bots that asked for this are told, and asking is a setting on the bot instead of a call: BotFather calls it inline feedback. Telegram samples it for busy bots, so this is a statistic, not a receipt, and a program that has to know something happened should learn it from the message.

editable property

editable: bool

Whether the message that was sent can still be rewritten.

Only if the result carried an inline keyboard. Telegram issues an id for that message so the buttons under it can be answered, and with no buttons there is nothing to answer and no id, so the message is gone from the bot's reach the moment it is sent.

edit async

edit(text: str, **options: Any) -> None

Rewrite the message this result sent.

edit_markup async

edit_markup(markup: Any = None) -> None

Change the buttons under the message this result sent.

from_raw classmethod

from_raw(update: Any, *, users: dict[int, Any] | None = None, client: Any = None) -> ChosenResult | None

Wrap a chosen result off the wire.

Members

sunnygram.types.member.Member dataclass

Someone's standing in a chat, whichever kind of chat it is.

is_admin property

is_admin: bool

Whether they may act on the chat, the creator included.

This is the question most callers are really asking, and asking it directly avoids the mistake of testing for admin and forgetting that the creator is not one.

present property

present: bool

Whether they are in the chat at all.

from_raw classmethod

from_raw(participant: Any, *, chat_id: int = 0) -> Member

Wrap whichever of the nine constructors arrived.

sunnygram.types.member.MemberStatus

Bases: StrEnum

Where someone stands, in the six ways Telegram distinguishes.

sunnygram.types.member.MemberUpdate dataclass

Someone's standing in a chat changing, from what to what.

Both kinds of chat produce this and they are the same thing here. What Telegram gives is a pair, and the pair is what answers every question worth asking: someone with no standing before and a standing now has joined; someone who was an ordinary member and is now an admin has been promoted; someone present before and absent now has left, or has been thrown out, and which of those it was is whether they did it to themselves.

Either side of the pair can be missing, and the missing one is the information: no standing before means they were not here, and no standing after means they are not here now.

chat_id is marked the Bot API way, which is the spelling resolve takes back, so a greeter can answer straight into the chat. The members inside carry no chat id of their own, deliberately: one number for the chat, in one place, instead of the same chat spelled two ways in one object.

what property

what: str

One word for what happened, which a log line wants.

The words are the questions below, tried in the order that makes the most specific one win: being banned is also leaving, and being promoted while joining is still joining.

was_present property

was_present: bool

Whether they were in the chat before this.

is_present property

is_present: bool

Whether they are in the chat now.

joined property

joined: bool

Whether this is someone arriving.

left property

left: bool

Whether this is someone going, by their own choice or not.

banned property

banned: bool

Whether they were thrown out instead of merely gone.

restricted property

restricted: bool

Whether they are present and newly limited in what they may do.

promoted property

promoted: bool

Whether they may now run the place and could not before.

demoted property

demoted: bool

Whether they could run the place before and may not now.

by_self property

by_self: bool

Whether they did this to themselves.

The difference between joining and being added, and between leaving and being removed. Telegram says both with the same pair and only the actor tells them apart.

invite_link: str

The link they came in through, if they came in through one.

Worth reading on a join: a chat with several links knows which campaign someone arrived from, and this is where that is said.

status property

status: MemberStatus | None

Where they stand now, or nothing if they are no longer anywhere.

from_raw classmethod

from_raw(update: Any) -> MemberUpdate | None

Wrap either of the two updates that say this.

sunnygram.types.join.JoinRequest dataclass

One person waiting to be let into one chat.

invite_link: str

The link they used, when it was a link they could be told apart by.

A chat with several links can tell where someone came from, which is the whole reason to have several. A request that came in some other way has nothing here instead of a made up link.

approve async

approve() -> None

Let them in.

decline async

decline() -> None

Turn them down.

They are told, and they may ask again: this is a refusal, not a ban. Banning is what stops someone asking forever.

from_raw classmethod

from_raw(update: Any, *, users: dict[int, Any] | None = None, client: Any = None) -> JoinRequest | None

Wrap a request off the wire, with whoever came alongside it.

Reactions and polls

sunnygram.types.reaction.ReactionUpdate dataclass

Reactions on one message changing, in whichever way we were told.

by_person property

by_person: bool

Whether this reading is one person's reactions, not totals.

The one question worth asking before reading anything else. A bot gets this reading and a user account gets the other, and which one arrived decides whether actor_id and the before and after pair mean anything.

added property

added: tuple[str | int, ...]

What this person just put on the message.

Empty on the totals reading, because it cannot be worked out from totals: two people swapping reactions leaves every number the same.

removed property

removed: tuple[str | int, ...]

What this person just took off the message.

total property

total: int

How many reactions the message carries, over every kind.

get_message async

get_message() -> Message

Fetch the message these reactions are on. Costs a call.

from_raw classmethod

from_raw(update: Any, *, client: Any = None) -> ReactionUpdate | None

Wrap whichever of the three updates that say this arrived.

sunnygram.types.poll.Poll dataclass

A poll as it stands, as far as this update says.

known property

known: bool

Whether this update carried the poll itself or only its results.

Telegram sends the question and the answers when the poll changed and the results alone the rest of the time, so a program that wants to draw the whole poll from an update either keeps the first one it saw or asks for it with get_poll.

located property

located: bool

Whether this update said which message the poll is in.

It usually does not. The poll id is what Telegram considers the name of a poll, and the message is only mentioned when the update happens to have it, so anything acting on the message has to be told where it is.

winner property

winner: PollAnswer | None

The answer with the most votes, or nothing if the poll is tied.

Nothing instead of an arbitrary one: a tie is a real outcome and picking a side of it silently is the kind of thing that is found out much later.

correct property

correct: PollAnswer | None

The right answer, for a quiz that has said which it is.

close async

close() -> None

Stop the poll taking votes, which cannot be undone.

vote async

vote(*positions: int) -> Any

Answer the poll by position, or with nothing to take a vote back.

refresh async

refresh() -> Any

Ask for the poll's standing instead of waiting to be told.

from_raw classmethod

from_raw(update: Any, *, client: Any = None) -> Poll | None

Wrap a poll update, with or without the poll itself in it.

sunnygram.types.poll.PollAnswer dataclass

One of the things a poll offers, and how it is doing.

sunnygram.types.poll.PollVote dataclass

One person's vote in one poll.

Only a public poll produces this. The answers are positions, which is how they are counted from when the poll is sent, so a program reads options[0] as an index into the answers it wrote.

retracted property

retracted: bool

Whether this is someone taking their vote back, not casting one.

from_raw classmethod

from_raw(update: Any) -> PollVote | None

Wrap a vote off the wire.

The small events

Records with nothing to do about them, which is why they share a file.

sunnygram.types.events.DeletedMessages dataclass

Messages that are gone, and where they were if that was said.

located property

located: bool

Whether Telegram said which chat these were deleted from.

Only a channel says so. Everywhere else the ids belong to the account's own numbering across all of its private chats and small groups, and no chat is named, so this is False and chat_id is zero, not a plausible wrong answer.

from_raw classmethod

from_raw(update: Any) -> DeletedMessages | None

Wrap either of the two deletion updates.

sunnygram.types.events.Status dataclass

Someone being online, or having been, as far as they let us see.

online property

online: bool

Whether they are here right now.

from_raw classmethod

from_raw(update: Any) -> Status | None

Wrap a status change off the wire.

sunnygram.types.events.Typing dataclass

Someone doing something in a chat that is worth showing.

Typing is one of eighteen of these and the rest are just as real: recording a voice note, uploading a video, picking a sticker. They are all one event with a word for which, since a program either shows what someone is doing or ignores all of it.

from_raw classmethod

from_raw(update: Any) -> Typing | None

Wrap any of the three updates that say someone is doing something.

sunnygram.types.events.Blocked dataclass

Someone blocking this account, or unblocking it.

from_raw classmethod

from_raw(update: Any) -> Blocked | None

Wrap a block off the wire.

sunnygram.types.events.Stopped dataclass

Someone stopping a bot, or starting it again after having stopped it.

The bot side of being blocked, and the one number every bot should watch: it is the difference between a quiet audience and one that has left.

from_raw classmethod

from_raw(update: Any) -> Stopped | None

Wrap a bot being stopped off the wire.

Buttons

A keyboard is a shape with no call attached, which is why it is here, not among the methods. See Buttons.

sunnygram.types.buttons.Button

Every kind of button, one call each.

Nothing here talks to the network and nothing holds state, so a button can be built once and reused for as long as the program runs.

callback staticmethod

callback(text: str, data: str | bytes | None = None, *, password: bool = False) -> types.KeyboardButtonCallback

A button that calls the bot back with a payload.

The payload defaults to the label, which a program wants often enough to be worth not writing twice. It is capped at sixty four bytes by Telegram, so it is a key into what the program knows, not the thing itself, and something too long is refused here instead of on the wire.

password asks the person to confirm with their account password before the press goes through, which is Telegram's own safeguard for a button that does something irreversible.

url staticmethod

url(text: str, url: str) -> types.KeyboardButtonUrl

A button that opens a link. Nothing reaches the program.

login staticmethod

login(text: str, url: str, *, button_id: int = 0, forward_text: str = '') -> types.KeyboardButtonUrlAuth

A button that logs the person into a website as themselves.

The site is told who pressed it, once they have agreed to that, which is what makes this different from an ordinary link.

switch_inline staticmethod

switch_inline(text: str, query: str = '', *, same_chat: bool = False) -> types.KeyboardButtonSwitchInline

A button that starts an inline query somewhere.

Pressing it opens the chat picker with the bot's name and the query already typed. same_chat keeps it in the chat the button is in, which is the form a bot uses to hand someone its own results.

web_app staticmethod

web_app(text: str, url: str, *, simple: bool = False) -> Any

A button that opens a mini app.

The ordinary form belongs under a message and tells the app who opened it. simple is the other one, which sits above the text field and does not.

game staticmethod

game(text: str) -> types.KeyboardButtonGame

The button that starts the game a message carries.

pay staticmethod

pay(text: str) -> types.KeyboardButtonBuy

The button that pays for the invoice a message carries.

copy staticmethod

copy(text: str, copy_text: str) -> types.KeyboardButtonCopy

A button that puts something on the clipboard, and calls no one.

profile staticmethod

profile(text: str, user_id: int) -> types.KeyboardButtonUserProfile

A button that opens someone's profile.

text staticmethod

text(label: str) -> types.KeyboardButton

A plain suggestion above the text field, which sends its own label.

Nothing comes back but an ordinary message, so a handler for one of these is a text filter instead of a callback handler.

request_phone staticmethod

request_phone(text: str) -> types.KeyboardButtonRequestPhone

A suggestion that asks for the person's phone number.

They are asked to confirm, and what arrives is an ordinary message carrying a contact.

request_location staticmethod

request_location(text: str) -> types.KeyboardButtonRequestGeoLocation

A suggestion that asks where the person is, with the same confirmation.

request_poll staticmethod

request_poll(text: str, *, quiz: bool | None = None) -> types.KeyboardButtonRequestPoll

A suggestion that opens the poll composer.

Saying nothing about quiz lets them choose; saying True or False fixes it to one kind.

sunnygram.types.buttons.keyboard

keyboard(rows: Any, *, columns: int = 0, resize: bool = True, one_time: bool = False, persistent: bool = False, selective: bool = False, placeholder: str | None = None) -> base.ReplyMarkup

Build a keyboard out of buttons, in whichever kind they belong to.

The rows are a list of lists, or a flat list for a single row, or one button on its own. A string counts as a plain label, so a reply keyboard can be written as the words on it.

columns lays a flat list out for you, which is the case that otherwise turns three buttons into a nested list no one enjoys reading.

The remaining arguments only mean anything to a reply keyboard: resize shrinks it to the buttons rather than taking a third of the screen, and is on here because the other way round looks like a mistake in every client that draws it; one_time folds it away after a press; persistent keeps it up instead of the ordinary text field; selective shows it only to the people a message names or replies to; and placeholder is the grey text in the field behind it. An inline keyboard has nowhere to put any of them and ignores them.

sunnygram.types.buttons.force_reply

force_reply(*, placeholder: str | None = None, one_time: bool = True, selective: bool = False) -> types.ReplyKeyboardForceReply

Open the other side's keyboard with this message already being replied to.

The way to ask a question and be sure the answer comes back attached to it, which is worth more in a group than in a private chat: without it an answer is an ordinary message and matching it to the question is guesswork.

sunnygram.types.buttons.remove_keyboard

remove_keyboard(*, selective: bool = False) -> types.ReplyKeyboardHide

Take away the reply keyboard a previous message put up.

Only reply keyboards. An inline keyboard belongs to its message and is removed by editing that message's markup away.

sunnygram.types.buttons.buttons_of

buttons_of(message: Any) -> list[list[Any]]

The rows of buttons under a message, or nothing if it has none.

Only the inline kind. The other keyboard is a suggestion of things to type and pressing one sends its text, which arrives as an ordinary message instead of as anything to do with the message that put the keyboard up.

Rights

Both of these say what someone may do, in the readable direction: True is allowed. See Running a chat for why that is worth stating.

sunnygram.types.rights.AdminRights dataclass

What an administrator is allowed to do.

Everything is off by default, so promoting someone without saying what they may do gives them a title and no powers, which is the safe way round. Use the presets for the ordinary cases.

granted property

granted: tuple[str, ...]

The names of everything that is on, for showing someone.

everything classmethod

everything() -> AdminRights

Every power there is, short of anonymity.

Not the same as being the owner: transferring ownership is its own call and is deliberately not reachable from here.

moderator classmethod

moderator() -> AdminRights

The usual set for someone keeping order: remove people and posts.

with_

with_(**changes: bool) -> AdminRights

The same rights with a few changed, since these do not mutate.

to_raw

to_raw() -> types.ChatAdminRights

The TL form, which shares this convention and needs no flipping.

from_raw classmethod

from_raw(rights: Any) -> AdminRights

Read a set back off a chat or a participant.

sunnygram.types.rights.Permissions dataclass

What an ordinary member is allowed to do.

True is allowed. Restricting someone is passing the set with the things they may no longer do turned off, and the same shape sets the default for everybody in a chat.

Note what Telegram does with the group of them: turning off send_messages turns off everything that is a way of sending a message, whatever this says about the rest, because the server treats it as covering them. That is the server's rule and it is not undone here.

denied property

denied: tuple[str, ...]

The names of everything that is off, which is what a restriction is.

everything classmethod

everything() -> Permissions

No restriction at all, which is also what lifting one looks like.

read_only classmethod

read_only() -> Permissions

The usual mute: silenced but still present, able to read and no more.

none classmethod

none() -> Permissions

Nothing at all, not even seeing the chat, which is what a ban is.

with_

with_(**changes: bool) -> Permissions

The same permissions with a few changed, since these do not mutate.

to_raw

to_raw(*, until: int = 0) -> types.ChatBannedRights

The TL form, with every flag flipped to the banned convention.

until is when the restriction lifts, as a unix time. Zero is forever, which is Telegram's own spelling and not a missing value.

from_raw classmethod

from_raw(rights: Any) -> Permissions

Read a set back off a chat or a participant, flipping as it comes.

Updates

sunnygram.updates.manager.Event dataclass

One update, with the users and chats it talks about.

Updates name people and chats by id and leave the client to know the rest, so whatever came in the same container is carried along here. The peer cache has them too by the time this arrives, but it keeps only what is needed to reach someone; the full objects are here, for as long as the event is.

sunnygram.updates.manager.UpdateManager

The single source of truth for how far through the updates we are.

state property

state: UpdateState

Where the stream has been read up to.

events property

events: Queue[Event]

Updates in order, once each, for whoever wants to act on them.

dropped_events property

dropped_events: int

How many events were dropped because no one was draining them.

These are gone, and unlike every other loss in this layer they are not made up for later. By the time an event reaches the queue its counter has already been applied, so as far as the stream is concerned it was delivered, and no difference will ever mention it again. That is the deliberate half of rule P6: a program that stops reading loses the newest rather than stalling the session. A number above zero here means the program was handed something it never looked at, and the remedy is to drain faster or to build the manager with a bigger events_queue, because nothing this layer does afterwards can recover it.

The other kind of loss, further down, is recoverable and is counted by resyncs instead.

failures property

failures: int

How many times acting on an update did not go through.

Not fatal on its own: whatever failed leaves a gap, and the next update to notice it asks again. A number that keeps climbing is the signal.

resyncs property

resyncs: int

How many times the stream had to be rebuilt from a difference.

Counts the recoveries that were not the ordinary kind: a session the server started for itself, and updates the connection underneath threw away before they reached here. Both are survivable, because in both cases the counters had not moved yet and a difference can still fetch what went missing. Neither should be routine, so a number that keeps climbing says the program is either reconnecting or falling behind.

Not to be confused with dropped_events, which counts what was thrown away on the way out of this layer instead of on the way in, and which no difference can bring back.

idle_catch_up property

idle_catch_up: float

How long the stream may stay silent before we go and ask anyway.

Zero turns the watchdog off, which is the right answer only for a program that would rather miss the news than make a call it did not ask for.

start async

start(*, catch_up: bool = True) -> None

Learn where the stream is, then follow it.

catch_up decides what happens to whatever was missed while the program was not running. Fetching it is the right default for anything that acts on messages; skipping it suits a program that only cares about what happens from now on, and is much cheaper after a long absence.

stop async

stop() -> None

Stop following, and write down where we got to.

feed async

feed(container: TLObject) -> None

Take one thing the server sent and get every update out of it.

Public because a call that changes something answers with updates of its own, and those count exactly as much as the ones that arrive on their own. Sending a message and then being told about it twice is the bug this prevents.

catch_up async

catch_up() -> None

Ask for everything that happened while we were not listening.

sunnygram.recent.RecentMessages

A bounded, least-recently-used record of messages, by chat and id.

hits property

hits: int

Replies answered from here instead of from the network.

misses property

misses: int

Times the message asked for was not held.

remember

remember(message: Message) -> None

Hold onto a message, evicting the least recently used if full.

A message that does not say which chat it is in is skipped instead of stored under a guess: without one there is no key that a later lookup could use. The id is enough, so a message that arrived with no chat object alongside it is still held.

get

get(chat_id: int, message_id: int) -> Message | None

The message, if it is still held.

forget

forget(chat_id: int, message_id: int) -> None

Drop one, for a message that is known to be gone.

clear

clear() -> None

Let go of everything.

Text

sunnygram.parser.parse

parse(text: str, mode: str | None = 'markdown') -> tuple[str, list[Any]]

Read marked-up text, and answer with the text and its entities.

sunnygram.parser.unparse

unparse(text: str, entities: list[Any] | None, mode: str | None = 'markdown') -> str

Write text and its entities back out as marked-up text.