Stop Matrix refresh loop when login is unavailable

This commit is contained in:
jjbrunton committed 2026-08-08 15:41:02 +01:00
1 parent 674cd89cd7
commit 4db6de8bf5
2 files changed
+72 -2

No files matched your search

+20
View File
@@ -25,6 +25,13 @@ export interface TokenRefresher {
refresh(): Promise<void>;
}
class MatrixAuthenticationUnavailableError extends Error {
constructor(cause: unknown) {
super('Matrix authentication could not be restored', { cause });
this.name = 'MatrixAuthenticationUnavailableError';
}
}
/**
* Optional Reddit-API hook for resolving t2 IDs to real usernames. Without
* it, conversations show up as `t2_abc123` (stable but ugly). With it, the
@@ -174,7 +181,11 @@ export class ChatService {
} catch (err) {
if (err instanceof MatrixAuthError && this.tokenRefresher) {
this.logger.warn('[chat] Matrix token rejected — refreshing via browser session');
try {
await this.tokenRefresher.refresh();
} catch (refreshError) {
throw new MatrixAuthenticationUnavailableError(refreshError);
}
return await fn();
}
throw err;
@@ -331,6 +342,7 @@ export class ChatService {
try {
await this.loadDetails(chat);
} catch (err) {
if (err instanceof MatrixAuthenticationUnavailableError) throw err;
this.logger.error(`[chat] backfill: failed details for ${chat.roomName}`, err);
}
done++;
@@ -1062,6 +1074,10 @@ export class ChatService {
}) as { state_key?: string } | undefined;
t2 = otherMember?.state_key?.match(/^@(t2_[^:]+):/)?.[1];
} catch (err) {
if (err instanceof MatrixAuthenticationUnavailableError) {
this.logger.warn('[chat] username enrichment paused until Reddit login is restored');
return;
}
this.logger.debug(`[chat] /state lookup failed for ${chat.roomId}:`, err);
}
if (!t2) {
@@ -1074,6 +1090,10 @@ export class ChatService {
)?.sender;
t2 = otherSender?.match(/^@(t2_[^:]+):/)?.[1];
} catch (err) {
if (err instanceof MatrixAuthenticationUnavailableError) {
this.logger.warn('[chat] username enrichment paused until Reddit login is restored');
return;
}
this.logger.debug(`[chat] /messages lookup failed for ${chat.roomId}:`, err);
}
}
+52 -2
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import os from 'os';
import path from 'path';
import Database from 'better-sqlite3';
@@ -6,7 +6,7 @@ import { runMigrations } from '../src/database/client.js';
import { RedditUsersRepository } from '../src/database/repositories.js';
import { ChatService } from '../src/services/chat/service.js';
import { ChatCache } from '../src/services/chat/cache.js';
import { MatrixClient } from '../src/services/chat/matrix/client.js';
import { MatrixAuthError, MatrixClient } from '../src/services/chat/matrix/client.js';
import { createMockLogger } from './mockLogger.js';
const creds = {
@@ -103,4 +103,54 @@ describe('ChatService username persistence', () => {
expect(chats[0].roomName).toBe('alice');
db.close();
});
it('aborts username enrichment after one failed token refresh', async () => {
const logger = createMockLogger();
const cache = tmpCache(logger);
cache.set([
{
username: 'unknown',
roomName: 'unknown',
lastMessageTime: Date.now(),
lastMessageSnippet: '',
isFromUs: false,
unread: false,
roomId: '!first:reddit.com',
messages: [],
},
{
username: 'unknown',
roomName: 'unknown',
lastMessageTime: Date.now(),
lastMessageSnippet: '',
isFromUs: false,
unread: false,
roomId: '!second:reddit.com',
messages: [],
},
]);
const matrix = buildMatrix();
matrix.getRoomFullState = vi.fn().mockRejectedValue(new MatrixAuthError('expired'));
matrix.getRoomMessages = vi.fn().mockRejectedValue(new MatrixAuthError('expired'));
const tokenRefresher = {
refresh: vi.fn().mockRejectedValue(new Error('interactive login required')),
};
const usernameResolver = { resolveT2Ids: vi.fn().mockResolvedValue({}) };
const svc = new ChatService(
matrix,
logger,
undefined,
undefined,
tokenRefresher,
usernameResolver,
cache,
);
await svc.enrichUsernames();
expect(tokenRefresher.refresh).toHaveBeenCalledOnce();
expect(matrix.getRoomFullState).toHaveBeenCalledOnce();
expect(matrix.getRoomMessages).not.toHaveBeenCalled();
});
});