feat: support concurrent Reddit accounts

This commit is contained in:
jjbrunton committed 2026-08-08 19:00:38 +01:00
1 parent d819a0f1e6
commit 16a31335d0
51 files changed
+3241 -250

No files matched your search

+59 -7
View File
@@ -4,6 +4,7 @@ import crypto from 'crypto';
import { z } from 'zod';
import type { Logger } from '../utils/logger.js';
import type { ActiveChat, ChatMedia, ChatMessage } from './chat.js';
import type { AccountContentRepository } from '../database/account-repositories.js';
export interface MediaDownloadResult {
buffer: Buffer;
@@ -15,6 +16,7 @@ export interface MediaDownloader {
}
export interface MediaRecord {
accountId?: string;
id: string;
chatUsername: string | null;
chatRoomId: string | null;
@@ -59,13 +61,16 @@ export class MediaService {
private downloader: MediaDownloader,
private logger: Logger,
mediaRoot?: string,
private readonly databaseStore?: AccountContentRepository,
private readonly getAccountId: () => string | null = () => null,
) {
this.mediaRoot = mediaRoot ?? path.join(process.cwd(), 'data', 'media');
this.indexPath = path.join(this.mediaRoot, 'index.json');
}
async attachMedia(chat: ActiveChat, messages: ScrapedChatMessage[]): Promise<ChatMessage[]> {
await this.loadIndex();
const databaseBacked = Boolean(this.databaseStore && this.getAccountId());
if (!databaseBacked) await this.loadIndex();
const updated: ChatMessage[] = [];
for (const message of messages) {
@@ -79,11 +84,26 @@ export class MediaService {
});
}
await this.saveIndex();
if (!databaseBacked) await this.saveIndex();
return updated;
}
async getMediaItems(): Promise<MediaRecord[]> {
const accountId = this.getAccountId();
if (this.databaseStore && accountId) {
return this.databaseStore.listMedia(accountId).map((item) => ({
accountId: item.accountId,
id: item.mediaId,
chatUsername: item.chatUsername,
chatRoomId: item.chatRoomId,
sender: item.sender,
timestamp: item.timestamp,
originalUrl: item.originalUrl,
fileName: item.fileName,
relativePath: item.relativePath,
createdAt: item.createdAt,
}));
}
await this.loadIndex();
return (this.index?.items || []).slice().sort((a, b) => b.timestamp - a.timestamp);
}
@@ -133,11 +153,23 @@ export class MediaService {
// Try the full-resolution URL first, fall back to original
const fullUrl = this.resolveFullMediaUrl(url);
const id = crypto.createHash('sha1').update(fullUrl).digest('hex');
const accountId = this.getAccountId();
const id = crypto.createHash('sha1').update(`${accountId ?? 'legacy'}\0${fullUrl}`).digest('hex');
if (this.databaseStore && accountId) {
const existingRecord = this.databaseStore.getMedia(accountId, id);
if (existingRecord) {
try {
await fs.access(path.join(this.mediaRoot, existingRecord.relativePath));
return { ...existingRecord, id: existingRecord.mediaId };
} catch {
this.databaseStore.deleteMedia(accountId, id);
}
}
}
const existing = this.index?.items.find((item) => item.id === id);
if (existing) {
// Verify the file still exists on disk
const existingPath = path.join(this.mediaRoot, existing.fileName);
const existingPath = path.join(this.mediaRoot, existing.relativePath);
try {
await fs.access(existingPath);
return existing;
@@ -165,13 +197,17 @@ export class MediaService {
}
const fileName = `${id}${extension}`;
const relativePath = fileName;
const fullPath = path.join(this.mediaRoot, fileName);
const accountDirectory = accountId
? crypto.createHash('sha256').update(accountId).digest('hex').slice(0, 16)
: '';
const relativePath = accountDirectory ? path.join(accountDirectory, fileName) : fileName;
const fullPath = path.join(this.mediaRoot, relativePath);
await fs.mkdir(this.mediaRoot, { recursive: true });
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, download.buffer);
const record: MediaRecord = {
accountId: accountId ?? undefined,
id,
chatUsername: chat.username || null,
chatRoomId: chat.roomId || null,
@@ -183,6 +219,22 @@ export class MediaService {
createdAt: Date.now(),
};
if (this.databaseStore && accountId) {
this.databaseStore.upsertMedia({
accountId,
mediaId: id,
chatUsername: record.chatUsername,
chatRoomId: record.chatRoomId,
sender: record.sender,
timestamp: record.timestamp,
originalUrl: record.originalUrl,
fileName: record.fileName,
relativePath: record.relativePath,
createdAt: record.createdAt,
});
return record;
}
if (!this.index) {
this.index = { version: 1, savedAt: Date.now(), items: [record] };
} else {