Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/376.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix incoming messages in DM rooms created after having left an older DM room with the same XMPP user.
34 changes: 27 additions & 7 deletions src/MatrixEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,33 @@ export class MatrixEventHandler {
return;
}

if (membershipEvent && roomType === MROOM_TYPE_GROUP) {
if (this.bridge.getBot().isRemoteUser(event.sender)) {
return; // Don't really care about remote users
}
if (["join", "leave"].includes(event.content.membership as string)) {
await this.handleJoinLeaveGroup(ctx, membershipEvent);
return;
if (membershipEvent) {
switch (roomType) {
case MROOM_TYPE_GROUP:
if (bridgeBot.isRemoteUser(event.sender)) {
return; // Don't really care about remote users
}
if (["join", "leave", "ban"].includes(membershipEvent.content.membership)) {
await this.handleJoinLeaveGroup(ctx, membershipEvent);
return;
}
break;
case MROOM_TYPE_IM:
if (membershipEvent.content.membership === "leave" && membershipEvent.sender === ctx.remote.get<string>("matrixUser")) {
await this.store.removeRoomByRoomId(membershipEvent.room_id);
const protocol = this.purple.getProtocol(roomProtocol);
if (protocol) {
await this.bridge.getIntent(
protocol.getMxIdForProtocol(
ctx.remote.get<string>("recipient"),
this.config.bridge.domain,
this.config.bridge.userPrefix,
).getId()
).leave(membershipEvent.room_id);
}
log.info(`Left and removed entry for IM room ${membershipEvent.room_id} because the user left`);
return;
}
}
}

Expand Down
116 changes: 112 additions & 4 deletions src/MatrixRoomHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class MatrixRoomHandler {
private readonly accountRoomLock = new Set<string>();
private readonly remoteEventIdMapping = new Map<string, string>(); // remote_id -> event_id
private readonly roomCreationLock = new Map<string, Promise<RoomBridgeStoreEntry>>();
private readonly staleIMRoomLock = new Map<string, Promise<boolean>>();
constructor(
private readonly purple: IBifrostInstance,
private readonly profileSync: ProfileSync,
Expand Down Expand Up @@ -101,6 +102,105 @@ export class MatrixRoomHandler {
purple.on("read-receipt", handleAsyncEvent(this.handleReadReceipt.bind(this)));
}

/**
* Begin scanning for stale IM rooms, which will be removed asynchronously.
*
* @returns a Promise that resolves after having retrieved all IM rooms from the store
* and kicking off an asynchronous staleness scan for each of them.
*/
public async startStaleIMRoomScan(): Promise<void> {
const rooms = await this.store.getRoomsOfType(MROOM_TYPE_IM);
log.info(`Got ${rooms.length} IM rooms`);
for (const room of rooms) {
if (!room.matrix) {
log.warn(
`Not checking IM room for remote recipient ${room.remote?.get<string>("recipient") || "<unknown>"} because it has no matrix component`,
);
continue;
}
const waiter = this.dropStaleIMRoom(room);
const roomId = room.matrix.getId();
this.staleIMRoomLock.set(roomId, waiter);
waiter.finally(() => this.staleIMRoomLock.delete(roomId));
}
}

/**
* Removes the Matrix room for a bridged IM chat from the store
* if it is determined to be stale by {@link isIMRoomStale}.
*
* @param room The bridged IM chat to check.
* @returns whether the chat's Matrix room was deemed as stale & was removed.
*/
private async dropStaleIMRoom(room: RoomBridgeStoreEntry): Promise<boolean> {
const [roomId, remoteIntent] = await this.isIMRoomStale(room);
if (roomId) {
await this.store.removeRoomByRoomId(roomId);
if (remoteIntent) {
// May be done concurrently
void remoteIntent.leave(roomId);
}
return true;
} else {
return false;
}
}

/**
* Checks whether a Matrix room for a bridged IM chat is no longer usable.
*
* @param room The bridged IM chat to check.
* @returns A two-element tuple:
* 1. The ID of the chat's Matrix room if it is stale, or an empty string otherwise.
* 2. The {@link Intent} of the room's remote user, if one was found.
*/
private async isIMRoomStale(room: RoomBridgeStoreEntry): Promise<[string, Intent | null]> {
const roomId = room.matrix.getId();
const recipient = room.remote.get<string>("recipient");
if (!recipient) {
log.warn(`IM room ${roomId} is stale because it has no recipient`);
return [roomId, null];
}
const protocol = this.purple.getProtocol(room.remote.get<string>("protocol_id"));
if (!protocol) {
log.warn(`IM room ${roomId} is stale because it has no valid protocol`);
return [roomId, null];
}
const remoteIntent = this.bridge.getIntent(
protocol.getMxIdForProtocol(
recipient,
this.config.bridge.domain,
this.config.bridge.userPrefix,
).getId()
);
const matrixUser = room.remote.get<string>("matrixUser");
if (!matrixUser) {
log.warn(`IM room ${roomId} is stale because it has no matrix user`);
return [roomId, remoteIntent];
}
let content: Record<string, unknown>;
try {
content = await remoteIntent.matrixClient.getRoomStateEventContent(roomId, "m.room.member", matrixUser);
} catch (ex) {
switch (ex.statusCode) {
case 403:
log.warn(`IM room ${roomId} is stale because remote intent isn't a room member and never was`);
return [roomId, remoteIntent];
case 404:
log.warn(`IM room ${roomId} is stale because its matrix user has no membership`);
return [roomId, remoteIntent];
default:
log.error(`IM room ${roomId} staleness unknown, failed to look up room state:`, ex);
return ["", null];
}
}
if (content.membership === "leave") {
log.info(`IM room ${roomId} is stale because its matrix user left`);
return [roomId, remoteIntent];
}
return ["", null];
}

public async onChatJoined(ev: IConversationEvent) {
if (this.purple.needsDedupe()) {
this.deduplicator.incrementRoomUsers(ev.conv.name);
Expand All @@ -127,11 +227,19 @@ export class MatrixRoomHandler {
await (this.roomCreationLock.get(remoteId) || Promise.resolve());
log.info("room was created, no longer waiting");
}
const remoteEntries = await this.store.getIMRoom(matrixUser.getId(), data.account.protocol_id, data.sender);
if (remoteEntries != null && remoteEntries.matrix) {
return remoteEntries.matrix.getId();
const remoteEntry = await this.store.getIMRoom(matrixUser.getId(), data.account.protocol_id, data.sender);
if (!remoteEntry?.matrix) {
return null;
}
const roomId = remoteEntry.matrix.getId();
const staleIMRoomWaiter = this.staleIMRoomLock.get(roomId);
if (staleIMRoomWaiter) {
const isStale = await staleIMRoomWaiter;
if (isStale) {
return null;
}
}
return null;
return roomId;
}

private async createOrGetIMRoom(data: IReceivedImMsg, matrixUser: MatrixUser, intent: Intent): Promise<string> {
Expand Down
1 change: 1 addition & 0 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ class Program {
log.info("Started appservice listener on port", port);
await this.pingBridge();
await this.registerBot();
await this.roomHandler.startStaleIMRoomScan();
log.info("Bridge has started.");
try {
await purple.start();
Expand Down
10 changes: 8 additions & 2 deletions src/store/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const MROOM_TYPE_GROUP = "group";
export const MUSER_TYPE_ACCOUNT = "account";
export const MUSER_TYPE_GHOST = "ghost";

export type MROOM_TYPES = "user-admin"|"im"|"group";
export type MUSER_TYPES = "account"|"ghost";
export type MROOM_TYPES = typeof MROOM_TYPE_UADMIN | typeof MROOM_TYPE_IM | typeof MROOM_TYPE_GROUP;
export type MUSER_TYPES = typeof MUSER_TYPE_ACCOUNT | typeof MUSER_TYPE_GHOST;

export interface IRemoteRoomData {
protocol_id?: string;
Expand All @@ -31,6 +31,12 @@ export interface IRemoteUserAdminData extends IRemoteRoomData {
matrixUser?: string;
}

export interface RoomTypeToRemoteRoomData {
[MROOM_TYPE_IM]: IRemoteImData;
[MROOM_TYPE_GROUP]: IRemoteGroupData;
[MROOM_TYPE_UADMIN]: IRemoteUserAdminData;
};

export interface IMatrixUserData {
accounts: {[key: string]: IRemoteUserAccount};
}
Expand Down
Loading
Loading