Layers¶
Everything under the client, for a program that wants one piece of it. ARCHITECTURE.md
in the repository explains how they fit together; this is the reference for reaching them
directly.
Sessions¶
sunnygram.storage.base.Storage ¶
Somewhere a session can be kept.
Subclasses decide what that means: memory, a file, a string you carry around. All three are interchangeable, which lets a program be written once and moved between them.
sunnygram.storage.base.SessionState
dataclass
¶
Everything about a session that has to survive a restart.
The keys are per datacenter because each one issues its own, and a client that has been to more than one datacenter for files or a migration holds several. dc_id names the one that is home: the one that answers for the account.
sunnygram.storage.sqlite.SQLiteStorage ¶
sunnygram.storage.string.StringStorage ¶
Bases: MemoryStorage
A session that came from a string, and can go back to being one.
Built on the memory backend because that is what it is: nothing is written anywhere until someone asks for the string and puts it somewhere.
export ¶
The string for the session as it stands.
Sync, because there is nothing to wait for, and because the natural moment to call it is right after a login, printing the result once.
sunnygram.storage.string.encode_session ¶
Spell a session as a string.
Only the key for the home datacenter travels. Keys for the others are worth keeping in a file, where re-negotiating one costs nothing, but not worth tripling the length of something a person has to paste.
sunnygram.storage.string.decode_session ¶
Read a session back from a string, refusing anything that is not one.
Logging in¶
sunnygram.auth.login.log_in
async
¶
log_in(invoker: Invoker, *, phone_number: str | Callable[[], Any], code: Callable[[SentCode], Any], password: Callable[[str], Any] | None = None, bot_token: str | None = None) -> types.User
Sign in, asking for whatever is missing along the way.
The callables are how this stays usable from a script, a prompt or a chat window without knowing which it is: each is called when the answer is needed and may be sync or async. password is handed the account's hint, which is often the only reminder the person has.
A session that is already signed in is returned as it is, so this is safe to call every run.
sunnygram.auth.login.send_code
async
¶
Ask Telegram to send a login code to a phone number.
The number may belong to another datacenter than the one we are talking to, in which case the server says so and the invoker moves before this returns.
sunnygram.auth.login.sign_in
async
¶
Finish a phone login with the code that arrived.
Raises SessionPasswordNeeded when the account has a second factor, which is not a failure: the code was right, and check_password is what comes next.
sunnygram.auth.login.check_password
async
¶
Get past a second factor with the account password.
The password itself never leaves this machine. What goes out is a proof built from it, and building that proof is deliberately slow, so it happens off the event loop.
sunnygram.auth.login.sign_in_qr
async
¶
sign_in_qr(invoker: Invoker, show: Callable[[LoginToken], Any], *, timeout: float = QR_TIMEOUT, poll: float = QR_POLL) -> types.User
Log in by having an already-signed-in client scan a code.
show is called with each token, and again whenever one expires and is replaced, so whatever is drawing the code can redraw it. Raises SessionPasswordNeeded if the account has a second factor, exactly as a phone login does, and check_password finishes it the same way.
sunnygram.auth.login.sign_in_bot
async
¶
Sign in as a bot, which needs nothing but its token.
sunnygram.auth.login.resend_code
async
¶
Ask for the code again, usually by another route than the first.
sunnygram.auth.login.log_out
async
¶
End this session and forget everything that was kept for it.
The key is dead on the server the moment this returns, so keeping our copy would only be a credential that no longer opens anything.
sunnygram.auth.login.get_me
async
¶
Who this session is signed in as.
sunnygram.auth.login.SentCode
dataclass
¶
A code is on its way, and this is what signing in with it will need.
The hash is what ties the code to the request that asked for it, so it has to come back with the code. kind says where the code went, which is worth telling the person, since a code in the Telegram app and a code in an SMS look for different things.
sunnygram.auth.login.LoginToken
dataclass
¶
A QR login waiting to be scanned.
url
property
¶
The link to put in the QR code.
An official client that scans this is being asked to authorize us, so what it encodes is a credential in flight. Show it to the person logging in and no one else.
Peers¶
sunnygram.peers.resolver.resolve
async
¶
Name a peer to the server, however the caller happened to say it.
Costs nothing for anything the session has already seen. A username or a phone number that is genuinely new costs one call, after which it is known for good.
sunnygram.peers.resolver.resolve_username
async
¶
Ask the server who holds a username, and remember the answer.
Goes to the network even when the peer is already known, which is what makes it the way to notice that a name changed hands. resolve is the one to call for ordinary work.
sunnygram.peers.resolver.resolve_phone
async
¶
Ask the server whose number this is, and remember the answer.
Only works for numbers already in the account's contacts, which is Telegram being careful instead of this being incomplete.
sunnygram.peers.resolver.mark_id ¶
Spell an id the way the Bot API does, for talking to things that do.
Only useful at the edges: Sunnygram works in the ids the protocol uses.
sunnygram.peers.resolver.mark_peer ¶
A Peer as the one number that names it, or nothing if it is not a peer.
The useful part is that the answer is a single int, so a peer fits anywhere something has to be written down: a dict key, a database column, a portable file reference. Handing it back to resolve reaches the same peer.
sunnygram.peers.resolver.unmark_id ¶
Read a Bot API style id back, with the kind its sign implies.
The kind that comes out is the coarse one, because that is all a sign can carry: a person and a bot are spelled alike, and so are a channel and a supergroup. That is enough to build an input peer, and the cache knows better anyway whenever it has met the peer before.
Files¶
sunnygram.files.download.download_file
async
¶
download_file(invoker: Invoker, source: Any, *, into: str | PathLike[str] | None = None, chunk_size: int = DOWNLOAD_CHUNK, workers: int = WORKERS, progress: Progress | None = None, refresh: Refresh | None = None, limit: int = 0, cdn: bool = True) -> bytes | Path
Fetch a file, into memory or onto disk.
The source is a message, the media off one, a document, a photo, or a FileSource that locate has already worked out. into names a file to write to and makes the answer that path; leaving it out makes the answer the bytes themselves, which is what a thumbnail or a small document usually wants.
limit refuses anything bigger than it, before fetching instead of after, for a program that is downloading something it did not choose.
cdn says whether to accept being sent to a content delivery network for a popular file, which is faster nearly everywhere and is what every other client does. Turning it off keeps the whole transfer inside Telegram, at the price of the datacenter having to serve it itself.
sunnygram.files.upload.upload_file
async
¶
upload_file(invoker: Invoker, source: str | PathLike[str] | bytes | bytearray | BinaryIO, *, name: str | None = None, part_size: int = UPLOAD_PART, workers: int = WORKERS, progress: Progress | None = None) -> base.InputFile
Send a file up, and return the handle for attaching it to something.
The source is a path, the bytes themselves, or anything with a read method. The answer is only good for one send, and only for a while: hand it to the call that posts the message rather than keeping it.
name is what the file will be called on the other side, and defaults to the name on disk, or to the id when there is nothing to take it from.
sunnygram.files.cdn.CdnSession ¶
One file, on the CDN it was redirected to.
Built from the redirect and shared by every worker on that file, which is what lets one of them learn a hash or fix a cold cache for all of them.
block
property
¶
How much of the file one published hash covers.
Read off the hashes themselves rather than assumed, since it is the server's choice, and taken as the largest of them, since the last block of a file is a short one. It is what a request gets rounded out to, because a block is the smallest thing that can be checked.
fetch
async
¶
One piece of the file, decrypted and checked.
The bytes handed back are exactly the ones asked for. Getting them may mean fetching a little more, when the range asked for starts or ends inside a block, since a block is what a hash covers. That only happens to a caller who chose pieces smaller than the blocks Telegram publishes for the file, which is no one by default.
sunnygram.files.ref.file_ref ¶
The portable reference for whatever file the caller is holding.
Takes the same things locate does: a message, the media off one, a document or a photo. thumb names a particular rendition instead of the largest, which is how to write down a thumbnail on its own.
A message also says where it came from, and that is packed in so the reference can refresh its own token later. Pass origin=False to leave it out.
sunnygram.files.ref.decode_ref ¶
Read a reference back, or say why this is not one.
Everything about the string is checked before anything is believed: the length, the version, the checksum, and that each part is inside what was written down. A reference that has been truncated in a database column or had a character eaten by a URL fails here rather than becoming a request for some other file (rule S3).
sunnygram.files.ref.parse_ref ¶
The same, answering None instead of raising, for a caller that is guessing.
This is what makes a string usable anywhere a file is: send_media and download can be handed one without a program having to say which of the two kinds of string it is holding.
sunnygram.files.ref.FileRef
dataclass
¶
A file, named portably: everything needed to fetch or resend it.
Topics¶
sunnygram.methods.forum.iter_topic_pages
async
¶
iter_topic_pages(invoker: Invoker, peer: Target, *, query: str = '', limit: int = 100, batch: int = TOPIC_BATCH) -> AsyncIterator[Any]
The topics in a forum, a page at a time.
The cursor is three things again: the date and id of the last topic's most recent message, and the topic's own id. Pinned topics come first however the rest are ordered, which is the server's doing and not something a cursor can express, so a page is taken as it arrives.
sunnygram.methods.forum.topics_by_id
async
¶
Particular topics, by id, with the messages that opened them.
sunnygram.methods.forum.create_topic
async
¶
create_topic(invoker: Invoker, peer: Target, title: str, *, icon_color: int | None = None, icon_emoji_id: int | None = None, send_as: Target | None = None) -> Any
Open a topic, and answer with the updates that made it.
The new topic's id is the id of the message this creates, which is the one thing worth knowing about how forums are built: there is no separate id space. icon_color is one of the six Telegram allows and is ignored when a custom emoji is given, since that replaces the icon instead of tinting it.
sunnygram.methods.forum.edit_topic
async
¶
edit_topic(invoker: Invoker, peer: Target, topic_id: int, *, title: str | None = None, icon_emoji_id: int | None = None, closed: bool | None = None, hidden: bool | None = None) -> Any
Change a topic, leaving out whatever is not being changed.
Closing one stops anybody but an administrator posting in it. Hiding one takes it off the list without deleting anything, and only the general topic can be hidden.
sunnygram.methods.forum.pin_topic
async
¶
Hold a topic at the top of the list, or let it go.
sunnygram.methods.forum.reorder_topics
async
¶
Put the pinned topics in a given order, first in the list first.
force says that topics left out of the list are to be unpinned, rather than left pinned in whatever order they were.
sunnygram.methods.forum.delete_topic
async
¶
Delete a topic and everything in it, and say how much went.
Telegram deletes a history a slice at a time and answers with how far it got, so this asks again until it says nothing is left. rounds is a ceiling on that, since a topic being written to as fast as it is deleted would otherwise never end.
sunnygram.methods.forum.toggle_forum
async
¶
Turn topics on or off for a supergroup.
Telegram refuses this for a group with too few members, and turning it off does not delete the topics: everything that was in one moves back into the single conversation the group used to be.
sunnygram.methods.messages.reply_header ¶
Where in a chat a message belongs: a reply, a topic, or neither.
A forum topic is the message that opened it, and being in one is spelled as replying to that message, which is why one field says both things. Given only a topic, the message is posted to it; given both, the reply names the message being answered and the topic names the thread it is in.
Crypto¶
Written from scratch and validated against official vectors. Not something to call directly, and worth reading before trusting.
sunnygram.crypto.describe ¶
One line saying which ciphers are in use, for a diagnostic or a bug report.
Worth printing when a transfer is slower than it should be: the answer is almost always that this says python.
The wire¶
sunnygram.network.connection.Connection ¶
One live, encrypted conversation with one datacenter.
Built around an already-connected transport and a session that holds the auth key for it, so what happens when the socket cannot be opened, and where the key came from, are both someone else's decision. See connect for the usual way to get one.
in_flight
property
¶
How many calls are waiting on an answer right now.
The one honest measure of how busy this connection is, which is what the pool above chooses on: a connection with nothing outstanding will answer sooner than one already carrying a file.
running
property
¶
Whether the reader is alive and calls can still be made.
A recorded failure counts as not running even in the moment before the reader task has finished unwinding, so that nothing new is handed to a connection already known to be dead.
updates
property
¶
Everything the server sent that was not an answer to a call.
The update layer drains this. It is bounded, because a consumer that stops draining must not be able to grow it without limit, and the reader never blocks on it: when it is full the newest update is dropped and counted, and a non-zero count is the update layer's cue that it has to catch up through getDifference instead of trust what it has.
dropped_updates
property
¶
How many updates were dropped because no one was draining them.
unknown_constructors
property
¶
How many incoming objects no constructor in the pinned layer claimed.
Expected to be zero, and not fatal if it is not: a server running ahead of our schema can send something we cannot read, and dropping it is better than dropping the connection.
invoke
async
¶
Call a TL function and wait for what the server answers with.
The answer is typed as whatever the function says it is answered with, so invoking help.GetConfig gives back a Config and not an anything.
Raises whatever the server refused with, as a typed error. A FLOOD_WAIT short enough to sit out is waited out here instead, which is the safe default: the alternative is every caller writing the same retry.
sunnygram.session.session.Session ¶
The state of one conversation with one datacenter.
time_known
property
¶
Whether this session has been told what time the server thinks it is.
adopt_server_time ¶
Set our clock from a message id the server itself minted.
Used when the server complains that our ids are too low or too high, and on the first message of a session that started from a stored key. Its own id says what time it thinks it is, which is the only opinion that matters, and adopting it lets the next id land inside the window.
reset ¶
Start again on the same key, under a new session id.
The remedy for a sequence number the server will not accept: there is no way to argue about the count, so the count is abandoned along with the session it belonged to. The key, the salt and the clock all survive, since none of them was what went wrong.
next_seq_no ¶
The sequence number for the next message.
Content-related messages take an odd number and move the count on; everything else reads the count without touching it.
encrypt ¶
Wrap a serialized message, returning its id and the frame to send.
The id comes back because it is how an answer will be matched to this request.
encrypt_off_loop
async
¶
encrypt, with a big cipher call moved to a worker thread.
The id and the sequence number are still taken here, on the loop, and only the cipher travels. That matters: both of those are the order the server expects things in, so a caller has to hold whatever lock keeps sends in order across the await, exactly as it would for encrypt.
decrypt_off_loop
async
¶
decrypt, with a big cipher call moved to a worker thread.
The checks stay here rather than travelling with the cipher, so the replay window is only ever touched by the task that owns this session.
receive ¶
Unwrap a frame into the messages it really carries.
What arrives may be one message or a container of several, and each one inside a container has an id of its own that has to pass the same checks as the envelope's. A copy of something already handled is dropped rather than raised over, because a container is allowed to redeliver an answer we never acknowledged while still carrying messages that are new.
receive_off_loop
async
¶
receive, with a big cipher call moved to a worker thread.
sunnygram.transport.tcp.TCPTransport ¶
One TCP connection to one datacenter.
sunnygram.tl.core.TLObject ¶
Base class for every TL constructor and function.
Subclasses set ID to their constructor id and implement the two halves of the codec. Defining a subclass with an id registers it, so a hand-written type is readable the moment its module is imported.