mtflute 0.4.0
mtflute: ^0.4.0 copied to clipboard
Pure-Dart Telegram MTProto client (API layer 228) with a built-in HTTP streaming engine for mediakit / video players.
Changelog #
0.4.0 #
Implements the remaining missing MTProto features from the deep feature audit.
Auth #
- QR-code login —
client.qrLogin()(auth.exportLoginToken/importLoginToken), returns atg://login?token=…URL and completes when scanned. - 2FA password setting/changing —
client.edit2FA(currentPassword, newPassword, hint, email)with a newcomputeDigestSRP primitive; pass an emptynewPasswordto remove the password. - Multiple simultaneous DC connections with per-DC persisted auth keys —
SessionDatanow stores adc_keysmap (file + gogram-compatible string session).exportToDcreuses a cached per-DC key instead of re-running export/import each time. - AUTH_KEY_DUPLICATED — regenerates the auth key on that DC and retries instead of reusing a poisoned key.
logOut()also clears in-memory auth and the persisted session.
Updates #
- Common-box
ptsdedup + gap detection — already-applied updates are skipped and a forward gap triggersgetDifference. seqgap detection forupdates/updatesCombinedenvelopes.
Transport #
- Proxy support —
Proxy.socks5/Proxy.socks4/Proxy.http(with auth); passproxy:toMtpClient.
Files #
- CDN download —
upload.getCdnFilewith AES-256-CTR decryption andreuploadCdnFilehandling; CDN-served files no longer throw.
Secret chats (E2E) #
startSecretChat/acceptSecretChat/completeSecretChat/discardSecretChat— Diffie-Hellman key exchange, key-fingerprint verification, MTProto-2.0 E2E encryption, in/out seq tracking.sendSecretMessage/decryptSecretMessagefor text messages;onSecretChatUpdate/onEncryptedMessagehandlers.
Misc #
- Default
appVersionnow tracks the library version (libVersion). - TL strings and SRP passwords are UTF-8.
0.3.0 #
New MTProto features found by a deep missing-feature audit against the official docs.
Updates #
channels.getChannelDifference: per-channelptsis now tracked and gaps are recovered viagetChannelDifference(channels have their own message box; the commongetDifferencenever returns channel messages). HandlesUpdatesChannelDifference/Empty/TooLong.updateChannelTooLongnow triggers a channel-difference catch-up instead of being dropped.getDifferencenow loops onupdates.differenceSliceuntil it drains, instead of applying a single slice and stopping.- Channel-message gap detection from
pts/pts_countonupdateNewChannelMessage.
Service messages #
msg_detailed_info/msg_new_detailed_infoare acknowledged (the server otherwise re-sends the notification indefinitely).msgs_state_reqis answered by acking the referenced message ids.- The periodic keepalive is now
ping_delay_disconnect(75s), so the server also reaps a half-open connection if the client goes silent.
Outgoing #
- Requests larger than 512 bytes are
gzip_packedbefore sending, per the spec's recommendation to compress large queries.
Transport #
- IPv6 endpoints are bracketed (
[addr]:port) so the port is unambiguous; address parsing no longer breaks on the colons in IPv6.
Auth / media #
logOut()now invalidates the auth key, clears the peer cache, and deletes the persisted session file.inflateStrippedThumb()reconstructs a full JPEG from aphotoStrippedSizethumbnail.
0.2.9 #
Stability under parallel file transfer, plus a round of MTProto spec-conformance fixes.
Stability #
- Fixed an unhandled async error crash:
connect()'s single-flight gate completed with an error that had no listener when_doConnectfailed and no concurrent caller was waiting, killing the isolate mid-download. - Outgoing
msgs_ack: the client now acknowledges content-related server messages (odd seqno), batched at a threshold of 10 with a 300ms flush timer. Missing acks made the server resend and drop connections under sustained load ("Peer closed connection"). invokenow retries transient network / reconnect / salt errors within a 2-minute budget instead of giving up after 3 tries, so a flapping worker recovers instead of throwing.- Reconnect backoff is flap-aware: a connection that dies seconds after connecting no longer reconnects at full speed; the delay escalates with the flap count.
- Download now keeps
workers × pipelinechunks in flight (pipelined) rather than justworkers.
MTProto spec conformance #
- Decrypt now validates the message length and padding (length divisible by 4, padding in [12,1024], no read past the buffer) per the security guidelines, replacing an uncontrolled
RangeErrorwith a clean protocol error. - Container inner messages now handle
bad_server_salt/new_session_created/bad_msg_notification/pong/msgs_ack, not justrpc_result— previously service messages wrapped in a container were ignored. gzip_packedis now unwrapped when it appears as an update body (top-level or inside a container).- 4-byte transport-error frames (e.g.
-404) are detected in the transport and surfaced asTransportError. _lastMsgIdis reset after abad_msg16/17 time correction so the monotonicity clamp doesn't pin new ids to the pre-correction clock.- DH handshake now verifies
dh_primeis a safe prime (Miller-Rabin onpand(p-1)/2) and that the generatorgsatisfies its quadratic-residue condition;g_bgets the same2^1984range bound asg_a. - Download
chunkSizemust now divide 1 MB (in addition to being a multiple of 4096, ≤ 1 MB) so no request crosses a 1 MB boundary.
0.2.8 #
- Fixed
MSGID_DECREASE_RETRYwhen a caller mixes parallel worker RPCs (viathreads: >1onuploadFile/downloadStream/downloadRange) with main-connection RPCs.copyAuthFromnow copies the parent's_timeOffsetso same-DC sub-clients agree with main on encoded msg-id timestamps. - Exported sub-clients created by
exportToDcare markedworkerMode = trueand skip the updates loop / ping timer, avoiding needlessupdates.getStatetraffic on file-transfer workers. - Raised the internal
_maxWorkerscap from 3 to 16 so explicitthreads: Nrequests up to 16 are honored. - Verified end-to-end at
threads= 1, 4, 8, 12: noMSGID_DECREASE_RETRY, byte-exact roundtrip, main connection intact for subsequent RPCs.
0.2.7 #
- Logger now prefixes each line with a
HH:MM:SS.mmmtimestamp by default. Disable viaLogger(showTimestamp: false).
0.2.6 #
- Updated Telegram API to layer 228 (from TDesktop dev).
- README rewritten for clarity, with an expanded Flutter integration example.
0.2.5 #
- New
client.downloadBlock(peer:, msgId:, blockIndex:)— a streaming-optimized single-block fetch for local range servers. Downloads one aligned block (default 1 MB) clamped to the file's real size, over the primary connection with the 512 KB pipelined sub-chunk config that benchmarked fastest and most drop-resistant for serialized byte-range streaming (a local HTTP shim feeding a player: 512 KB sub-chunks pipeline twoUpload.getFileper block ≈ 1.4 MB/s vs ≈ 0.9 MB/s for a single 1 MB request). WrapsdownloadMessageRangewith the offset/clamp math a range server would otherwise repeat, sharing the same media cache andfile_referencerefresher. Returns an empty list past EOF.
0.2.4 #
downloadRangeworker pool now puts the primary (main) connection first and always includes it. The primary connection is the most stable; exported sub-DC senders can be dropped by the DC under concurrent load, so preferring the primary eliminates the "Peer closed connection" reconnect churn that was tanking sustained throughput. Retuned_countWorkers/_pipelinePerWorkerfor pipelined reads on the primary connection.
0.2.3 #
- Tuned
downloadRangeconcurrency: 4 connections x 4 pipelined requests each, retuned_countWorkersthresholds. Balances throughput against the DC's connection-drop anti-abuse on high-latency mobile paths.
0.2.2 #
Fixes video playback stability + eliminates player jank. Validated end-to-end with ffprobe/ffmpeg against a real streamed file (clean decode mid-file, no corruption, faster than realtime).
- Off-isolate decryption. Large ciphertexts (file chunks, ≥128 KB) now
decrypt in a persistent worker-isolate pool (
decrypt_pool.dart) instead of on the main isolate. The AES-IGE block loop no longer blocks UI rendering / the local stream server — measured main-isolate stalls dropped from ~90 ms to ~38 ms during sustained streaming._processIncoming/deserializeEncryptedgained async variants; small messages still decrypt inline. Dispatch is by msgId so the isolate hop cannot reorder RPC results. - Reconnect no longer crashes downloads. Transient "pending request invalidated" reconnect errors are retried instead of surfacing as an unhandled async exception (which previously killed the serving connection — the "nothing plays" symptom for HTTP-streamed media).
0.2.1 #
Streaming throughput + smoothness fixes (benchmarked on a high-RTT mobile path).
downloadRange / worker pool #
- Per-connection request pipelining (up to 3 concurrent
upload.getFileper connection, demuxed by msgId) — the previous 1-request-per-worker model capped throughput at ~1 MB/s on high-RTT paths. Measured ~10x improvement (0.3 → 3+ MB/s sustained). Connection count kept low because DCs drop many simultaneous fresh connections; pipelining supplies the concurrency instead. - Transient reconnect errors ("reconnect: pending request invalidated", "Not connected", timeouts) are now retryable in the part-fetch loop instead of fatally aborting the whole range download.
Future.wait(eagerError: false)on part fetches so a fast-failing part can't orphan sibling in-flight parts (which previously surfaced as an unhandled async exception during a reconnect storm)._countWorkersretuned; retries per part raised to 20 with capped backoff.
crypto #
- AES-IGE encrypt/decrypt rewritten to be allocation-free per block (was
~3
Uint8Listallocations per 16-byte block → ~96k allocations + heavy GC per 512 KB chunk on the single Dart isolate, a major source of video-player jank / frozen spinners). Verified byte-identical to the previous output.
0.2.0 #
Streaming engine release. Three deep-audit rounds against a gogram reference (150+ raw findings, ~80 confirmed, ~50 fixed).
Layer + codegen #
- Bumped Telegram API to layer 227 (from TDesktop dev).
- Fixed
tool/tlgen.darthandling of{X:Type}generics and!Xtemplate refs.
File streaming #
- New
client.downloadStream(location)returnsStream<Uint8List>(in-order, parallel workers under the hood). - New
client.downloadRange(location, start, end)for arbitrary byte ranges with parallel workers, FILE_MIGRATE + FILE_REFERENCE_EXPIRED handling. - New
client.streamMessage(peer, msgId)andclient.downloadMessageRange(peer, msgId, start, end)— resolve media via the built-in cache and yield bytes. - New
TelegramFileStreamServer— local HTTP server that publishes any Telegram file as a URL with full HTTP Range 206 support, HEAD, CORS, seek-aware prefetch, per-entry LRU chunk cache, and per-request cancellation on client disconnect. Feed the URL tomediakit/video_player/<video>. server.publishMessage(peer, msgId)returns a stable URL; the chunk cache lives across HTTP requests.server.warmup(dcId, workers)pre-opens cross-DC senders so first fetch doesn't pay exportAuth latency.MediaLocationCache— LRU keyed by(peerId, msgId)→ResolvedMedia(location, size, mime, dcId, fileName, duration)with 20h TTL under Telegram's 24hfile_referencewindow.
Session + peer persistence #
- Peer cache (users, channels, chats, usernames) now serialized inside
SessionDataand reloaded on next start — no more re-resolve after app restart. - Atomic session write (unique tmp filename + POSIX rename, no delete-then-rename data-loss window).
- Async debounced flush (2s window) + guaranteed flush on
close(). - String session format now byte-compatible with gogram (
1BvE+ RawURL base64 JSON; legacy1BvXalso parsed correctly for binary keys via latin1). SessionData.dcIdalways round-trips.
Reconnect + transport #
_ensureReadywaits for in-flight reconnect (bounded by clienttimeout); no more racing double-dials._reconnectis single-flight, uses persisted auth key, exponential backoff with jitter.- Ping is fire-and-forget — no leaked pending completers.
bad_msgcodes 32/33/48 drain all pending requests and letinvoke()transparently retry on the fresh session.bad_server_salttransparently retries only the affected request (was killing all pending).- Terminal auth errors (AUTH_KEY_UNREGISTERED, etc.) disable autoReconnect; a subsequent successful
loginBot/signIn/checkPasswordre-enables it. - Transport now tracks
lastReadAt;_ensureReadyforce-reconnects if the socket has been idle >90s (catches silently-dead sockets after Android backgrounding). - Transport
close()wakes stuck reads instead of hanging. - Read-idle timeout on transport (75s) surfaces silently-dead peers.
File upload/download #
- Upload closes RAF only after all worker futures resolve (was closing while siblings still reading).
chunkSizevalidated as power-of-2 multiple of 1024 that divides 512 KB.- FLOOD_WAIT respected per part.
- Retries bounded, non-retryable errors (StateError, UnimplementedError) fail fast instead of looping 5 times.
- Parallel
downloadStreamstops correctly on last chunk (not on any short chunk).
Crypto + TL correctness #
- TL strings are now UTF-8 (was UTF-16 code units — broke every non-ASCII payload).
bigIntToBytesfixed: big-endian left-pad, high-order trim (was truncating low bytes / padding wrong side).auth_keyleft-padded to exactly 256 bytes.writeBytesrefuses payloads > 16 MiB instead of silently truncating.
Android/Flutter #
- No default relative
sessionFile— you must pass an absolute path (frompath_provider). - Login callback prompts throw a clear error on mobile instead of using stdin.
- SIGINT handler uses logger instead of print.
- Everything runs on
dart:ioonly — nodart:mirrors/dart:ffi/isolates. Seeexample/flutter_engine.dartfor a ready-to-dropTgEnginesingleton.
Cache correctness #
- Access hash preserved when server sends minimal peer without hash.
- Reverse username mappings pruned on rename.
- LRU eviction (default 100k users / 20k channels) prevents unbounded growth in long-running engines.
getDifferencebacks off on FLOOD_WAIT instead of hammering every 3s.
0.1.3 #
- Fixed DC migration loop: migrate now kills the old poll loop before reconnecting, preventing duplicate pollers and repeated migrations.
- Migration depth capped at 5 (matching gogram) to prevent infinite recursion.
- Poll loop and reconnect logic bail immediately during active migration instead of racing.
0.1.2 #
- Fixed reconnect-spam loop: long-poll TCP reads no longer time out at 15s and trigger false reconnects.
- Reconnect is now single-flight (mutex-guarded), capped at 20 attempts, with exponential backoff (1s → 30s max).
- Poll loop runs once per client lifetime — no more concurrent pollers spawned per reconnect.
0.1.1 #
- Shorter pub.dev description.
- Replaced stub example with a real bot quickstart (inline buttons + callback handler).
- Bumped
pointycastleto^4.0.0andlintsto^6.0.0. - Added dartdoc coverage on generated TL types and the main client surface.
0.1.0 — first publish #
Initial release. Pure-Dart MTProto client against Telegram API layer 225.
- Auth: bot tokens, phone + OTP + 2FA (SRP), string + file sessions
- Messaging: send/edit/delete/forward/pin, Markdown + HTML parse modes, inline button builder
- Updates: socket push + getDifference polling, dedup, auto-reconnect with backoff
- Files: multi-worker parallel up/download, cross-DC sender pool with caching
- DC migration: transparent on USER/PHONE/FILE/NETWORK_MIGRATE
- Conversation API, channels admin ops, participant iteration, bot commands, callback/inline answers
- Logger with TRACE/DEBUG/INFO/WARN/ERROR levels
- 64 tests: 55 unit + 9 live against Telegram