feat(avatars): keep every profile photo each account saw and show earlier ones - #479
Conversation
…lier ones
chats.avatar_photo_id was one overwritten pointer, so the archive forgot every
earlier photo and could not tell a seen removal from never recorded.
Migration 031 adds the append-only avatar_history table; upsert_chat appends a
row whenever the recorded id changes (a removal is a NULL row). The viewer
lists the history at /api/chats/{ref}/avatars, serves an earlier photo with
?photo_id= only when this account recorded it, answers a seen removal with a
404 instead of another account's newest file, and shows a Previous photos row
in the info panel.
|
Warning Review limit reachedNext included review available in 14 minutes. View limit detailsLimit details: You’ve used all 2 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: GeiserX/Telegram-Archive/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🐳 Dev images published!
The dev/test instance will pick up these changes automatically (Portainer GitOps). To test locally: docker pull drumsergio/telegram-archive:dev
docker pull drumsergio/telegram-archive-viewer:dev |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #479 +/- ##
==========================================
- Coverage 94.96% 94.94% -0.03%
==========================================
Files 29 29
Lines 12318 12396 +78
==========================================
+ Hits 11698 11769 +71
- Misses 620 627 +7
🚀 New features to boost your workflow:
|
|
🐳 Dev images published!
The dev/test instance will pick up these changes automatically (Portainer GitOps). To test locally: docker pull drumsergio/telegram-archive:dev
docker pull drumsergio/telegram-archive-viewer:dev |
…otos Semantic port of upstream GeiserX#469/GeiserX#479/GeiserX#481 for one account per archive (no account dimension; each account's stack has its own avatars). - chats.avatar_photo_id (migration 018) records the photo currently seen. The viewer serves that file instead of whichever avatar file is newest, falls back to the newest file when nothing is recorded or the file has not landed yet (not cached, so it shows as soon as it downloads), and a recorded removal shows no avatar. - avatar_history (018) is append-only: upsert_chat adds a row whenever the recorded photo changes, in a SAVEPOINT so a failure never aborts the chat update; a removal is a NULL photo id. "min" entities, which omit the photo, record nothing. - GET /api/chats/{id}/avatars (scoped like the chat): the current photo and every previous one on disk, dated by history where sighted. The Chat Info panel shows the real photo and a "Previous photos" strip. The chat list resolves removals with one history query per page, and the single chat endpoint now carries avatar_url too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The archive kept every avatar file but only one pointer per account and chat (
chats.avatar_photo_id), and the backup overwrote it on every change. So it forgot which photos an account had seen before, and when the pointer was empty the viewer could not tell "this account saw the photo removed" from "never recorded", and served another account's newest file for both.Fix
avatar_history(account, chat, photo id or NULL, seen_at) with one index on (account_id, chat_id, seen_at). It is idempotent both ways, adds no stamping rung, and theAvatarHistorymodel matches it (schema parity is green on SQLite and on PostgreSQL 17).upsert_chatappends a row, in the same transaction, wheneveravatar_photo_idis in the payload and differs from the value stored for that account and chat. A missing chat row counts as stored None. There is no unique constraint: 1111, then 2222, then 1111 again is three rows, and a removal is a NULL row. The listener's partial upserts (no key) write nothing. The insert runs in a savepoint like_record_message_version, so a failure there never loses the chat upsert.DatabaseAdapter.get_avatar_history(chat_id, account_id=)returns the rows newest first (seen_at, then id).GET /api/chats/{ref}/avatarslists them asphoto_id,seen_at,url(/media/avatar/{ref}?photo_id=N, None for a removal) andavailable(file on disk). It resolves throughrequire_chat, so it has the same scoping as the other chat routes, and it never returns a chat id./media/avatar/{ref}?photo_id=Nserves that exact file only when this account recorded N for this chat (the current id or a history row), and 404s otherwise, with no newest-file fallback. It does not read or write the 5-minute path cache, so it cannot change the default answer.photo_id: if the recorded id is None and the newest history row is a removal, the route answers 404. With no history rows at all it keeps the newest-file fallback. The sender avatar route (/media/avatar/{ref}/{message_id}) applies the same removal rule, since it reads the same pointer.?download=1.avatar_photo_idis no longer listed as known debt; the archive principle now namesavatar_history.What it deletes / overwrites / forgets
avatar_history, as every downgrade drops what its upgrade added.chats.avatar_photo_idis still overwritten in place, but every value it had is now kept inavatar_historyfirst.updated_at, which is when that photo was last confirmed, not when it was first seen. A photo that changed before 029 or before this upgrade was never recorded and cannot be recovered.Decisions to push back on
chatsand skips chats that already have history, so re-runs andcreate_all()databases stay safe.delete_chat_and_related_datadoes not removeavatar_historyrows. Chat exclusion leaves avatar files on disk too, so it now leaves their history as well. If exclusion should purge this table, that is a one-line delete plus a test.SELECT ... FOR UPDATEbefore the upsert. On PostgreSQL this makes two writers changing the same chat's photo at once wait for each other. SQLite ignores it and serializes writers anyway. Duplicate rows would be harmless, since nothing is unique.Tests:
tests/test_avatar_history.pycovers the migration, the write and read paths on both backends, the API, both avatar routes and the panel row (run under node). Each test was checked with a one-line mutant that turned it red and went green again once the line was restored.