Skip to content

Client

The one object most programs hold. See Quickstart for what to do with it and Handling updates for the handler side.

sunnygram.client.Client

One Telegram account, with everything needed to use it.

invoker property

invoker: Invoker

The layer below, for anything this one does not wrap.

updates property

updates: UpdateManager

The update state machine, for its counters and its queue.

dispatcher property

dispatcher: Dispatcher

The handlers, in the order they will run.

me property

me: User | None

Whoever this client signed in as, once it has.

recent property

recent: RecentMessages

The messages lately seen or sent, which answers a reply.

invoke async

invoke(request: TLFunction[TLResult], *, dc_id: int | None = None, bulk: bool = False, timeout: float | None = None) -> TLResult

Call a TL function this client has no friendlier spelling for.

The whole schema is reachable this way, and the answer is typed as whatever the function says it is answered with, so reaching past the wrapped methods costs nothing in what a type checker can tell you.

Peers are the one thing to know: a raw call wants an InputPeer, which resolve gives back for a username, an id or "me". Everything else is the schema as Telegram documents it.

start async

start(*, phone_number: str | Callable[[], str] | None = None, code: Callable[[Any], Any] | None = None, password: Callable[[str], Any] | None = None, bot_token: str | None = None, catch_up: bool = True) -> User

Connect, sign in if this session has not, and start listening.

A session that has been used before needs none of the arguments: the key is in the file and this returns the account it belongs to.

stop async

stop() -> None

Stop listening and put everything down.

run

run(work: Awaitable[Any] | None = None, *, fast_loop: bool = True, **start: Any) -> Any

Start, run until interrupted or until work finishes, then stop.

The one-line way to turn a script into a program. Everything it does can be done by hand with start and stop when a program has its own loop to fit into.

This is the only place the library makes a loop, not joining one, so it is the only place that gets to pick which kind. If uvloop is installed it is used here, which is worth a multiple on everything that waits on a socket and needs no other change. Pass fast_loop=False to get asyncio's own loop instead, which is worth doing when something in the program depends on loop internals or when a bug needs ruling in or out. A program that runs its own loop is not touched either way; see sunnygram.loop for opting in from there.

on_message

