feat: recover missing conversations, fix media attachment, add manual inbox tab
Missing conversations: recipient Reddit accounts that are deleted or suspended left conversations unaccounted for. A new RecipientAccountStatusService polls Reddit for the users behind cached conversations and records their account status durably, with progressive backfill and delta-media fixes in ChatService so history and images are no longer dropped mid-sync. Media: attachMedia now surfaces download failures as MediaAttachmentError so the sync cursor can be retried instead of silently losing an image, recognizes image bytes when Matrix returns a generic content type, and keeps separate gallery occurrences when the same asset appears in more than one event. Manual inbox tab: the conversations list now carries the reply-loop status, so the inbox can separate conversations the operator is driving by hand from the ones automation is still handling. A Manual filter pill and a per-row chip explain why each conversation was handed over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
725b5fc4e6
commit
ac12a3146a
35 files changed
+1164
-129
No files matched your search
@@ -6,6 +6,7 @@ import path from 'node:path';
|
||||
import {
|
||||
AccountContentRepository,
|
||||
RedditAccountsRepository,
|
||||
RedditRecipientAccountStatusRepository,
|
||||
} from '../src/database/account-repositories.js';
|
||||
import { LEGACY_UNASSIGNED_ACCOUNT_ID, runMigrations } from '../src/database/client.js';
|
||||
import { importLegacyAccountContent } from '../src/database/legacy-content-import.js';
|
||||
@@ -54,6 +55,15 @@ describe('multi-account database ownership', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('stores recipient account state case-insensitively and allows later recovery', () => {
|
||||
const statuses = new RedditRecipientAccountStatusRepository(db);
|
||||
statuses.upsert('Alice', 'deleted', 100);
|
||||
expect(statuses.get('alice')).toEqual({ username: 'Alice', status: 'deleted', checkedAt: 100 });
|
||||
|
||||
statuses.upsert('ALICE', 'active', 200);
|
||||
expect(statuses.get('alice')).toEqual({ username: 'Alice', status: 'active', checkedAt: 200 });
|
||||
});
|
||||
|
||||
it('updates matrix credentials in place without touching cookies or status', () => {
|
||||
addAccount('t2_alice', 'alice-bot', 100);
|
||||
|
||||
@@ -196,4 +206,19 @@ describe('multi-account database ownership', () => {
|
||||
fs.rmSync(dataDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('stores separate media occurrences that share one downloaded file', () => {
|
||||
addAccount('t2_alice', 'alice-bot', 100);
|
||||
const base = {
|
||||
accountId: 't2_alice', chatUsername: 'sender', chatRoomId: 'room-one', sender: 'sender',
|
||||
originalUrl: 'mxc://reddit.com/shared', fileName: 'shared.jpg', relativePath: 'shared.jpg',
|
||||
createdAt: 10,
|
||||
};
|
||||
|
||||
content.upsertMedia({ ...base, mediaId: 'event-one', timestamp: 1 });
|
||||
content.upsertMedia({ ...base, mediaId: 'event-two', timestamp: 2 });
|
||||
|
||||
expect(content.listMedia('t2_alice')).toHaveLength(2);
|
||||
expect(content.getMediaByOriginalUrl('t2_alice', base.originalUrl)?.mediaId).toBe('event-one');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user