Fix chat visibility and conversation sync
This commit is contained in:
1 parent
f4e8fdc42f
commit
34ae532cdd
10 files changed
+416
-23
No files matched your search
@@ -54,6 +54,11 @@ export interface RoomSummary {
|
||||
recentEvents: MatrixTimelineEvent[];
|
||||
}
|
||||
|
||||
export interface DirectRoomMapping {
|
||||
recipientMxid: string;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap on how long we'll sit and sleep waiting out a 429. Anything longer
|
||||
* is surfaced as a `MatrixRateLimitError` so the main loop can back off
|
||||
@@ -405,7 +410,7 @@ export class MatrixClient {
|
||||
* "Start chat" in the UI:
|
||||
*
|
||||
* POST /_matrix/client/v3/createRoom
|
||||
* { "preset": "reddit_dm", "invite": [mxid] }
|
||||
* { "preset": "reddit_dm", "invite": [mxid], "is_direct": true }
|
||||
*
|
||||
* `reddit_dm` is a Reddit-specific preset that the homeserver expands
|
||||
* server-side into the right power levels, history visibility, and
|
||||
@@ -413,12 +418,53 @@ export class MatrixClient {
|
||||
*/
|
||||
async createDirectRoom(recipientMxid: string): Promise<string> {
|
||||
const res = await this.request<{ room_id: string }>('POST', '/_matrix/client/v3/createRoom', {
|
||||
body: { preset: 'reddit_dm', invite: [recipientMxid] },
|
||||
body: { preset: 'reddit_dm', invite: [recipientMxid], is_direct: true },
|
||||
});
|
||||
this.logger.debug(`[matrix] created DM room ${res.room_id} with ${recipientMxid}`);
|
||||
return res.room_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing rooms to the account-level `m.direct` map used by Matrix
|
||||
* clients to classify joined rooms as direct chats. Existing mappings are
|
||||
* preserved verbatim; the write is skipped when there is nothing to repair.
|
||||
*/
|
||||
async repairDirectRoomMappings(mappings: DirectRoomMapping[]): Promise<number> {
|
||||
if (mappings.length === 0) return 0;
|
||||
|
||||
let directRooms: Record<string, string[]>;
|
||||
try {
|
||||
directRooms = await this.request<Record<string, string[]>>(
|
||||
'GET',
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(this.creds.userId)}/account_data/m.direct`,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('→ 404')) {
|
||||
directRooms = {};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let repaired = 0;
|
||||
for (const { recipientMxid, roomId } of mappings) {
|
||||
const existing = Array.isArray(directRooms[recipientMxid])
|
||||
? directRooms[recipientMxid]
|
||||
: [];
|
||||
if (existing.includes(roomId)) continue;
|
||||
directRooms[recipientMxid] = [...existing, roomId];
|
||||
repaired++;
|
||||
}
|
||||
|
||||
if (repaired === 0) return 0;
|
||||
await this.request<Record<string, never>>(
|
||||
'PUT',
|
||||
`/_matrix/client/v3/user/${encodeURIComponent(this.creds.userId)}/account_data/m.direct`,
|
||||
{ body: directRooms },
|
||||
);
|
||||
return repaired;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-shot: create a DM room with the recipient and send the opening
|
||||
* message. Returns the new room_id and the event_id of the message.
|
||||
@@ -428,6 +474,13 @@ export class MatrixClient {
|
||||
body: string,
|
||||
): Promise<{ roomId: string; eventId: string }> {
|
||||
const roomId = await this.createDirectRoom(recipientMxid);
|
||||
try {
|
||||
await this.repairDirectRoomMappings([{ recipientMxid, roomId }]);
|
||||
} catch (error) {
|
||||
// Room creation has already succeeded. Do not turn a metadata repair
|
||||
// failure into an orphan-and-retry cycle that creates duplicate rooms.
|
||||
this.logger.warn(`[matrix] failed to register ${roomId} as a direct chat:`, error);
|
||||
}
|
||||
const eventId = await this.sendTextMessage(roomId, body);
|
||||
return { roomId, eventId };
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export class ChatService {
|
||||
if (!chat.roomName || chat.roomName.startsWith('t2_')) chat.roomName = resolved;
|
||||
}
|
||||
await this.cache.save(chats);
|
||||
await this.repairDirectRoomMappings(chats);
|
||||
|
||||
let done = 0;
|
||||
for (const chat of chats) {
|
||||
@@ -727,17 +728,21 @@ export class ChatService {
|
||||
existing.set(roomId, next);
|
||||
}
|
||||
|
||||
if (this.mediaService) {
|
||||
for (const [roomId, room] of Object.entries(joined)) {
|
||||
const scraped = this.scrapeMessageEvents(room?.timeline?.events ?? []);
|
||||
const mediaMessages = scraped.filter((m) => m.mediaUrls.length > 0);
|
||||
if (mediaMessages.length === 0) continue;
|
||||
const chat = existing.get(roomId);
|
||||
if (!chat) continue;
|
||||
for (const [roomId, room] of Object.entries(joined)) {
|
||||
const scraped = this.scrapeMessageEvents(room?.timeline?.events ?? []);
|
||||
if (scraped.length === 0) continue;
|
||||
const chat = existing.get(roomId);
|
||||
if (!chat) continue;
|
||||
|
||||
let newMessages: ChatMessage[] = scraped.map(
|
||||
({ mediaUrls: _ignored, ...message }) => message,
|
||||
);
|
||||
if (this.mediaService && scraped.some((message) => message.mediaUrls.length > 0)) {
|
||||
try {
|
||||
const withMedia = await this.mediaService.attachMedia(chat, mediaMessages);
|
||||
this.mergeNewMessages(chat, withMedia);
|
||||
if (withMedia.some((message) => !message.isSelf && (message.media?.length ?? 0) > 0)) {
|
||||
newMessages = await this.mediaService.attachMedia(chat, scraped);
|
||||
if (newMessages.some(
|
||||
(message) => !message.isSelf && (message.media?.length ?? 0) > 0,
|
||||
)) {
|
||||
this.recordReceivedImage(chat);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -746,6 +751,7 @@ export class ChatService {
|
||||
);
|
||||
}
|
||||
}
|
||||
this.mergeNewMessages(chat, newMessages);
|
||||
}
|
||||
|
||||
const merged = Array.from(existing.values()).sort(
|
||||
@@ -757,9 +763,49 @@ export class ChatService {
|
||||
// Resolve any t2_-prefixed usernames the delta just introduced, so the
|
||||
// dashboard doesn't show raw Matrix-style IDs for new rooms.
|
||||
await this.enrichUsernames();
|
||||
await this.repairDirectRoomMappings(merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile cached one-to-one chats with Matrix account data. The durable
|
||||
* username repository lets delta sync repair older rooms even when Reddit
|
||||
* omits their member state from the latest /sync response.
|
||||
*/
|
||||
private async repairDirectRoomMappings(chats: ActiveChat[]): Promise<void> {
|
||||
const mappings: Array<{ recipientMxid: string; roomId: string }> = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const chat of chats) {
|
||||
if (!chat.roomId) continue;
|
||||
const rawT2 = this.extractT2(chat);
|
||||
const persistedT2 = rawT2
|
||||
? `t2_${rawT2}`
|
||||
: this.redditUsersRepo?.getByUsername(chat.username)?.t2Id;
|
||||
if (!persistedT2) continue;
|
||||
|
||||
const recipientMxid = MatrixClient.t2ToMxid(persistedT2);
|
||||
const key = `${recipientMxid}|${chat.roomId}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
mappings.push({ recipientMxid, roomId: chat.roomId });
|
||||
}
|
||||
|
||||
if (mappings.length === 0) return;
|
||||
try {
|
||||
const repaired = await this.withAuth(() =>
|
||||
this.matrix.repairDirectRoomMappings(mappings),
|
||||
);
|
||||
if (repaired > 0) {
|
||||
this.logger.info(`[chat] repaired ${repaired} missing direct-room mapping(s)`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Sync remains useful even if Reddit temporarily rejects account-data
|
||||
// maintenance. A later delta/full sync will try the additive repair again.
|
||||
this.logger.warn('[chat] direct-room mapping repair failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private scrapeMessageEvents(events: MatrixTimelineEvent[]): ScrapedChatMessage[] {
|
||||
return events
|
||||
.filter((e) => e.type === 'm.room.message')
|
||||
|
||||
Reference in new issue
Block a user