feat: persist settings and enrich conversation context
This commit is contained in:
1 parent
a36723a325
commit
2c100d72b6
84 files changed
+2248
-644
No files matched your search
+33
-38
@@ -24,6 +24,7 @@ import {
|
||||
ChatService,
|
||||
MatrixClient,
|
||||
MatrixMediaDownloader,
|
||||
MatrixUsernameResolver,
|
||||
MatrixTokenRefresher,
|
||||
loadCachedCredentials,
|
||||
saveCredentials,
|
||||
@@ -33,11 +34,11 @@ import {
|
||||
import { MediaService } from './services/media.js';
|
||||
import { MediaSyncService } from './services/media-sync.js';
|
||||
import { RateLimiter } from './utils/rate-limiter.js';
|
||||
import { fetchWithTimeout } from './utils/fetch-with-timeout.js';
|
||||
import { WebServer } from './web/server.js';
|
||||
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';
|
||||
|
||||
export interface AppContainer {
|
||||
config: Config;
|
||||
@@ -81,7 +82,11 @@ export interface AppContainer {
|
||||
* Returns a populated container. Caller owns lifecycle (start loop, handle
|
||||
* shutdown).
|
||||
*/
|
||||
export async function bootstrap(config: Config, logger: Logger): Promise<AppContainer> {
|
||||
export async function bootstrap(
|
||||
config: Config,
|
||||
logger: Logger,
|
||||
existingDatabase?: Database.Database,
|
||||
): Promise<AppContainer> {
|
||||
// Wire winston → dashboard log buffer immediately so the matrix-token
|
||||
// sniff and other early bootstrap diagnostics are visible in /api/logs.
|
||||
const broadcaster = new LogBroadcaster();
|
||||
@@ -90,7 +95,7 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
}
|
||||
|
||||
// Database
|
||||
const db = createDatabase(config);
|
||||
const db = existingDatabase ?? createDatabase(config);
|
||||
runMigrations(db);
|
||||
const postsRepo = new PostsRepository(db);
|
||||
const messagesRepo = new MessagesRepository(db);
|
||||
@@ -186,35 +191,12 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
// or token-sniff flow.
|
||||
const tokenRefresher = new MatrixTokenRefresher();
|
||||
|
||||
// 4. Username resolver — turns t2 IDs into real Reddit usernames so the
|
||||
// dashboard isn't full of `t2_abc123`. Uses Reddit's bulk endpoint.
|
||||
// 4. Username resolver — Matrix profiles expose Reddit display names using
|
||||
// the same authenticated session that powers chat sync.
|
||||
const matrixUsernameResolver = new MatrixUsernameResolver(activeMatrixClient);
|
||||
const usernameResolver: UsernameResolver = {
|
||||
async resolveT2Ids(ids) {
|
||||
if (ids.length === 0) return {};
|
||||
const out: Record<string, string> = {};
|
||||
for (let i = 0; i < ids.length; i += 100) {
|
||||
const chunk = ids.slice(i, i + 100).join(',');
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
// www.reddit.com serves this unauthenticated; the api.reddit.com
|
||||
// host 404s the same path without an OAuth token.
|
||||
`https://www.reddit.com/api/user_data_by_account_ids.json?ids=${encodeURIComponent(chunk)}`,
|
||||
{ headers: { 'User-Agent': config.reddit.userAgent }, timeoutMs: 15_000 },
|
||||
);
|
||||
if (!res.ok) {
|
||||
logger.debug(`user_data_by_account_ids → ${res.status}`);
|
||||
continue;
|
||||
}
|
||||
const json = (await res.json()) as Record<string, { name?: string }>;
|
||||
for (const [t2, info] of Object.entries(json)) {
|
||||
if (info?.name) out[t2] = info.name;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.debug('user_data_by_account_ids failed:', err);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
resolveT2Ids: (ids) => matrixUsernameResolver.resolveT2Ids(ids),
|
||||
resolveUsernameToT2: (username) => redditApi.resolveUsernameToT2(username),
|
||||
};
|
||||
|
||||
const chatService = new ChatService(
|
||||
@@ -244,6 +226,8 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
setMatrixClient(client) {
|
||||
activeMatrixClient = client;
|
||||
chatService.setMatrixClient(client);
|
||||
mediaDownloader.setMatrixClient(client);
|
||||
matrixUsernameResolver.setMatrixClient(client);
|
||||
},
|
||||
},
|
||||
(credentials) => new MatrixClient(credentials, logger),
|
||||
@@ -268,7 +252,7 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
// Stream sync-progress frames over the dashboard's SSE channel so the
|
||||
// full-sync UI can show live phase + per-room progress.
|
||||
let lastSyncProgress: { kind: 'full' | 'delta'; current: number; startedAt: number } | null = null;
|
||||
chatService.setProgressListener((progress) => {
|
||||
chatService.setProgressListener((progress, completion) => {
|
||||
webServer!.broadcastSyncProgress(progress as Record<string, unknown> | null);
|
||||
if (progress) {
|
||||
const syncId = `chat-${progress.startedAt}`;
|
||||
@@ -286,12 +270,22 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
}
|
||||
lastSyncProgress = progress;
|
||||
} else if (lastSyncProgress) {
|
||||
broadcaster.dashboardEvents.sync({
|
||||
phase: 'completed',
|
||||
id: `chat-${lastSyncProgress.startedAt}`,
|
||||
kind: lastSyncProgress.kind,
|
||||
processed: lastSyncProgress.current,
|
||||
});
|
||||
const id = `chat-${lastSyncProgress.startedAt}`;
|
||||
if (completion?.status === 'failed') {
|
||||
broadcaster.dashboardEvents.sync({
|
||||
phase: 'failed',
|
||||
id,
|
||||
kind: lastSyncProgress.kind,
|
||||
error: completion.error,
|
||||
});
|
||||
} else {
|
||||
broadcaster.dashboardEvents.sync({
|
||||
phase: 'completed',
|
||||
id,
|
||||
kind: lastSyncProgress.kind,
|
||||
processed: lastSyncProgress.current,
|
||||
});
|
||||
}
|
||||
lastSyncProgress = null;
|
||||
}
|
||||
});
|
||||
@@ -319,6 +313,7 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
||||
webServer.setSessionImporter(sessionImporter);
|
||||
webServer.setAccountAuth(accountAuth);
|
||||
webServer.setConversationStateRepository(conversationStateRepo);
|
||||
webServer.setConfigStore(new ConfigStore(db));
|
||||
webServer.setOnConfigChange(() => logger.info('Runtime config updated via dashboard'));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user