github GeiserX/Telegram-Archive v9.0.0

3 hours ago

The archive keeps more of what Telegram changes. An edit that replaces a photo or file keeps the old one, reactions keep every state the archive saw, and polls and link previews keep their later states. Locations, venues and contacts show as cards, the chat list shows each chat's newest message, and both chat exports list every kept version and media. check-media finds media files that are gone and puts them back, and backfill-details fills details older releases did not store. Several changes break scripts and API clients: read Upgrading to 9.0 first. The first start runs migrations 034 to 038.

Added

  • Both chat exports list each message's media and every kept version, and say which messages were deleted or edited. The viewer's Export chat and telegram-archive export give each message a media list, its current media with media_id, type, file name, size, MIME type, width, height and duration, and a versions list, every earlier version the archive kept of it, oldest first, whatever the version's date. Each version has text, date, captured_at, source, entities, rich_message and media: the earlier media an edit replaced, under the version it was shown with, paired as the edit history pairs them. Earlier media with no text version of its moment is listed as its own entry, with text null and media_only true. No file path and no file leaves the archive. The viewer's export also gains is_deleted, deleted_at, edit_date and edit_hide, which the command already had, so a deleted message no longer reads as a live one and an edit time moved only by a reaction no longer reads as an edit. A transcript of a replaced voice note used to name a media the file never listed. Now each transcript's media_id matches a media on its message or under one of its versions. Each export reads its messages, versions, media and transcripts from one snapshot, so a backup running meanwhile, the voice and audio twin cleanup included, cannot make them disagree. The viewer's export still streams its messages one at a time however long the chat, after reading the chat's transcripts for the date window up front, and keeps its date window and its access rules. It stops with an error rather than write a message without its versions. See Export.
  • Locations, venues, live locations and contacts show as cards. The viewer draws a location as a card with a pin, its coordinates and their accuracy, a venue with its name and address, a live location with the sender's picture and when the archive last saw a position, and a shared contact with initials, name and phone, with buttons to call and to copy. A location card opens the place on OpenStreetMap in a new tab: it needs no account and carries no ads or ad trackers, and the viewer loads no map tiles and sends nothing until you click. A live location card never says the share is live now, since the archive does not follow it as it moves. Replies, the pinned bar and a folded deleted message name them as Location, Live location and Contact, and a reply to a venue reads Location, <name>. A message archived before these details were kept shows its card with Details not archived, and the empty placeholder file an older release left on some of these messages is no longer offered as a download. The info panel no longer lists that file either, and a poll whose details are not archived shows a Poll chip that says so, in place of a file placeholder. These cards replace the venue and live location chips. The viewer's chat export carries the details as media_payload, and /api/chats/{ref}/messages adds reply_to_media_title for a reply to a venue. See Locations and contacts.
  • backfill-details fills old locations, venues, live locations, contacts and polls, and the hidden-edit flag. A new command asks Telegram again for archived messages of these kinds whose details are missing, and for messages archived before this release with an edit time, no hidden-edit flag and no kept earlier version, each message once, 100 per request with a pause between requests, waiting out a FloodWait. It adds only the missing key under raw_data and never replaces text, dates, reactions or a detail already stored. Chats and messages Telegram no longer serves are skipped and counted. It also clears the placeholder path that releases up to 7.28.0 left on location, contact and poll rows. A path is cleared when the details are stored, or when the file is missing, empty or a broken link. A contact's vCard file is read into its details first when Telegram no longer serves the contact. Any other file keeps its path. The row stays and no file on disk is changed. When the media folder is missing or empty where the command runs, every path is kept, and so is the path of a message Telegram did not answer because of an error. A FloodWait too long to wait out stops the run, which says so; run it again later. It stores the hidden-edit flag only when Telegram returns the edit time the archive holds, so a reaction loses its pencil and a real edit keeps it; a message with a later edit time is counted and left for the next backup. It is a dry run unless given --apply. The rows still missing details or a flag are the work list, so a stopped run resumes when run again and a second run fills nothing. No migration runs. See Fill old locations, contacts, polls and edit flags.
  • Reactions taken back show in the bubble, partial drops and returns included. The archive already kept a reaction that was taken back, but the viewer showed only the live ones. After the live chips, a quiet chip with an undo arrow and a count opens the reactions taken back, each emoji with its latest drop and when the archive noticed it. A count that drops without reaching zero reads as how many went of how many there were, "2 of 7 · 11:14", and a reaction taken back and given again stays in the list with when the archive saw it again, "1 · 11:08, back 11:12". A live reaction frame follows the same rule. The chip is a button, reachable by keyboard and touch, and its text keeps 4.5:1 in every theme. /api/chats/{ref}/messages returns them beside reactions as removed_reactions, one entry per emoji from the reaction history: count (how many went), count_before, removed_at and back_at. An emoji that came back is in both lists. They come from the same read as the messages, so every viewer restriction applies. See Reactions, edits and deletions and Messages.
  • What changed for one chat. More actions, What changed in this chat and the same row in the chat's info panel open the What changed feed narrowed to that chat, with the chat as a chip whose cross widens it back to every chat. For a channel or group that several accounts hold, it lists what any of them captured, each change once. A private chat lists only the conversation of the account you opened it from. The Deleted messages and Edited messages rows keep opening the chat's own Deleted only and Edited only modes. /api/changes takes chat_ref, a read-only filter that keeps the viewer's chat and account restrictions: a chat the viewer cannot see answers 404, like an unknown one. See What changed.
  • The chat list shows each chat's newest message. The second line of a chat row is now the newest message not deleted in Telegram, the way Telegram's own list shows it: the sender's first name first in a group, You: for the account's own message, and a word for a message with no text, such as Photo, Voice message, Location or Contact. A poll shows its question. A message deleted in Telegram is skipped in the list and still shows in the chat with its mark. The kind of chat and its member count move to the line's tooltip and the info panel, and fill the line when a chat has no message to show. /api/chats returns the preview with every row as preview: the text on one line and cut to 100 characters, the sender's label, the kind, the date and whether the account sent it. It is read in one query per page from the copy of the chat the row opens, under the same restrictions as the chat's messages, so a viewer limited to some accounts, a share link and a login without downloads see only what they could open. The chat's last message date and the order of the list are unchanged. See The chat list preview.
  • The messages list and both exports return the reaction history. /api/chats/{ref}/messages, the viewer's Export chat and telegram-archive export give each message a reaction_history list: every state of its reactions the archive kept, oldest first, with emoji, count, previous_count, observed_at and source. On the messages list a busy post returns its 20 newest states, plus each emoji's newest state, latest drop and return, and reaction_history_omitted says how many older states it left out; the exports return every state. It comes from the same reads as the rest of the message, so every viewer restriction applies, and each export reads it from the same snapshot as the messages. See Messages and Export.
  • Reactions taken back in What changed. The filter gains Reactions taken back, unticked until you tick it, since reactions come and go far more often than deletions and edits. Each card shows the message with the bubble's dashed chip, the emoji and how many went, "2 of 7" when some stayed. The cards read from the reaction history, which also holds the removals kept before 9.0, under the same restrictions as the rest of the feed, and What changed for one chat lists them too. /api/changes returns them as kind reaction with reactions=true, and leaves them out otherwise. See What changed and Search, tags and the change feed.
  • telegram-archive check-media finds media files that are gone, and puts them back. It checks every downloaded media row of every account and counts broken links (a link into media/_shared whose shared file is gone) and missing files (nothing at the row's path). With --repair it puts back each file a copy of which is already on disk, never replacing anything, and marks the rest not downloaded so the next backup run fetches them from Telegram. Without --repair it changes nothing and exits 1 when something is missing. It reads the database and stats each row's path, and never walks the media folder. See check-media.
  • Polls and link previews keep their later states. A poll's votes, results and closing, and a link preview Telegram fills in or changes, were never seen after the first capture. Each later state is now added to a new table, message_snapshots, with when the archive saw it and the path that saw it: the listener from edit events and poll updates, the sync and a backup from their reads. A state equal to the newest kept one adds nothing, and nothing is ever updated or removed except by DELETION_MODE=hard or deleting a chat. The viewer shows the newest state: the poll's results and total, Final results once closed, and the newest card. A quiet "updated" with the time follows when it differs from the first capture. /api/chats/{ref}/messages returns the newest state of each kind as snapshots, and both exports list every state per message beside its media, versions and reaction history, under the same chat and account restrictions, and the command's statistics count them as total_message_snapshots. telegram-archive merge and the move to PostgreSQL copy them. Live locations are not followed. Upgrading runs migration 038. See Poll and link preview snapshots.
  • A backup run says when it meets media files behind broken links. Without VERIFY_MEDIA, nothing told an operator that rows said downloaded while their link into media/_shared pointed at nothing. A backup run that reads such a row now ends with one warning: how many it met and the telegram-archive check-media command to run. It counts only the rows the run reads, scans nothing more, and logs no path, chat id or file name. Run check-media once after upgrading to 9.0.

Changed

  • Clicking a chat's name in the header opens its info panel, for every kind of chat. It used to work only in a private chat, where it opened a smaller details window, and did nothing in a group, a channel or a topic. Now the name, and on a phone the photo beside it, open the info panel for every chat, as a tap on the title does in Telegram. It is one button inside the chat's heading, reachable with the keyboard, and closing the panel puts focus back on it. With the panel already open, it scrolls the panel back up to the chat. The chat information button still opens and closes the panel. The details window still opens from a sender's photo in a message. See Info panel.
  • Breaking: both chat exports drop the flat message_versions list. Each message's versions is now the one complete list. The flat list was picked by the version's own date, so it could hold versions of messages outside the window, and in the command's file it did not say which account kept each version. A script that read message_versions must read each message's versions. In the command's file, total_message_versions now counts the entries under the messages. See Upgrading to 9.0.
  • A replaced photo shows at once in an open chat. The listener's live edit frame now carries the message's current media when the edit replaced the photo or file, in the shape the messages route gives it: the {message_id}_{type} key, the ref-addressed URL with its ?v= cache key, and no URL or path for a login whose downloads are off. The viewer swaps the new media into the bubble at once, a video, a GIF or a round video with a new player so the new clip plays, for an older message too, and while a search, a filter, a jump to a pinned message or the pinned messages list is open, which the refresh of the newest 50 messages never reached. The pinned messages list now takes edits of its text as well. When the new file is not downloaded yet, the frame carries the pending row, as a reload would show it. A frame that would pass PostgreSQL's notification size limit drops the formatting first, then the media, and never the text. See Live updates and notifications.
  • Breaking: the live new_message frame's media has the messages route's shape. Its nested media used to pass on the storage id, which spells the chat id, and had no URL, so a live row showed no photo until the next refresh. It now carries the {message_id}_{type} key as id and a url, and a login whose downloads are off gets no URL and no path. See Live updates over WebSocket.
  • Breaking: a message with no formatting is stored with an empty list. The backup, the listener and the sync stored formatting only when a message had some, so a missing key meant either "none" or "archived before formatting was kept", and formatting added later to a plain message was filled in silently. A message with no formatting is now stored with entities: [] in raw_data, and an edit that removes all formatting leaves that list, so bold added to a plain message is an edit like any other formatting-only edit (see Fixed). A script that took a missing entities key to mean "no formatting" should treat [] the same way. A message archived before this release keeps the old rule: its formatting is unknown, is filled in without a version, and a read that finds it still plain leaves it unknown. An empty list never replaces other extras another writer stored. See Edits.
  • Breaking: the listener keeps an edit of a message it has not stored yet, and counts it apart. An edit to a message that was not in the archive was dropped, so the newest text waited for the next backup run, or was lost when no run read the message again. With LISTEN_NEW_MESSAGES on, the listener now stores the message with its current text and edit time, the way it stores a new message, and quietly: the message is not new, so no notification is sent, no message_edited webhook fires and no live frame goes out. When a backup run read the older text in the same moment, that text is kept as an earlier version. The listener's stop statistics count such an edit as Stored as new messages, no longer as a skipped edit. An edit the listener could not store either, such as with LISTEN_NEW_MESSAGES=false, still counts as skipped. See Edits and Checking that it runs.
  • The optional akou service in docker-compose.yml and the transcription guide pin drumsergio/akou:0.5.4. akou 0.5.4 writes the final transcript with Qwen whenever Qwen is downloaded and shows how far a long pass is. Its HTTP API only adds fields, so the archive needs no change. See Transcription.
  • What changed for one chat opens on All time, and its period is not remembered. It used the period remembered for the feed for every chat, so a reader who had looked at the last 24 hours saw only a day of the chat's changes, and a period picked in the chat's feed became the feed for every chat's period too. A chat's feed is short, so it now opens on All time, and a period picked there lasts while it is open. The feed for every chat keeps its own remembered period. See What changed.
  • Breaking: raw_data.poll and raw_data.webpage keep the first capture. A backup that read a message again used to replace them with what it saw, so the first capture was lost. They now stay as first captured, a missing one is filled once, and each later state goes to message_snapshots. A reader that wants the newest poll results or preview reads snapshots in /api/chats/{ref}/messages or in either export. See Poll and link preview snapshots.
  • Breaking: MASS_OPERATION_THRESHOLD counts deletions only. Edits are no longer limited (see Fixed), so a threshold set to slow down edits no longer touches them. See Mass-operation protection.
  • Breaking: transcript presses are limited for every login but the master. Past TRANSCRIPTION_ASK_RATE_LIMIT presses in 10 minutes from one client (30 by default), or while TRANSCRIPTION_ASK_MAX_OPEN pressed files (50) wait for the backup, the transcript POST routes answer 429 with Retry-After. In an open viewer a press on a file already transcribed or skipped returns that result and queues nothing. A script that presses many files must handle 429, or set the limits to 0 (see Security). See Transcripts.

Fixed

  • A media volume that is not mounted no longer marks files as not downloaded. The transcription drain, check-media --repair and VERIFY_MEDIA marked a file not downloaded when its path held nothing. With the media volume not mounted, or a network share that dropped, every file reads that way, so the drain marked a few dozen files each run, their downloads failed until they gave up, and once the volume was back they stayed hidden from the gallery with nothing to bring them back. A file is now marked only when it is provably gone: the media folder is there and not empty, and the row's folder exists under it. Otherwise the row stays as it is: the drain records the file as missing, as 8.18 did, VERIFY_MEDIA skips the run with a warning, and a failed download of a file a row already holds leaves the row's flag alone. check-media says the media folder is not visible and exits 1 without changing anything. check-media --repair also marks downloaded again a row marked not downloaded whose own file is back at its path, so rows a past outage left behind come back. See A missing shared file and check-media.
  • A replaced photo is found after a reaction, and an old file name no longer freezes a download. A reaction moves Telegram's edit time and hides the edit before it, and a replacement was taken only from an edit Telegram shows. So a photo swapped while the listener was away was never found once someone reacted, and a pending download of it was skipped on every run until it gave up. A hidden edit newer than the edit the archive holds now counts: the old media is kept as a version and the new one downloaded. A row whose Telegram id was only guessed from an old file name, such as a name that starts with a long number, no longer counts as holding other media, so its download goes ahead. See Edits.
  • A poll update for a poll the archive does not hold looks it up every 6 hours, not every 10 minutes. The lookup reads the whole messages table, and such polls are mostly in chats the archive does not keep. A poll the listener stores replaces the cached miss at once. See Polls and link previews.
  • A repaired disk or server is enough for transcription to resume. Every failed attempt used to count toward the limit of three, whatever its reason, so a file that failed three times because its shared file was missing, or because the transcription server's engine was down or lost the job, was never tried again after the repair. Only failures about the file itself now count toward the three. A file that was missing or unreadable is checked on every drain and sent once it is back, and nothing new is stored while it stays broken. A failure that was the server's (engine_unavailable, models_missing, model_download_failed, not_found, expired, invalid_job, invalid_json) is retried once the server has finished another transcript since. Until then each drain sends one such file as a test, after a wait that doubles with each failed attempt, so a server that is still broken adds one row a drain, not one per file. Ten failed attempts in all still end the retries. The rule reads the reasons already stored, so files that failed this way before the upgrade are picked up too, with no migration and no row deleted. See Retries.
  • A location or a shared contact keeps what it says. The archive kept only the kind of a plain location or a shared contact, not its coordinates, name or phone. The backup and the listener now keep a location under raw_data.geo (lat, long, accuracy_radius) and a contact under raw_data.contact (first_name, last_name, phone_number, vcard, user_id), through the same builder that keeps venues, live locations and polls. Venues gain venue_id, venue_type and accuracy_radius, and live locations heading, accuracy_radius and at, the time of the position. The listener now keeps polls too, which only the backup kept before. A Telegram Desktop JSON import keeps locations, venues, live locations, contacts and polls under the same keys. Messages archived before this release keep only their kind until backfill-details fills them in. None of these details is ever written to the logs. See Media.
  • A later read no longer drops a poll, a location or a contact. A message upsert replaced raw_data whole when the new read carried anything, so import --merge of a forwarded message dropped an archived poll, venue or live location. A payload the archive holds and the new read lacks is now kept. A newer read from Telegram still replaces a location, venue or contact payload. A poll keeps its first capture, and its later tallies go to snapshots. An import never replaces a payload the archive holds, since an export carries less than Telegram served (no poll option ids, no vCard, no venue provider), and only fills one the archive lacks. A live location keeps every position the archive's reads saw: the newest is on top and the others are listed under earlier, and a read with no point never replaces one.
  • A partial reaction snapshot no longer marks a reaction as taken back. Telegram can send reactions flagged min, which may leave out the account's own reaction. The live reaction handler and the backup's message read reconciled them anyway, so the account's own reaction could show as taken back when nobody took it back. The backup now stores a min snapshot only for a message that has no reactions stored yet, where it cannot mark anything as taken back, and skips it otherwise. The listener skips a min update: it is not written and the viewer gets no live change for it. In both cases the reactions catch up at the next full snapshot, from a backup or the re-sweep (REACTION_RESWEEP_DAYS).
  • A burst of reactions no longer drops real edits. Telegram sends many reaction changes as edit events. The listener's mass-operation protection counted them together with edits and deletions, so a burst of reactions could use up a chat's budget and the next text edits in that chat were dropped for 30 seconds. Edits are no longer rate limited, since an edit keeps the earlier text and its formatting as a version. Deletions keep the same limit, and now have the whole budget to themselves, since edits no longer use it up. MASS_OPERATION_THRESHOLD now counts deletions only. See Mass-operation protection.
  • A reaction no longer marks a message edited. Telegram moves a message's edit time when only its reactions change, and flags that edit as one not to show. The archive stored the time and dropped the flag, so a message first archived after a reaction showed a pencil with no earlier text. It now keeps the flag in messages.edit_hide, beside edit_date, and a message with a hidden edit and no earlier text kept is not edited in the bubble, the Edited only list or the chat's edited_messages count. Both exports, the messages list and the live edit frame carry edit_hide. Messages archived before this release have no flag and read as edited until a backup reads them again: a re-scan, a gap fill or a SYNC_DELETIONS_EDITS pass fills the flag when the edit time is the same, and changes nothing else. backfill-details --apply fills it for every chat in one pass. Upgrading runs migration 034. See Reactions, edits and deletions.
  • An edit keeps the earlier formatting, and an edit of the formatting alone is an edit. Each version in message_versions now keeps its formatting in a new entities column, and a Rich Text Editor message's block tree in rich_message, so an edit no longer replaces bold, links, spoilers or headings in place and loses them. An edit that changes only the formatting, which used to leave no version and no pencil, now keeps a version and moves the edit time, like any edit. A live edit with other formatting in the same second as the last one counts too, since bots often edit twice a second, and each such edit keeps its own version. A message archived before formatting was captured has none on record, so an edit time that moved with the same text fills its formatting and is not counted as an edit. An edit Telegram hides (a reaction) is still not one, and replaces no formatting, and a version kept after one is dated at the send time. A backup read or an import that is not an edit keeps the archived formatting and only fills it where the archive had none. Each version also names the path that saw it in a new source column: listener, sync, backup or import. /api/chats/{ref}/messages/{id}/versions and both exports return captured_at, source, entities and rich_message per version, and the live edit frame carries the new entities, so an open chat shows a formatting-only edit at once. A frame whose entities would pass PostgreSQL's notification size limit goes without them, and the open chat keeps the formatting it shows. The edit history draws every version with its own formatting, the way the bubble does, marks a formatting-only edit, and says "at least 3 edits" when a version came from the sync, a backup or an import, which read only the text current at that moment, or when the archive first saw the message already edited. Versions from before this release have none of the new columns set, and count the same way, since where they came from is unknown. Upgrading runs migration 035. See Reactions, edits and deletions.
  • An edit that replaces a photo or file keeps the old one and downloads the new one. Telegram lets a sender swap the media of a message. The listener, the sync and a backup read compared only the text, so the old file stayed, the new media was never downloaded, the new caption sat beside the old picture, and VERIFY_MEDIA could fetch the new media into the old row once the old file went missing. The archive now records Telegram's id of each photo and document in media.telegram_file_id, and reads it from the file name for older rows. When a message carries another id, the old media row is kept in a new media_versions table with its file, beside the text it was shown with, and the new media is downloaded into the message's media row under the usual rules (SKIP_MEDIA_CHAT_IDS, MAX_MEDIA_SIZE_MB, the media type filters). An edit that swaps only the media is an edit. VERIFY_MEDIA never moves or refills an old version's file. The edit history shows the earlier media on the version it belonged to, and the versions endpoint returns it as media with a /media/{chat_ref}/{message_id}_v{n} URL, n counting the message's earlier media from 1. The current media's URL changes too, so a browser never shows the old picture from its cache. Only an edit Telegram shows replaces media: a read with no edit time, a reaction (a hidden edit) and a read older than an edit the archive already holds replace nothing, and a link preview's card picture, which Telegram can change for a message nobody edited, is not compared. A download still running when an edit replaces the media never puts the old media back as current, and a file a backup read and downloaded while the listener stored other media is kept as earlier media. VERIFY_MEDIA and the pending downloads move the edit time when they find a replacement, as the sync does. The earlier media keeps its skip reason and when it was first seen, and its transcripts stay in both exports and in search. DELETION_MODE=hard, deleting a chat and SKIP_MEDIA_DELETE_EXISTING remove earlier media with the rest; telegram-archive merge and the move to PostgreSQL copy it. Media imported from a Telegram Desktop export has no Telegram id and is not compared, nor is an older file name that starts with a number shorter than Telegram's ids, such as a phone's timestamp. Upgrading runs migration 036. See Edits.
  • Reactions keep every state the archive saw. A count that dropped without reaching zero, say from 7 to 5, was written over the earlier count, and an emoji that was taken back and given again lost its removal. A new reaction_history table now keeps one row per state of an emoji on a message: count (0 when taken back), previous_count, observed_at and source (listener, backup, or baseline for rows copied when the history began). The listener and the backup add a row whenever a count differs from the newest one kept, and never change or remove a row. It stores counts per emoji, like reactions, never who reacted. DELETION_MODE=hard, deleting a chat and EXCLUDE_DELETE_EXISTING remove a message's history with its reactions; telegram-archive merge and the move to PostgreSQL copy it. Upgrading runs migration 037, which seeds the history from the reactions already kept. See Reactions.
  • A shared media file is no longer lost behind a link, and a lost one comes back. A chat folder link could point at a media/_shared file that was gone while its row still said downloaded. Older code moved a freshly published shared file into one chat's folder when that chat's symlink failed, which left every other link to it pointing at nothing, and migration 013 then pointed legacy channel rows at their marked-id folder while the files stayed in the plain-id one. Nothing noticed: VERIFY_MEDIA trusted every symlink, the download path trusted any entry that existed, and the viewer served the plain-id copy through its legacy fallback. Now a link whose shared file is gone counts as missing everywhere. VERIFY_MEDIA, the transcription drain and the new check-media command restore it from a copy already on disk: the same name elsewhere in _shared, the chat's other id-form folder, or another row with the same content hash. With no copy, the row is marked not downloaded, and the download fills the shared file under the name the link holds, so the link is never rewritten. The transcription drain no longer spends a failed row on a file it can restore or fetch again. A failed symlink now gets a copy and never moves the shared file. Links into a store this process cannot follow, such as git-annex, are still trusted and left alone. See A missing shared file.
  • A removal the operator asked for no longer takes what something else still uses. YOUTUBE_VIDEOS_DELETE_EXISTING deleted a shared file once no row held its content hash, but rows from before content hashing carry no hash, so a file such a row still used could go. It now keeps a shared file while any row or earlier media of any account names the file or holds its hash, or any chat folder links to it. SKIP_MEDIA_DELETE_EXISTING and the YouTube cleanup delete their rows first and then keep each chat-folder file another account's row still names, and EXCLUDE_DELETE_EXISTING keeps a chat's media folder while another account still archives the chat. The sharding migration also repoints links named differently from the shared file they point at, which content deduplication creates, before it moves the file.
  • Shared Media shows a preview on every tile loaded by scrolling, not only on the first page. The tiles of the second and later pages, loaded as you reach the end or with Load more, kept their placeholder for good. The viewer added them to the grid without telling the queue that hands out previews a few at a time. They now join that queue like the first page. See Shared media.
  • The pinned messages list shows a message the way the chat shows it. The pinned-only view draws the pinned list with the chat's own bubble, but the list carried no reactions and none of the reactions taken back, and since this release a pinned poll or link preview would have stayed at its first capture. /api/chats/{ref}/pinned now returns snapshots, reactions, removed_reactions, reaction_history and reaction_history_omitted for each message, read the way the messages route reads them, the history capped the same way, in four batched statements whatever the number of pins. See Messages.
  • scripts/restore_chat.py sends every file of a message again. Since both exports list a message once with all its media, the restore read only the first file, so a message with several files went back with one. It now sends each file as its own Telegram message: the first with the text and each other one after it without text, in the order the exports list media: downloaded first, then by media id. The text is still sent once. A file that hits a flood or slow-mode wait is sent again after the wait, so the files after it are not dropped.

Security

  • A login whose downloads are off no longer receives a shared contact's vCard. The archive now keeps a contact's vCard text under raw_data.contact.vcard, where releases up to 8.18 kept it as a file such a login could not download. The messages route, the pinned list, the date jump and the live new_message frame now leave the vCard out for that login and keep the name, the phone and the rest of the card, which is all the viewer shows. Every other login still gets it. See No-download logins.
  • virtualenv 21.14.2 in the development lockfile. It closes two advisories: seed wheels downloaded without an integrity check, and prompt values written into pyvenv.cfg without sanitizing line breaks. virtualenv comes in only through pre-commit, so the wheel and the Docker images do not change.
  • urllib3 2.8.0. The lockfile, and so the images and the wheel's pinned install, move urllib3 from 2.7.0 to 2.8.0, which closes three advisories: an HTTPS proxy TLS configuration that could be ignored (GHSA-8988-9cw3-xx77), an unbounded chunk-size line read into memory (GHSA-vxq7-64xx-v4gw) and an infinite loop in chunked deflate streaming (GHSA-gh4c-6fx4-qh6g). The archive only reaches urllib3 through requests, in the transcription provider adapters.
  • An open viewer can no longer queue unlimited transcription work. With ALLOW_ANONYMOUS_VIEWER=true anyone could press the transcript button, and a press queues a file again even after it was transcribed or failed, so one visitor could fill the queue without end. In an open viewer a press on a file that was already transcribed or skipped now returns that result and queues nothing, so an anonymous visitor cannot send a finished file to the transcription server again. Logins keep the press that asks again. Presses are now limited per client: TRANSCRIPTION_ASK_RATE_LIMIT (30 per 10 minutes by default). A client is its login session, the proxy user name, or, in an open viewer, the client IP, found as the login rate limit finds it. TRANSCRIPTION_ASK_MAX_OPEN (50 by default) caps the pressed files waiting for the backup; past it, presses are refused until a backup run picks some up. The master is exempt from both limits. Only asks from the last 24 hours on downloaded files count, so asks no backup run picks up cannot hold it full for good. Those older asks stay queued and still run when a backup takes them, so while the backup is stopped the waiting asks can grow by up to one cap a day. Pressing a file already queued returns it and counts against neither limit. Both answer 429 with Retry-After, the viewer shows the reason, and 0 turns either limit off. See Environment variables.
  • The browser asks the viewer again before it shows media from its cache. Originals were sent with Cache-Control: private and no lifetime, and thumbnails and avatars with private, max-age=86400. Their URLs name the chat and the file, not the session, so after a logout, or after a login lost a chat, the same browser could still show them from its cache without asking: thumbnails and avatars for up to a day, originals for as long as the browser's own guess allowed, which can be weeks for an old file. Every original, thumbnail and avatar is now sent with private, no-cache and a validator, and the viewer answers 304 Not Modified to a session that still passes the checks, so a reuse costs no bandwidth. A session that ended gets 401 and a session that lost the chat gets 404. Originals and thumbnails answer 403 to a login whose downloads are off; avatars stay available to it. Logout, and ending every session including your own, also send Clear-Site-Data: "cache". Copies a browser cached before the upgrade can still be reused until their old lifetime runs out, up to a day for thumbnails and avatars; logging out once over HTTPS clears them. The page now reloads when its session ends, so the next login on the same tab no longer sees the chat that was open, its messages or its media. A chat export is sent with private, no-store. See Media.

📋 Full changelog: docs/CHANGELOG.md

Don't miss a new Telegram-Archive release

NewReleases is sending notifications on new releases.