feat: support concurrent Reddit accounts
This commit is contained in:
1 parent
d819a0f1e6
commit
16a31335d0
51 files changed
+3241
-250
No files matched your search
+89
-11
@@ -39,6 +39,13 @@ import { reportReply } from './web/event-reporting.js';
|
||||
import { RedditSessionImporter } from './services/reddit-session-importer.js';
|
||||
import { AccountAuthContext } from './services/account-auth-context.js';
|
||||
import { ConfigStore } from './config/persistence.js';
|
||||
import { RedditAccountsRepository, AccountContentRepository } from './database/account-repositories.js';
|
||||
import { RedditAccountsService } from './services/reddit-accounts.js';
|
||||
import type { Cookie } from 'playwright';
|
||||
import { ChatCache } from './services/chat/cache.js';
|
||||
import { RedditAccountRuntimeManager } from './services/reddit-account-runtime.js';
|
||||
import { createConcreteRedditAccountRuntimeFactory } from './services/reddit-account-runtime-builder.js';
|
||||
import { importLegacyAccountContent } from './database/legacy-content-import.js';
|
||||
|
||||
export interface AppContainer {
|
||||
config: Config;
|
||||
@@ -47,6 +54,8 @@ export interface AppContainer {
|
||||
|
||||
browserClient: BrowserClient;
|
||||
accountAuth: AccountAuthContext;
|
||||
redditAccountsRepo: RedditAccountsRepository;
|
||||
accountRuntimeManager: RedditAccountRuntimeManager;
|
||||
|
||||
postsRepo: PostsRepository;
|
||||
messagesRepo: MessagesRepository;
|
||||
@@ -97,14 +106,25 @@ export async function bootstrap(
|
||||
// Database
|
||||
const db = existingDatabase ?? createDatabase(config);
|
||||
runMigrations(db);
|
||||
const postsRepo = new PostsRepository(db);
|
||||
const messagesRepo = new MessagesRepository(db);
|
||||
const successfulMessagesRepo = new SuccessfulMessagesRepository(db);
|
||||
const activityLogRepo = new ActivityLogRepository(db);
|
||||
const bootstrapAccountsRepo = new RedditAccountsRepository(db);
|
||||
const bootstrapAccount = bootstrapAccountsRepo.listPublic()
|
||||
.filter((account) => account.status === 'verified')
|
||||
.sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
||||
const bootstrapAccountId = bootstrapAccount?.accountId;
|
||||
const postsRepo = new PostsRepository(db, bootstrapAccountId);
|
||||
const messagesRepo = new MessagesRepository(db, bootstrapAccountId);
|
||||
const successfulMessagesRepo = new SuccessfulMessagesRepository(db, bootstrapAccountId);
|
||||
const activityLogRepo = new ActivityLogRepository(db, bootstrapAccountId);
|
||||
const redditUsersRepo = new RedditUsersRepository(db);
|
||||
const deliveryOutboxRepo = new DeliveryOutboxRepository(db);
|
||||
const conversationStateRepo = new ConversationStateRepository(db);
|
||||
const conversationOutcomesRepo = new ConversationOutcomesRepository(db);
|
||||
const deliveryOutboxRepo = new DeliveryOutboxRepository(db, bootstrapAccountId);
|
||||
const conversationStateRepo = new ConversationStateRepository(db, bootstrapAccountId);
|
||||
const conversationOutcomesRepo = new ConversationOutcomesRepository(db, bootstrapAccountId);
|
||||
const redditAccountsRepo = bootstrapAccountsRepo;
|
||||
const accountContentRepo = new AccountContentRepository(db);
|
||||
const legacyImport = importLegacyAccountContent(db);
|
||||
if (legacyImport.conversations > 0 || legacyImport.media > 0) {
|
||||
logger.info(`Imported legacy unassigned content into SQLite (${legacyImport.conversations} conversations, ${legacyImport.media} media items)`);
|
||||
}
|
||||
logger.info('Database initialized');
|
||||
|
||||
// Browser
|
||||
@@ -112,6 +132,18 @@ export async function bootstrap(
|
||||
const accountAuth = new AccountAuthContext(logger);
|
||||
await browserClient.initialize();
|
||||
|
||||
const storedAccount = bootstrapAccount;
|
||||
if (storedAccount) {
|
||||
const privateAccount = redditAccountsRepo.get(storedAccount.accountId);
|
||||
if (privateAccount) {
|
||||
try {
|
||||
await browserClient.importRedditCookies(JSON.parse(privateAccount.cookiesJson) as Cookie[]);
|
||||
} catch (error) {
|
||||
logger.warn(`Could not restore stored Reddit cookies for u/${storedAccount.redditUsername}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The extension import is the only session acquisition path. A missing
|
||||
// session is recoverable: the dashboard remains available while all
|
||||
// account-bound work waits for a verified dump.
|
||||
@@ -151,7 +183,10 @@ export async function bootstrap(
|
||||
// Matrix credentials come only from a verified extension dump (or its
|
||||
// persisted cache on restart). The backend never opens Reddit Chat to
|
||||
// sniff or refresh a token.
|
||||
let matrixCreds: MatrixCredentials | null = await loadCachedCredentials();
|
||||
const storedPrivateAccount = storedAccount ? redditAccountsRepo.get(storedAccount.accountId) : null;
|
||||
let matrixCreds: MatrixCredentials | null = storedPrivateAccount
|
||||
? JSON.parse(storedPrivateAccount.matrixCredentialsJson) as MatrixCredentials
|
||||
: await loadCachedCredentials();
|
||||
if (matrixCreds) {
|
||||
logger.info(`Loaded cached Matrix credentials (user=${matrixCreds.userId})`);
|
||||
}
|
||||
@@ -185,7 +220,13 @@ export async function bootstrap(
|
||||
// 2. Build the media downloader + service on top of the Matrix client so
|
||||
// `mxc://` URIs resolve through the same auth + rate-limiter pipeline.
|
||||
const mediaDownloader = new MatrixMediaDownloader(activeMatrixClient, logger);
|
||||
const mediaService = new MediaService(mediaDownloader, logger);
|
||||
const mediaService = new MediaService(
|
||||
mediaDownloader,
|
||||
logger,
|
||||
undefined,
|
||||
accountContentRepo,
|
||||
() => accountAuth.getSnapshot().accountId,
|
||||
);
|
||||
|
||||
const indexedMedia = await mediaService.getMediaItems();
|
||||
const receivedImageRooms = new Set(
|
||||
@@ -224,7 +265,7 @@ export async function bootstrap(
|
||||
mediaService,
|
||||
tokenRefresher,
|
||||
usernameResolver,
|
||||
undefined,
|
||||
new ChatCache(logger, undefined, accountContentRepo, storedAccount?.accountId ?? null),
|
||||
messagesRepo,
|
||||
redditUsersRepo,
|
||||
accountAuth,
|
||||
@@ -247,13 +288,42 @@ export async function bootstrap(
|
||||
mediaDownloader.setMatrixClient(client);
|
||||
matrixUsernameResolver.setMatrixClient(client);
|
||||
},
|
||||
switchAccount(accountId) {
|
||||
return chatService.switchAccount(accountId);
|
||||
},
|
||||
},
|
||||
(credentials) => new MatrixClient(credentials, logger),
|
||||
saveCredentials,
|
||||
accountAuth,
|
||||
{
|
||||
save(input) {
|
||||
redditAccountsRepo.upsertVerified({
|
||||
accountId: input.accountId,
|
||||
redditUsername: input.redditUsername,
|
||||
matrixUserId: input.matrixUserId,
|
||||
cookiesJson: JSON.stringify(input.cookies),
|
||||
matrixCredentialsJson: JSON.stringify(input.credentials),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let webServer: WebServer | null = null;
|
||||
const accountRuntimeManager = new RedditAccountRuntimeManager(
|
||||
redditAccountsRepo,
|
||||
createConcreteRedditAccountRuntimeFactory({
|
||||
config,
|
||||
logger,
|
||||
db,
|
||||
isBotRunning: () => webServer?.isRunning() ?? true,
|
||||
}),
|
||||
);
|
||||
const redditAccounts = new RedditAccountsService(
|
||||
redditAccountsRepo,
|
||||
sessionImporter,
|
||||
accountAuth,
|
||||
accountRuntimeManager,
|
||||
browserClient,
|
||||
);
|
||||
if (config.web.enabled) {
|
||||
webServer = new WebServer(
|
||||
config.web.port,
|
||||
@@ -330,6 +400,8 @@ export async function bootstrap(
|
||||
webServer.setBrowserClient(browserClient);
|
||||
webServer.setSessionImporter(sessionImporter);
|
||||
webServer.setAccountAuth(accountAuth);
|
||||
webServer.setRedditAccountsService(redditAccounts);
|
||||
webServer.setAccountRuntimeManager(accountRuntimeManager);
|
||||
webServer.setConversationStateRepository(conversationStateRepo);
|
||||
webServer.setConfigStore(new ConfigStore(db));
|
||||
webServer.setOnConfigChange(() => logger.info('Runtime config updated via dashboard'));
|
||||
@@ -337,6 +409,10 @@ export async function bootstrap(
|
||||
|
||||
// Start background services
|
||||
if (webServer) webServer.start();
|
||||
await accountRuntimeManager.startEnabled().catch((error) => {
|
||||
logger.error('One or more Reddit account runtimes failed to start:', error);
|
||||
});
|
||||
if (accountRuntimeManager.list().length > 0) await browserClient.close();
|
||||
|
||||
// Prime processor with successful examples already on disk.
|
||||
const existingExamples = responseChecker.getSuccessfulExamples(3);
|
||||
@@ -352,6 +428,8 @@ export async function bootstrap(
|
||||
db,
|
||||
browserClient,
|
||||
accountAuth,
|
||||
redditAccountsRepo,
|
||||
accountRuntimeManager,
|
||||
postsRepo,
|
||||
messagesRepo,
|
||||
successfulMessagesRepo,
|
||||
|
||||
Reference in new issue
Block a user