on_message(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle new messages, on the ones the filter says yes to.

on_edited

on_edited(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle messages that were changed after they were sent.

on_scheduled

on_scheduled(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle messages queued for later instead of sent.

This fires when something is put in the schedule, not when it goes out. The moment it is actually sent it arrives again as an ordinary message, because by then it is one.

on_album

on_album(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle albums, once all of their parts have arrived.

The handler is given the list of messages, oldest first. Each part also reaches message handlers on its own, since that is what it is: an album is several messages sharing a group id, not one message carrying several files.

A filter here is asked about the first part, which is the one that carries the caption in every client that shows one.

on_callback_query

on_callback_query(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle inline buttons being pressed.

The handler is given a CallbackQuery, and its first duty is to answer it: Telegram holds the press open until something does, and every client draws that as a spinner on the button. Answering with nothing is fine and is what a bot does when the real reply is an edit.

Filters work here as they do on messages. A callback query's text is its payload, so filters.data and filters.regex both read what the button was built with.

on_inline_query

on_inline_query(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone typing this bot's name in any chat.

Bot sessions only, and the handler's first duty is to answer: Telegram holds the query open until something does, and the person is looking at a panel that never finishes loading in the meantime. An answer with no results is a complete answer.

filters.query asks what has been typed so far, and filters.regex works here too, since a query has text.

on_chosen_result

on_chosen_result(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle one of this bot's inline results being picked.

Bot sessions only, and only if the bot asked to be told: inline feedback is a setting in BotFather instead of a call. Telegram samples it for busy bots, so this counts what people pick instead of witnessing every pick.

on_chat_member

on_chat_member(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone's standing in a chat changing.

Joining, leaving, being promoted, being banned, being restricted: one event with the standing before and the standing after, and the difference between them is what happened. A bot is told this for chats it administers; a user account is told about the chats it is in.

on_join_request

on_join_request(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone asking to be let into a chat.

Bot sessions, for a chat the bot administers whose invite link puts people in a queue. Nothing happens until the request is answered, and approve and decline are both on the request itself.

on_deleted

on_deleted(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle messages being deleted.

Outside a channel Telegram does not say which chat they were in, only which ids are gone, so a program that has to know where has to have written it down when the message arrived.

on_reaction

on_reaction(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle reactions on a message changing.

Two readings of one event and which one arrives depends on the session: a user account is told the running totals, a bot is told what one named person changed. by_person says which of the two this is.

on_poll

on_poll(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle a poll's standing changing.

The question and the answers arrive when the poll itself changed and the results alone the rest of the time, which is most of the time.

on_poll_vote

on_poll_vote(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle one person voting in a public poll.

Only a public poll produces this, since an anonymous one is the promise not to say who voted for what.

on_shipping

on_shipping(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle Telegram asking what delivery costs for an address.

Only arrives for invoices sent with flexible on. Without that, no shipping query is ever sent however many handlers are waiting.

on_pre_checkout

on_pre_checkout(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle the last question before a customer is charged.

Answer within about ten seconds. Past that the payment fails on the customer's side and nothing is said on this one, so check what has to be checked, answer, and do the rest afterwards.

on_story

on_story(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle a story being posted, changed or taken down.

One update covers all three, so what arrives is the story as it now stands. A story that was deleted has nothing left but its id and does not reach here at all.

on_status

on_status(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone coming online or going offline.

User sessions only. A bot is never told this about anybody.

on_typing

on_typing(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone typing, or doing any of the other things shown.

Recording a voice note, uploading a video and picking a sticker are the same event with a different word, which is why one handler covers them.

on_blocked

on_blocked(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle this account blocking someone, or unblocking them.

User sessions only. The bot side of the same idea is on_stopped.

on_stopped

on_stopped(filters: Filter | None = None, *, group: int = 0) -> Callable[[Callback], Callback]

Handle someone stopping this bot, or starting it again.

Bot sessions only, and the number every bot should watch: it is the difference between an audience that is quiet and one that has left.

on_raw

on_raw(*, group: int = 0) -> Callable[[Callback], Callback]

Handle every update, as it came off the wire.

The escape hatch: anything the friendly layer does not wrap arrives here as the Event the update manager produced.

add_handler

add_handler(callback: Callback, *, kind: Kind = 'message', filters: Filter | None = None, group: int = 0) -> Handler

Register a handler without a decorator.

The kind is one of the words the decorators above stand for, and it is a fixed list rather than any string: a handler registered for a kind that does not exist is not an error anywhere, it is a handler that never runs, so the spelling is checked when the program is type checked instead of never.

load_plugins

load_plugins(where: str | PathLike[str] | Any, *, include: tuple[str, ...] | None = None, exclude: tuple[str, ...] = ()) -> int

Import a package of handlers and register them against this client.

app.load_plugins("plugins")

Every module in the package is imported and every function in it decorated with sunnygram.plugins is registered. Returns how many handlers that came to, which is worth checking: a package whose plugins were written without the decorators registers nothing, and a program that answers no one looks exactly like a program with nothing to answer.

A plugin that fails to import raises instead of being skipped, because a feature that is silently absent is the fault this library refuses.

resolve async

resolve(target: Target) -> Any

Name a peer to the server, however it was written.

Costs nothing for anybody this session has already met, one call for a username or a phone number it has not. What it will not do is invent a peer: an id alone does not reach a stranger on MTProto, so a peer this account has never encountered raises PeerNotFound rather than a call that fails later somewhere less obvious.

conversation async

conversation(peer: Target, *, timeout: float = DEFAULT_TIMEOUT, exclusive: bool = True) -> Conversation

A back and forth with one chat, for code that asks questions.

async with await app.conversation("@someone") as talk:
    await talk.send("What should I call you?")
    name = await talk.wait()

A message that answers a question does not also reach the ordinary handlers, which stops a command router from seeing someone's name as a command. exclusive=False if both should see it.

Awaited before the async with because the chat has to be resolved to the id updates will arrive with, and resolving is a call.

ask async

ask(peer: Target, text: str, *, filters: Filter | None = None, timeout: float = DEFAULT_TIMEOUT, exclusive: bool = True, **options: Any) -> Message

Send a question and wait for the answer. One call.

Raises NoAnswer if nothing arrives in time. The listening is in place before the question goes out, so a fast answer cannot arrive while the send is still in flight and end up somewhere else.

wait_for async

wait_for(peer: Target, *, kind: Kind = 'message', filters: Filter | None = None, timeout: float = DEFAULT_TIMEOUT, exclusive: bool = True) -> Any

Wait for the next message from a chat without asking anything first.

For the half of a conversation that starts with them instead of with us: a confirmation, a file someone was told to send, a button press.

forget_peer async

forget_peer(target: Target) -> bool

Drop what is remembered about a peer, and say whether there was any.

Rarely needed by hand, because a call the server refuses on the grounds of the peer already drops it. This is here for the case that refusal cannot see: a hash that is wrong in a way the server answers by pretending the peer does not exist, where the call looks successful and the answer is empty.

refresh_peer async

refresh_peer(username: str) -> Any

Ask the server about a username again, and keep the new answer.

The repair for a peer whose access hash has stopped working. resolve answers from the cache, which is the whole point of it and exactly what is unhelpful when the cached answer is the problem, so this is the one that always goes to the network.

get_me async

get_me() -> User

Who this client is signed in as, asked freshly.

log_out async

log_out() -> None

End the session on Telegram's side and forget the key.

send_message async

send_message(peer: Target, text: str, *, parse_mode: str | None = '', entities: list[Any] | None = None, reply_to: int | None = None, topic: int | None = None, silent: bool = False, no_webpage: bool = False, reply_markup: Any = None, schedule_date: datetime | int | None = None) -> Message

Send a message, and answer with the one the server made of it.

parse_mode defaults to the client's, and passing None sends the text exactly as it is. Passing entities skips parsing altogether, which is what forwarding someone else's formatting looks like.

reply_markup is a keyboard to put under it, built by types.keyboard out of types.Button. Only a bot may send one, which is Telegram's rule rather than this one.

schedule_date queues the message for later instead of sending it now, as a datetime or a unix timestamp, or sunnygram.WHEN_ONLINE to send it the moment the recipient next appears. What comes back is the queued message, which lives in get_scheduled until its time.

edit_message async

edit_message(peer: Target, message_id: int, text: str, *, parse_mode: str | None = '', entities: list[Any] | None = None, no_webpage: bool = False, reply_markup: Any = None) -> Message

Rewrite a message that is ours to rewrite.

The keyboard is replaced along with the text when one is given, and left alone when none is. Taking one away is edit_markup with nothing, since there is no way to tell "no keyboard" from "do not touch the keyboard" in one argument.

edit_media async

edit_media(peer: Target, message_id: int, media: Any, *, caption: str | None = None, parse_mode: str | None = '', entities: list[Any] | None = None, reply_markup: Any = None, **upload: Any) -> Message

Replace the file on a message that already carries one.

The media is anything send_file or send_media would take: a path, the bytes, a file Telegram already holds, or a portable reference. A path is uploaded first, which is the only reason this is longer than the call it makes.

Telegram will not put a file on a message that has none, and will not take one off, so this edits a photo into a different photo instead of turning a text message into one. Passing no caption leaves the existing one alone.

edit_markup async

edit_markup(peer: Target, message_id: int, markup: Any = None) -> Message

Change the buttons under a message and leave its text alone.

Passing nothing takes the keyboard away, which a bot does with a menu once it has been used and is the reason this is a call of its own instead of an argument to edit_message.

Taking them away is the field left unset, not an inline keyboard with no rows in it. The empty keyboard reads like the obvious spelling and the server refuses it outright, which is a REPLY_MARKUP_INVALID on every close button a menu has.

edit_inline_message async

edit_inline_message(inline_id: Any, text: str, **options: Any) -> bool

Rewrite a message an inline query produced.

These have no chat behind them, so they are named by the opaque id that comes off a callback query rather than by a peer and a message id, and the answer is whether the edit went through instead of the message.

edit_inline_markup async

edit_inline_markup(inline_id: Any, markup: Any = None) -> bool

Change the buttons on an inline message, or take them away.

Taking them away is the field left unset, the same as for a message in a chat: an inline keyboard with no rows is refused rather than read as no keyboard.

answer_inline_query async

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

Answer an inline query with what this bot is offering.

InlineQuery.answer is the same call reached from the query itself and is the one a handler normally wants. This is here for a program holding the id.

The results are InlineResult, built by its factories, or the raw constructors for anything they do not cover. The message each one sends is styled here instead of when the result was built, because the parse mode belongs to the client and a result is usually built before there is one in hand.

answer_callback_query async

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

Answer a button press, which stops the button spinning.

CallbackQuery.answer is the same call reached from the press itself, and is the one a handler normally wants. This is here for a program that has kept the id and is answering later.

delete_messages async

delete_messages(peer: Target, ids: list[int], *, everywhere: bool = True) -> int

Delete messages, and say how many the server owned up to.

everywhere takes them back for the other side too, which is allowed for a while after sending and always in a chat we administer.

forward_messages async

forward_messages(target: Target, source: Target, ids: list[int], *, silent: bool = False) -> None

Send messages from one chat on to another.

get_history async

get_history(peer: Target, *, limit: int = 100, offset_id: int = 0, batch: int = 100) -> AsyncIterator[Message]

Read a chat backwards, oldest call last, paging handled here.

Telegram answers history a page at a time and expects the client to keep asking with the id it got to. That bookkeeping is the whole reason this exists: a caller says how many they want and reads them.

get_messages async

get_messages(peer: Target, ids: list[int]) -> list[Message]

Fetch particular messages by id.

A message that is not there, because it was deleted or never existed, is left out instead of coming back as a hole, so the answer may be shorter than what was asked for.

send_invoice async

send_invoice(peer: Target, invoice: Any, **options: Any) -> Message

Send an invoice, built by methods.as_invoice or as_stars_invoice.

An invoice is a kind of media instead of a call of its own, so this is send_media with a clearer name and the same options.

answer_shipping async

answer_shipping(query_id: int, *, options: list[Any] | None = None, error: str | None = None) -> bool

Answer a shipping query with options, or a reason there are none.

answer_pre_checkout async

answer_pre_checkout(query_id: int, *, ok: bool = True, error: str | None = None) -> bool

Approve or reject a payment. About ten seconds to do it in.

get_stars_balance async

get_stars_balance(peer: Target = 'me') -> int

How many Stars this account, or a channel it runs, holds.

get_stars_transactions async

get_stars_transactions(peer: Target = 'me', **options: Any) -> Any

The Stars ledger, newest first.

refund_stars async

refund_stars(user: Target, charge_id: str) -> Any

Give back a Stars payment, by the charge id it arrived with.

get_stars_topup_options async

get_stars_topup_options() -> Any

The bundles of Stars this account can buy, and what each costs.

get_stars_gift_options async

get_stars_gift_options(user: Target | None = None) -> Any

The bundles that can be bought for somebody else.

get_stars_giveaway_options async

get_stars_giveaway_options() -> Any

The bundles that can be put up as a giveaway prize.

get_stars_subscriptions async

get_stars_subscriptions(peer: Target = 'me', **options: Any) -> Any

The recurring Stars charges a peer is signed up to.

cancel_stars_subscription async

cancel_stars_subscription(subscription_id: str, peer: Target = 'me') -> bool

Stop paying for a subscription, from the subscriber's end.

resume_stars_subscription async

resume_stars_subscription(subscription_id: str, peer: Target = 'me') -> bool

Undo a cancellation, while the period already paid for is running.

fulfill_stars_subscription async

fulfill_stars_subscription(subscription_id: str, peer: Target = 'me') -> bool

Pay a charge that was missed, once the balance can cover it.

cancel_bot_subscription async

cancel_bot_subscription(user: Target, charge_id: str, **options: Any) -> bool

Cancel a subscription this bot is paid for, or put it back.

get_stars_revenue_stats async

get_stars_revenue_stats(peer: Target = 'me', **options: Any) -> Any

Earnings for a peer, as figures and as graphs to load separately.

get_stars_withdrawal_url async

get_stars_withdrawal_url(password: str, peer: Target = 'me', **options: Any) -> str

A one-time link for taking earnings out, which needs the password.

get_stars_ads_url async

get_stars_ads_url(peer: Target = 'me') -> str

A link into the ad platform, for spending earnings rather than taking them.

get_stars_transactions_by_id async

get_stars_transactions_by_id(ids: list[str], peer: Target = 'me', **options: Any) -> Any

Look up particular ledger entries instead of paging the whole ledger.

get_referral_bots async

get_referral_bots(peer: Target = 'me', **options: Any) -> Any

The affiliate programs this peer has joined, newest first.

get_referral_bot async

get_referral_bot(bot: Target, peer: Target = 'me') -> Any

One affiliate program, if this peer has joined it.

get_suggested_referral_bots async

get_suggested_referral_bots(peer: Target = 'me', **options: Any) -> Any

Affiliate programs on offer that this peer has not joined.

connect_referral_bot async

connect_referral_bot(bot: Target, peer: Target = 'me') -> Any

Join an affiliate program, which mints the link that earns commission.

revoke_referral_link(link: str, peer: Target = 'me') -> Any

Give up an affiliate link, which stops it earning and cannot be undone.

accept_gift_offer async

accept_gift_offer(message_id: int) -> Any

Take an offer for one of your gifts, which hands it over and pays you.

add_to_gift_collection async

add_to_gift_collection(collection_id: int, gifts: Any, peer: Target = 'me') -> Any

Put gifts on a shelf they are not already on.

bid_on_gift_auction async

bid_on_gift_auction(gift_id: int, amount: int, **options: Any) -> Any

Bid in an auction. This spends Stars, and a bid cannot be taken back.

buy_gift_transfer async

buy_gift_transfer(gift: Any, to: Target, peer: Target | None = None) -> Any

Pay the transfer fee and give an upgraded gift away. This spends Stars.

buy_gift_upgrade async

buy_gift_upgrade(gift: Any, peer: Target | None = None, **options: Any) -> Any

Pay to upgrade a gift whose upgrade was not included. This spends Stars.

buy_resale_gift async

buy_resale_gift(slug: str, to: Target = 'me', **options: Any) -> Any

Buy an upgraded gift somebody has listed. This spends Stars, or TON.

can_send_gift async

can_send_gift(gift_id: int) -> Any

Whether this account may send a particular gift, and why not if it may not.

convert_gift async

convert_gift(gift: Any, peer: Target | None = None) -> bool

Turn a gift back into Stars, which destroys it.

craft_gift async

craft_gift(gifts: Any, peer: Target | None = None) -> Any

Consume several gifts to make one. The ones put in are gone.

get_craftable_gifts async

get_craftable_gifts(gift_id: int, **options: Any) -> Any

Which of the gifts held could go into crafting one of this kind.

create_gift_collection async

create_gift_collection(title: str, gifts: Any, peer: Target = 'me') -> Any

Make a new shelf with something already on it.

decline_gift_offer async

decline_gift_offer(message_id: int) -> Any

Turn an offer down. The offer ends; the gift stays where it is.

delete_gift_collection async

delete_gift_collection(collection_id: int, peer: Target = 'me') -> bool

Get rid of a shelf. What was on it is still owned.

get_gift_auction_gifts async

get_gift_auction_gifts(gift_id: int) -> Any

What this account has already won in auctions of one kind of gift.

get_gift_auction_state async

get_gift_auction_state(auction: int | str, **options: Any) -> Any

Where an auction has got to, named by gift id or by slug.

get_gift_auctions async

get_gift_auctions() -> Any

Auctions running now.

get_gift_catalogue async

get_gift_catalogue() -> Any

Every gift on sale, which is the shop rather than anybody's shelf.

get_gift_collections async

get_gift_collections(peer: Target = 'me') -> Any

The shelves a peer has sorted their gifts onto.

get_gift_upgrade_attributes async

get_gift_upgrade_attributes(gift_id: int) -> Any

The full attribute pool for a kind of gift, with how rare each one is.

get_gift_upgrade_preview async

get_gift_upgrade_preview(gift_id: int) -> Any

What a gift of this kind could turn into, before one is owned.

get_gift_withdrawal_url async

get_gift_withdrawal_url(gift: Any, password: str, peer: Target | None = None) -> str

A one-time link for taking an upgraded gift out to the blockchain.

hide_gift async

hide_gift(gift: Any, peer: Target | None = None) -> bool

Take a gift off the public shelf. It is still owned, just not displayed.

pin_gifts async

pin_gifts(gifts: Any, peer: Target = 'me') -> bool

Set which gifts sit at the top of the shelf, in the order given.

remove_from_gift_collection async

remove_from_gift_collection(collection_id: int, gifts: Any, peer: Target = 'me') -> Any

Take gifts off a shelf. They are still owned, just not filed there.

rename_gift_collection async

rename_gift_collection(collection_id: int, title: str, peer: Target = 'me') -> Any

Change what a collection is called, and nothing else about it.

reorder_gift_collection async

reorder_gift_collection(collection_id: int, gifts: Any, peer: Target = 'me') -> Any

Set the order gifts sit in on one shelf, which is the whole order.

reorder_gift_collections async

reorder_gift_collections(order: list[int], peer: Target = 'me') -> bool

Set the order the shelves themselves sit in.

get_resale_gifts async

get_resale_gifts(gift_id: int, **options: Any) -> Any

Upgraded gifts of one kind that their owners have put up for sale.

get_saved_gift async

get_saved_gift(gifts: Any, peer: Target | None = None) -> Any

Particular gifts by their handles, rather than paging somebody's shelf.

get_saved_gifts async

get_saved_gifts(peer: Target = 'me', **options: Any) -> Any

The gifts a peer holds, newest first unless sorted by value.

send_gift async

send_gift(peer: Target, gift_id: int, **options: Any) -> Any

Buy a gift and give it to somebody. This spends Stars.

send_gift_offer async

send_gift_offer(peer: Target, slug: str, amount: int, **options: Any) -> Any

Offer to buy somebody's upgraded gift off them at a price.

set_gift_notifications async

set_gift_notifications(peer: Target, enabled: bool = True) -> bool

Whether to be told when a channel this account runs is sent a gift.

set_gift_resale_price async

set_gift_resale_price(gift: Any, amount: int, peer: Target | None = None) -> Any

Put an upgraded gift up for sale, or change what it is listed at.

show_gift async

show_gift(gift: Any, peer: Target | None = None) -> bool

Put a gift on the public shelf, where anyone looking at the profile sees it.

transfer_gift async

transfer_gift(gift: Any, to: Target, peer: Target | None = None) -> Any

Give an upgraded gift to somebody else, when the transfer is free.

get_unique_gift async

get_unique_gift(slug: str) -> Any

One upgraded gift by its public name, which anybody can look up.

get_unique_gift_value async

get_unique_gift_value(slug: str) -> Any

What an upgraded gift is reckoned to be worth, and what it last sold for.

upgrade_gift async

upgrade_gift(gift: Any, peer: Target | None = None, **options: Any) -> Any

Upgrade a gift whose upgrade was already paid for. This spends nothing.

send_story async

send_story(peer: Target, file: Any, *, caption: str = '', parse_mode: str | None = '', entities: list[Any] | None = None, privacy: str | list[Any] = 'everyone', pinned: bool = False, noforwards: bool = False, period: int | None = None, progress: Any = None, **upload: Any) -> list[Story]

Post a story, and answer with the stories the server made of it.

The file is a path, the bytes, or anything with a read method, the same as send_file takes, and is uploaded first.

privacy is who may see it: everyone, contacts, close_friends or no one. It has a default because the wire does not: an empty rule list means no one, so a story posted without saying is a story no one sees.

period is how long it stays up, one of 6, 12, 24 or 48 hours in seconds. pinned keeps it on the profile once it expires.

get_stories async

get_stories(peer: Target, ids: list[int] | None = None) -> list[Story]

Stories an account has up, or particular ones by id.

get_pinned_stories async

get_pinned_stories(peer: Target, *, limit: int = 100) -> list[Story]

The stories an account keeps on its profile after they expire.

edit_story async

edit_story(peer: Target, story_id: int, **changes: Any) -> None

Change a story that is already up: its caption, media or privacy.

delete_stories async

delete_stories(peer: Target, ids: list[int]) -> list[int]

Take stories down, and say which ones went.

pin_stories async

pin_stories(peer: Target, ids: list[int], *, pinned: bool = True) -> list[int]

Keep stories on the profile after they expire, or stop.

read_stories async

read_stories(peer: Target, max_id: int) -> list[int]

Mark everything up to a story as seen.

get_folders async

get_folders() -> list[Folder]

The folders this account has sorted its chats into.

The unfiltered view Telegram lists alongside them is not a folder and is left out, so what comes back is the folders someone actually made.

save_folder async

save_folder(folder_id: int, title: str, **rules: Any) -> bool

Create or replace a folder.

There is no separate call for creating one: saving under an id nothing is using makes it, and saving over a used id replaces it, which is Telegram's design rather than this one. Pick an id get_folders is not already showing.

The rules are which chats to include, exclude and pin, and the categories to sweep in: contacts, non_contacts, groups, broadcasts, bots, and the exclude_muted, exclude_read and exclude_archived switches.

delete_folder async

delete_folder(folder_id: int) -> bool

Remove a folder. The chats it showed are not affected.

reorder_folders async

reorder_folders(order: list[int]) -> bool

Set the order folders appear in, by id.

takeout async

takeout(**what: Any) -> methods.Takeout

Open a data-export session, which reads without the usual limits.

Say which parts the export may reach: contacts, message_users, message_chats, message_megagroups, message_channels, files, and file_max_size to cap how large a file it will take.

Use it as a context manager, which closes the session afterwards and tells Telegram whether the export finished or gave up:

async with await app.takeout(message_users=True) as export:
    history = await export.invoke(functions.messages.GetHistory(...))

Raises TakeoutInitDelay if the account holder has not approved the export yet. That is a person being asked to tap something in an official client, so its seconds are hours and it is not waited out for you the way an ordinary flood wait is.

get_scheduled async

get_scheduled(peer: Target, ids: list[int] | None = None) -> list[Message]

Messages queued for a chat and not sent yet.

The whole queue, or the ones named. These carry their own ids, which are not the ids the messages will have once they go out, so they are only good for the other scheduled calls.

send_scheduled async

send_scheduled(peer: Target, ids: list[int]) -> None

Send queued messages now instead of waiting for their time.

delete_scheduled async

delete_scheduled(peer: Target, ids: list[int]) -> None

Drop queued messages so they never go out.

search_messages async

search_messages(peer: Target, query: str = '', *, limit: int = 100, batch: int = 100, from_user: Target | None = None, filter: MessagesFilter | None = None) -> AsyncIterator[Message]

Search one chat, paging handled here.

An empty query with a filter is how to ask for every photo, or every link, without searching for anything in particular.

read_history async

read_history(peer: Target, *, max_id: int = 0) -> None

Mark a chat as read, up to a message or all of it.

pin_message async

pin_message(peer: Target, message_id: int, *, silent: bool = True, both_sides: bool = False) -> None

Pin a message, quietly unless told otherwise.

unpin_message async

unpin_message(peer: Target, message_id: int) -> None

Unpin one message.

unpin_all_messages async

unpin_all_messages(peer: Target) -> None

Unpin everything pinned in a chat.

send_action async

send_action(peer: Target, action: SendMessageAction | None = None) -> None

Show that something is being done. Typing, unless said otherwise.

Telegram forgets one of these after about six seconds, so anything slower than that has to say it again.

send_file async

send_file(peer: Target, file: Any, *, caption: str = '', parse_mode: str | None = '', entities: list[Any] | None = None, kind: FileKind = 'auto', name: str | None = None, mime_type: str | None = None, thumb: Any = None, duration: float = 0, width: int = 0, height: int = 0, title: str | None = None, performer: str | None = None, streaming: bool = True, spoiler: bool = False, ttl_seconds: int | None = None, reply_to: int | None = None, topic: int | None = None, silent: bool = False, reply_markup: Any = None, schedule_date: datetime | int | None = None, progress: Any = None, **upload: Any) -> Message

Send a file, and answer with the message that carries it.

The file is a path, the bytes themselves, or anything with a read method. What it arrives as is worked out from the name unless kind says otherwise, and getting that wrong is the difference between a video that plays in place and one that has to be downloaded first.

The caption is the message text, since Telegram has no separate field for one, and is parsed the same way a message is.

send_photo async

send_photo(peer: Target, photo: Any, **options: Any) -> Message

Send an image as a photo, which Telegram re-encodes and shows inline.

send_document async

send_document(peer: Target, document: Any, **options: Any) -> Message

Send anything as a file, kept exactly as it is.

send_video async

send_video(peer: Target, video: Any, **options: Any) -> Message

Send a video, playable in place.

duration, width and height are worth passing when they are known. Telegram works them out on its own eventually, and until it has, the video shows up as a file.

send_media async

send_media(peer: Target, media: Any, *, spoiler: bool = False, **options: Any) -> Message

Send a file Telegram already holds, without uploading it again.

This is the cheap half of sending. A file that has been sent once can go anywhere else by being named, so a program that has kept what it sent, or is passing along what it received, pays for a single call instead of a download and an upload.

The media is a message, the media off one, a Document or a Photo, either of their input forms, or the portable reference string that file_ref writes one down as. Anything else says so, since the alternative would be silently sending nothing.

The file reference inside goes stale after an hour or so. Where what was passed says which message the file came from, which a message and a portable reference both do, that is renewed here and the send is tried once more. Where it does not, a stale reference comes back as an error and the answer is to fetch the message again.

spoiler hides it behind a tap. That belongs to this send instead of to the file, so a photo kept in a cache goes out plain to one asker and covered to the next without being stored twice.

send_animation async

send_animation(peer: Target, animation: Any, **options: Any) -> Message

Send a gif, which Telegram stores as a soundless looping video.

A real .gif goes as one too: it is converted on arrival, and sending it as a document instead is what keeps the original bytes.

send_audio async

send_audio(peer: Target, audio: Any, **options: Any) -> Message

Send a music track, with title and performer if you have them.

send_voice async

send_voice(peer: Target, voice: Any, **options: Any) -> Message

Send a voice note, the round kind that plays where it sits.

send_sticker async

send_sticker(peer: Target, sticker: Any, **options: Any) -> Message

Send a sticker that already exists, by pointing at its document.

A sticker is a document, so this takes one off a message, an InputDocument, or anything else as_document understands. Uploading new sticker bytes is send_file's job and makes a file, not a sticker.

send_album async

send_album(peer: Target, files: list[Any], *, captions: list[str] | None = None, options: list[dict[str, Any]] | None = None, parse_mode: str | None = '', reply_to: int | None = None, topic: int | None = None, silent: bool = False, progress: Any = None, **upload: Any) -> list[Message]

Send several files as one album, and answer with their messages.

Each file is whatever send_file takes, and each is worked out the same way. Photos and videos group together; documents group together; the two cannot be mixed, and trying says so instead of failing on the wire.

captions runs alongside files. Most clients show only the first under the whole block, so that is usually the only one worth setting.

options runs alongside them too, and is what send_file takes for one file: kind, thumb, duration, width, height and the rest. A video in an album needs its duration and size as much as one sent on its own, and without somewhere to say them every video in an album arrives as a file until Telegram has worked them out for itself.

A file Telegram already holds may be passed instead of a path, and is pointed at instead of uploaded again. The two can be mixed freely. Such an entry takes spoiler out of its options like any other; the rest of them describe an upload and mean nothing to a file already there.

One thing an album does not do that a single send does: a stale file reference is not renewed here. A multi-media send names every file in one call, so there is no one file to retry, and an album of pointers old enough to have gone stale has to be rebuilt by whoever kept them.

send_poll async

send_poll(peer: Target, question: str, answers: list[str], **options: Any) -> Message

Send a poll, or a quiz if one of the answers is named as correct.

Answers are referred to by position everywhere in this library, so correct=0 is the first of them and voting takes the same numbers.

vote async

vote(peer: Target, message_id: int, *options: int) -> Any

Answer a poll by the positions of the answers, or none to retract.

close_poll async

close_poll(peer: Target, message_id: int) -> None

Stop a poll taking votes, which cannot be undone.

get_poll async

get_poll(peer: Target, message_id: int) -> Any

A poll's standing right now, without waiting for an update.

send_dice async

send_dice(peer: Target, kind: str = 'dice', **options: Any) -> Message

Roll something. Telegram decides the number, not the sender.

The names it knows are in methods.DICE, and the emoji itself works for anything added since.

send_location async

send_location(peer: Target, latitude: float, longitude: float, **options: Any) -> Message

Send a point on the map.

send_venue async

send_venue(peer: Target, latitude: float, longitude: float, title: str, address: str, **options: Any) -> Message

Send a named place, which is a location with a title on it.

send_contact async

send_contact(peer: Target, phone: str, first_name: str, **options: Any) -> Message

Send someone's card. This does not add them as a contact.

copy_message async

copy_message(peer: Target, source: Message, **options: Any) -> Message

Send a message again as a new one, with no sign of where it came from.

Not a forward: nothing on the copy says who wrote it first. What can be copied is text and media that already exists, which leaves out polls, since a poll is votes, not content.

react async

react(peer: Target, message_id: int, reaction: Any = None, *, big: bool = False) -> None

Set this account's reactions on a message, or clear them.

The call replaces instead of adds, which is Telegram's design: passing nothing takes every reaction back, and passing two sets both.

get_reactions async

get_reactions(peer: Target, message_id: int, **options: Any) -> list[Any]

Who reacted to a message, and with what.

Only chats small enough for Telegram to keep the list answer with names; a large channel gives counts and nothing else.

click async

click(message: Message, which: Any = 0, *, password: str = '') -> Any

Press a button under a message, and hand back the bot's answer.

which is how anybody refers to a button: its label, or its number in reading order, or a (row, position) pair. The answer is usually a short notice; a bot that replies by editing the message instead says nothing here and the edit arrives as an update.

buttons_of

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

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

inline_query async

inline_query(bot: Target, query: str = '', **options: Any) -> Any

Ask a bot for inline results, the way typing @bot query does.

send_inline_result async

send_inline_result(peer: Target, query_id: int, result_id: str, **options: Any) -> Any

Send one of the results an inline query answered with.

start_bot async

start_bot(bot: Target, **options: Any) -> Any

Press start on a bot, with the parameter a deep link would carry.

set_bot_commands async

set_bot_commands(commands: list[tuple[str, str]], **options: Any) -> Any

Publish the slash-command menu clients offer as autocomplete.

Each command is a name and a description. A leading slash is allowed and stripped, since that is how people write them down.

Signed in with a bot token only: this is the bot saying something about itself, and a user account has nothing to say here.

get_bot_commands async

get_bot_commands(**options: Any) -> list[tuple[str, str]]

The published command menu, in the shape set_bot_commands takes.

delete_bot_commands async

delete_bot_commands(**options: Any) -> Any

Take the published command menu away.

get_sessions async

get_sessions() -> list[Any]

Every place this account is signed in, this one included.

terminate_session async

terminate_session(hash: int) -> bool

Sign one other session out, by the hash the listing gave it.

terminate_other_sessions async

terminate_other_sessions() -> bool

Sign out everywhere but here.

set_password async

set_password(new: str, **options: Any) -> Any

Set or change the account's second factor.

Neither password leaves this machine. Set a recovery email while doing it: Telegram has no other way back into an account whose second factor is forgotten.

remove_password async

remove_password(current: str) -> Any

Take the second factor off, which needs the current one.

has_password async

has_password() -> bool

Whether this account has a second factor at all.

get_privacy async

get_privacy(setting: str) -> Any

What one privacy setting currently says.

The settings by name are in methods.PRIVACY.

set_privacy async

set_privacy(setting: str, allow: str = 'contacts', **options: Any) -> Any

Change one privacy setting, and optionally carve people out of it.

set_username async

set_username(username: str) -> Any

Claim a username, or give up the current one by passing nothing.

check_username async

check_username(username: str) -> bool

Whether a username can be claimed, without claiming it.

get_dialogs async

get_dialogs(*, limit: int = 100, batch: int = 100) -> AsyncIterator[Dialog]

Every conversation this account has, newest first.

get_participants async

get_participants(peer: Target, *, limit: int = 200, query: str = '', kind: MemberFilter = 'recent') -> AsyncIterator[User]

The people in a group or channel.

kind asks for one sort of member rather than all of them: "admins", "bots", "banned" for the ones thrown out, "restricted" for the ones still present but silenced, or "contacts". It is a fixed set of words, so a misspelling is a type error rather than a filter that quietly matches nobody.

A channel with many members only lets a client see so far down the list, which is Telegram's rule instead of this one: past a few thousand the answer stops whether or not more were asked for.

get_members async

get_members(peer: Target, *, limit: int = 200, query: str = '', kind: MemberFilter = 'recent') -> AsyncIterator[Member]

The same people as get_participants, with their standing attached.

get_participants answers who is in a chat. This answers what each of them is in it: the status, the rights an administrator was given, the custom title, who promoted them. Finding whoever made a chat is the short version of why it exists:

async for member in app.get_members(chat, kind="admins"):
    if member.status is MemberStatus.CREATOR:
        ...

The two are separate rather than one call handing back a pair because a User is frozen, and a standing belongs to one chat rather than to the person.

get_chat_stats async

get_chat_stats(peer: Target, *, dark: bool = False) -> Any

What Telegram counts about a channel or a supergroup.

The two kinds are counted differently and answered by different calls, and which one a chat needs is worked out here. Statistics start only past a size Telegram picks and does not publish; below it the server refuses, which is its rule and not this one.

The graphs in the answer are tokens rather than data. load_graph turns one into the other.

get_message_stats async

get_message_stats(peer: Target, message_id: int, *, dark: bool = False) -> Any

Views and reactions for one post, as two graphs.

get_story_stats async

get_story_stats(peer: Target, story_id: int, *, dark: bool = False) -> Any

Views and reactions for one story, as two graphs.

load_graph async

load_graph(token: str, *, x: int = 0) -> Any

Fetch a graph the statistics only handed back a token for.

get_public_forwards async

get_public_forwards(peer: Target, message_id: int, *, limit: int = 100) -> AsyncIterator[Message | Story]

Publicly, who reposted this.

A repost is a message in another public chat or a story, and Telegram answers with both mixed together, so this yields whichever each one is rather than dropping the kind it was not asked about.

get_boosts_status async

get_boosts_status(peer: Target) -> BoostStatus

What level a chat is at, and how many more boosts the next takes.

The count that matters is needed, which is not the difference between two of the numbers Telegram sends: the next level's figure is measured from zero, so reading it the obvious way is off by the boosts already spent.

get_my_boosts async

get_my_boosts() -> Any

This account's boost slots, and what each is lent to.

boost async

boost(peer: Target, *, slots: Sequence[int] | None = None) -> Any

Lend a chat one of this account's boost slots.

Naming no slots lets the server pick, which is what pressing the button in an official client does.

get_user_boosts async

get_user_boosts(peer: Target, user: Target) -> list[Boost]

Which of one person's slots are lent to this chat.

get_boosts async

get_boosts(peer: Target, *, limit: int = 100, gifts: bool = False) -> AsyncIterator[Boost]

Who is boosting a chat.

gifts narrows it to the ones that came from a giveaway or a gift rather than from somebody spending a slot of their own.

export_folder_link(folder_id: int, *, title: str = '', peers: Sequence[Target]) -> Any

Give a folder a link, naming which of its chats the link carries.

Not every chat can be shared, so the ones in the link are named rather than taken from the folder, and asking for one the server will not share is refused instead of quietly dropped.

get_folder_links(folder_id: int) -> Any

Every link this folder has been given.

edit_folder_link(folder_id: int, slug: str, *, title: str | None = None, peers: Sequence[Target] | None = None) -> Any

Change a link's title, or which chats it carries.

What is left out is left alone.

delete_folder_link(folder_id: int, slug: str) -> Any

Take a link back. Whoever already joined stays where they are.

preview_folder_link(slug: str) -> Any

What is behind somebody else's folder link, without joining it.

join_folder_link(slug: str, *, peers: Sequence[Target]) -> Any

Join a shared folder, taking the chats named and no others.

get_folder_updates async

get_folder_updates(folder_id: int) -> Any

Chats a shared folder has gained since this account joined it.

join_folder_updates async

join_folder_updates(folder_id: int, *, peers: Sequence[Target]) -> Any

Take the chats a shared folder has gained, or the ones named.

hide_folder_updates async

hide_folder_updates(folder_id: int) -> Any

Decline what a shared folder has gained, without leaving it.

get_leave_suggestions async

get_leave_suggestions(folder_id: int) -> Any

Which chats leaving this folder could reasonably take with it.

leave_folder async

leave_folder(folder_id: int, *, peers: Sequence[Target] = ()) -> Any

Leave a shared folder, and the chats named with it.

Naming none leaves the folder and stays in every chat, which is the safe default: leaving chats is the half that cannot be undone quietly.

create_sticker_set async

create_sticker_set(owner: Target, *, title: str, short_name: str, stickers: Sequence[Any], kind: StickerKind = 'regular', thumb: Any = None, software: str | None = None) -> Any

Make a set, owned by somebody, with its first stickers in it.

A set cannot be created empty. Build each entry with sticker_item, which pairs an uploaded file with the emoji it is found by.

upload_sticker async

upload_sticker(source: Any, emoji: str, *, keywords: Sequence[str] = (), mask_coords: Any = None, mime_type: str = 'image/webp', **upload: Any) -> Any

Turn a file off disk into a sticker ready to go in a set.

An upload is not a document yet, and a set is built out of documents, so this does the registering step in between. Pass "video/webm" for an animated sticker or "application/x-tgsticker" for a Lottie.

add_sticker async

add_sticker(short_name: str, sticker: Any) -> Any

Put one more sticker at the end of a set.

remove_sticker async

remove_sticker(sticker: Any) -> Any

Take a sticker out of whichever set it is in.

Named by the document, which already says which set that is.

move_sticker async

move_sticker(sticker: Any, position: int) -> Any

Move a sticker to a place in its set, counting from zero.

edit_sticker async

edit_sticker(sticker: Any, *, emoji: str | None = None, keywords: Sequence[str] | None = None, mask_coords: Any = None) -> Any

Change what a sticker already in a set is found by.

replace_sticker async

replace_sticker(sticker: Any, replacement: Any) -> Any

Swap one sticker for another, keeping its place in the set.

rename_sticker_set async

rename_sticker_set(short_name: str, title: str) -> Any

Change a set's title. The short name, and so the link, stays.

delete_sticker_set async

delete_sticker_set(short_name: str) -> Any

Delete a whole set. Not undoable, and the short name is not freed.

set_sticker_set_thumb async

set_sticker_set_thumb(short_name: str, *, thumb: Any = None, document_id: int | None = None) -> Any

Choose the picture a set is shown by.

suggest_short_name async

suggest_short_name(title: str) -> Any

Ask the server for a free short name that suits this title.

short_name_free async

short_name_free(short_name: str) -> bool

Whether a sticker set short name can still be taken.

get_topics async

get_topics(peer: Target, *, limit: int = 100, query: str = '') -> AsyncIterator[Topic]

The topics in a forum, pinned ones first.

A group that is not a forum has none, and says so, not answering with an empty list.

get_topic async

get_topic(peer: Target, topic_id: int) -> Topic

One topic, by id, which for a topic is the id of its first message.

create_topic async

create_topic(peer: Target, title: str, **options: Any) -> Topic

Open a topic, and answer with it.

The topic's id is the id of the message this makes, which is what the answer is dug out of: there is no separate id space for topics.

edit_topic async

edit_topic(peer: Target, topic_id: int, **options: Any) -> Any

Change a topic's title, icon, or whether it is closed or hidden.

close_topic async

close_topic(peer: Target, topic_id: int) -> Any

Stop anybody but an administrator posting in a topic.

reopen_topic async

reopen_topic(peer: Target, topic_id: int) -> Any

Let people post in a topic again.

pin_topic async

pin_topic(peer: Target, topic_id: int, *, pinned: bool = True) -> Any

Hold a topic at the top of the forum, or let it go.

delete_topic async

delete_topic(peer: Target, topic_id: int) -> int

Delete a topic and everything in it, and say how much went.

set_forum async

set_forum(peer: Target, enabled: bool = True, *, tabs: bool = False) -> Any

Turn topics on or off for a supergroup.

Telegram refuses this for a group with too few members. Turning it off deletes nothing: everything that was in a topic moves back into the one conversation the group used to be.

join_chat async

join_chat(peer: Target) -> None

Join a channel or supergroup, by name or by invite link.

leave_chat async

leave_chat(peer: Target) -> None

Leave a chat, whichever kind it is.

get_chat async

get_chat(peer: Target) -> Chat

Everything Telegram will say about a chat, not only what a list shows.

get_user async

get_user(peer: Target) -> User

Everything Telegram will say about one person.

get_contacts async

get_contacts() -> list[User]

This account's contact list.

block_user async

block_user(peer: Target) -> bool

Block someone, so they cannot write here.

unblock_user async

unblock_user(peer: Target) -> bool

Undo that.

update_profile async

update_profile(**fields: Any) -> User

Change this account's own first_name, last_name or about.

Only what is named changes. Leaving a field out leaves it alone; clearing one is passing an empty string.

download_profile_photo async

download_profile_photo(peer: Target = 'me', **options: Any) -> Any

Fetch someone's profile picture, or a chat's.

Their own copy is fetched instead of the small one carried around in answers, which is why this costs a call before it costs a download.

download async

download(what: Any, **options: Any) -> Any

Fetch a file, from a message, media, document, photo or reference.

A file reference goes stale after an hour or so, and the cure is to fetch whatever carried the file again. Where what was passed says which message that was, which a message and a portable reference both do, that happens here without being asked for, so a reference stored last week still downloads.

stream

stream(what: Any, **options: Any) -> AsyncIterator[bytes]

Fetch a file a piece at a time, in order, instead of all at once.

For anything that can start work on the front of a file before the back of it has arrived, and for anything too big to want in memory. Takes offset and length for a byte range, and otherwise the same arguments as download.

Not awaited, iterated:

async for piece in client.stream(message):
    ...

upload async

upload(source: Any, **options: Any) -> Any

Send a file up, and answer with the handle for attaching it.

promote async

promote(peer: Target, user: Target, rights: AdminRights | None = None, *, title: str = '') -> None

Make someone an administrator, with the powers named and no others.

Saying nothing about the rights grants none of them, which is the same thing demote does and is the safe way for a default to point.

demote async

demote(peer: Target, user: Target) -> None

Take every power back, leaving them an ordinary member.

restrict async

restrict(peer: Target, user: Target, permissions: Permissions, *, until: int = 0) -> None

Limit what one person may do here, until a unix time or forever.

ban async

ban(peer: Target, user: Target, *, until: int = 0) -> None

Remove someone and keep them out.

unban async

unban(peer: Target, user: Target) -> None

Lift every restriction, so they may come back if they want to.

kick async

kick(peer: Target, user: Target) -> None

Remove someone without keeping them out, so they may rejoin.

get_permissions async

get_permissions(peer: Target, user: Target) -> Permissions

What one person may do here, as the positive set instead of the raw one.

An administrator comes back allowed everything, since their powers are held separately and no restriction applies to them.

get_member async

get_member(peer: Target, user: Target) -> Member | None

One person's whole standing here, or nothing if they are not in it.

This is the question behind most permission checks, and asking it in one call beats inferring it from rights: member.is_admin counts the creator, which testing for admin alone does not.

Both kinds of chat answer. A basic group has no call for one member, so the membership is fetched and the one asked about picked out of it.

get_admin_rights async

get_admin_rights(peer: Target, user: Target) -> AdminRights

What powers one person holds here, or none if they hold none.

set_chat_title async

set_chat_title(peer: Target, title: str) -> None

Rename a chat, whichever kind it is.

set_chat_photo async

set_chat_photo(peer: Target, photo: Any = None) -> None

Change a chat's picture, or remove it by passing nothing.

set_chat_description async

set_chat_description(peer: Target, about: str) -> bool

Set the description shown above a chat.

set_chat_permissions async

set_chat_permissions(peer: Target, permissions: Permissions) -> None

What everybody who is not an administrator may do here.

set_slow_mode async

set_slow_mode(peer: Target, seconds: int) -> None

How long a member waits between messages, or zero for no wait.

create_group async

create_group(title: str, users: list[Target]) -> Chat

Start a basic group with some people in it.

create_channel async

create_channel(title: str, *, about: str = '', megagroup: bool = False, forum: bool = False) -> Chat

Start a broadcast channel, or a supergroup if megagroup is asked for.

delete_chat async

delete_chat(peer: Target) -> None

Delete a chat for everybody in it, which only its owner can do.

add_chat_members async

add_chat_members(peer: Target, users: list[Target], *, forward_limit: int = 0) -> None

Put people into a chat directly, rather than handing them a link.

export_invite_link(peer: Target, **options: Any) -> str

Make a new invite link and hand back the link itself.

Everything else about it is on the raw answer, which is what the methods layer hands over; this is the part programs want.

revoke_invite_link(peer: Target, link: str) -> Any

Kill a link, so it admits no one else.

get_invite_links(peer: Target, *, admin: Target = 'me', revoked: bool = False) -> list[Any]

The links one administrator has made here.

approve_join_request async

approve_join_request(peer: Target, user: Target, *, approved: bool = True) -> None

Let someone in who asked to join, or turn them down.

approve_all_join_requests async

approve_all_join_requests(peer: Target, *, approved: bool = True, link: str = '') -> None

Answer everybody waiting to join at once.

One call instead of one per person, which is the difference between emptying a week's queue and being rate limited halfway through it. Naming an invite link answers only the people who came in through it.

get_admin_log async

get_admin_log(peer: Target, *, limit: int = 100, query: str = '') -> AsyncIterator[Any]

What administrators have done here, newest first.

The entries are raw: there are several dozen kinds of them and they have nothing in common but an id, a date and who did it, so wrapping them would hide more than it explained.

wrap_message

wrap_message(raw: Any, *, users: dict[int, Any] | None = None, chats: dict[int, Any] | None = None, replies: dict[int, Any] | None = None) -> Message | None

Turn a raw message into one bound to this client, and remember it.

What the dispatcher calls on the way to a handler, and what to call by hand for a message pulled off a raw update. Two things happen here that Message.from_raw cannot do on its own: the message being replied to is looked up among the ones this client has lately seen, and this message is written down so that the next reply to it costs nothing either.

file_ref

file_ref(what: Any, **options: Any) -> str

A file as one string that can be written down and used later.

Takes a message, the media off one, a document or a photo. What comes back can be handed to send_media or download tomorrow, from another process, out of a database column.

Dispatcher

sunnygram.dispatcher.Dispatcher dataclass

The handlers a client holds, and the routing that feeds them.

listening property

listening: int

How many questions are waiting for an answer right now.

add

add(handler: Handler) -> Handler

Register a handler, keeping the list in the order it will run in.

remove

remove(handler: Handler) -> None

Take a handler out again.

listen

listen(chat_id: int, *, kind: Kind = 'message', filters: Filter | None = None, exclusive: bool = True) -> Listening

Wait for the next thing of a kind from one chat.

Returns the record rather than the future, because the caller has to be able to take it out again when it stops waiting, and a caller that only held the future would have no way to.

stop_listening

stop_listening(waiting: Listening) -> None

Take a question out of the table, answered or not.

feed async

feed(client: Any, event: Event) -> None

Turn one update into something friendly and offer it around.

The raw reading first in every case, so a program can watch everything and still act on the friendly shape the easy way, and then the friendly reading if this update has one.

The friendly one is built only when someone asked for that kind. Wrapping a message costs about as much as decoding it did, and a program with one inline handler in it has no reason to pay that for every message that goes past. _wanting already knows who asked, so asking it first is the whole of the saving.

collect_albums

collect_albums(*, wait: float = ALBUM_WAIT) -> AlbumCollector

Start putting albums back together, so album handlers can fire.

close

close() -> None

Let go of anything held between updates.

sunnygram.dispatcher.Handler dataclass

One callback, what it wants, and when it runs.

sunnygram.dispatcher.AlbumCollector

Puts the parts of an album back together.

Telegram sends an album as several ordinary messages that happen to share a group id, so a handler wanting the block rather than the pieces has to wait for the pieces to stop arriving. There is no marker for the last one, which is why this is a short silence instead of a count: the only thing that says an album is complete is nothing else turning up.

The parts still reach message handlers on their own. Nothing is swallowed here, so a program written before albums existed keeps working and a program that wants them asks for them.

dropped property

dropped: int

Parts thrown away because too many arrived at once.

add

add(message: Message) -> None

Take one part, and restart the clock for its group.

close

close() -> None

Stop waiting for anything, and let go of what is held.

sunnygram.dispatcher.StopPropagation

Bases: Exception

Raised by a handler that wants nothing after it to run.

Invoker

The layer below the client: one session, a connection per datacenter, and the retries that make a call survive a dropped socket.

sunnygram.network.invoker.Invoker

A session held open across connections and datacenters.

Owns the storage and the current connection. Start it, invoke through it, close it; what happens in between to the socket is its problem, not the caller's.

state property

state: SessionState

The session as it stands. Change it and call save.

client property

client: ClientInfo

The application this session belongs to.

updates property

updates: Queue[TLObject]

Everything the server sent that answered no call, across reconnects.

dropped_updates property

dropped_updates: int

How many updates were thrown away because no one was draining them.

Counted across connections rather than per connection, so that replacing one does not reset it. The update layer watches this: a number that has moved means something never arrived, and the only way to find out what it was is to ask for a difference.

limiter property

limiter: RateLimiter | None

The pacing in force, or nothing if it was turned off.

Worth reading instead of only setting. Its waited counter says how long this program has spent being held back, which is the honest measure of whether it is asking for more than the account can safely give.

peers property

peers: PeerCache

Who this session knows how to name, and how to look one up.

started property

started: bool

Whether this invoker has been started and not yet closed.

open_connections property

open_connections: int

How many sockets this session is holding open, everywhere.

connection property

connection: Connection | None

The connection in use, if there is one right now.

start async

start() -> SessionState

Load the session and connect to whichever datacenter is home.

save async

save() -> None

Write the session down as it stands.

close async

close() -> None

Put every connection down and let go of the storage.

invoke async

invoke(request: TLFunction[TLResult], *, dc_id: int | None = None, bulk: bool = False, timeout: float | None = None) -> TLResult

Call a TL function, following the server and the network as needed.

The answer is typed as whatever the function says it is answered with, so nothing above this has to guess or assert what came back.

A dropped connection is rebuilt and the call is sent again, and so is a call the server turned down with one of its "not right now" errors. That is safe for anything the server deduplicates, which is what random_id is for on the calls where it matters, and it is what makes a long-running program survive both a network that comes and goes and a datacenter having a bad minute.

dc_id sends the call somewhere other than home, which is what files need: a document lives in the datacenter it was uploaded to, and fetching it must not move the account there. The first call to another datacenter signs in to it by exporting the authorization from home.

bulk says this call is part of a transfer and should go through the connections kept for those. Telegram meters a connection rather than an account, so several of them move a file several times faster, and keeping that traffic off the main connection is what stops a download from delaying everything else. It is only right for calls whose order does not matter, which is why it is asked for instead of assumed.

migrate async

migrate(dc_id: int) -> None

Move home to another datacenter and connect there.

Each datacenter issues its own authorization key, so moving before logging in simply means negotiating another one. Moving an account that is already logged in is the login-time path this was written for; a call that merely has to happen somewhere else takes dc_id on invoke instead and leaves home where it is.

prepare_cdn async

prepare_cdn(dc_id: int) -> None

Find out where a CDN datacenter is, before anything is sent to it.

Called with the number out of a CDN redirect. Two questions go to the datacenter we are already talking to: where that number lives, which only help.getConfig knows, and which public key names it, which only help.getCdnConfig knows. From then on invoke(dc_id=...) reaches it like anywhere else, except that no authorization is exported to it, because it holds none and asking would tell it who we are.

Idempotent, and asked for rather than done automatically: a number that has not been through here is treated as one of Telegram's own.

is_cdn

is_cdn(dc_id: int) -> bool

Whether this number has been looked up as a CDN datacenter.

sunnygram.network.ClientInfo dataclass

Who is calling: the application, and what to call the device it runs on.

The api_id and api_hash are the pair my.telegram.org issues, and they belong together even though only the id is ever sent in initConnection. The device and app strings are what the account holder sees in their list of active sessions, so they are worth setting to something recognizable, not leaving as the default.

Pacing

sunnygram.network.limiter.RateLimiter

The pacing an account gets unless it asks for something else.

One bucket for every call, and one per chat for the calls that put something into a chat. A call waits on whichever of the two is behind.

waited property

waited: float

Seconds this limiter has held calls back, over its whole life.

Worth looking at. A number that keeps climbing means the program wants to go faster than the account safely can, and the answer is usually to do less rather than to raise the limit.

hold async

hold(request: TLObject, *, bulk: bool = False) -> None

Wait until this call may go out.

A transfer goes straight through. Telegram meters a file by the bytes on a connection instead of by the calls made, which is the whole reason the file engine spreads parts across several, and pacing those here would undo that without making the account any safer. They stay bounded by the pool and the in-flight cap instead (rule P6).

sunnygram.network.limiter.TokenBucket

A rate, a burst, and somewhere to wait.

Tokens accrue at a fixed rate up to a ceiling, and a call spends one. A bucket that is full is a program that has been quiet, and it is allowed to catch up all at once, which the burst is for.

tokens property

tokens: float

Roughly how many calls could go right now, for a diagnostic.

take async

take() -> float

Wait until a call may go, and say how long that took.

The wait happens holding the lock, so callers go through in the order they arrived instead of racing each time a token appears.

Getting out

sunnygram.transport.proxy.Proxy dataclass

Where to connect instead of straight to the datacenter.

Build one through socks5, http or mtproto rather than by hand: each of them checks the things that are only wrong for its own kind.

__repr__

__repr__() -> str

Never the secret or the password (rule S2).

These end up in logs and tracebacks, and an MTProxy secret is a credential: anyone holding it can use that proxy.

socks5 classmethod

socks5(host: str, port: int, *, username: str | None = None, password: str | None = None) -> Proxy

A SOCKS5 tunnel, with a username and password if it wants them.

http classmethod

http(host: str, port: int, *, username: str | None = None, password: str | None = None) -> Proxy

An HTTP proxy reached with CONNECT, which is the only verb used.

mtproto classmethod

mtproto(host: str, port: int, secret: str | bytes) -> Proxy

An MTProxy, with the secret written however it was handed over.

Hex and the url-safe base64 form both appear in the wild, sometimes for the same proxy, so both are read here.

from_link(link: str) -> Proxy

A proxy out of one of the links Telegram hands around.

Both the tg: and the t.me spellings, and both kinds: a proxy link with a secret is an MTProxy, one with a user and password is SOCKS5.

sunnygram.transport.obfuscation.Obfuscation dataclass

One obfuscated stream: what to send first, and the two ciphers.

The ciphers carry the keystream position, so one of these belongs to one connection and cannot be reused after a reconnect.

encrypt

encrypt(data: bytes) -> bytes

Scramble bytes on their way out.

wrap

wrap(reader: Reader) -> ObfuscatedReader

Put a reader behind the incoming cipher.