Files
ThreadNet-Web/apps/web/src/utils/DMRoomMap.ts
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

251 lines
9.5 KiB
TypeScript
Raw Normal View History

2016-09-06 16:39:21 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2016-2019 , 2021 The Matrix.org Foundation C.I.C.
2016-09-06 16:39:21 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2016-09-06 16:39:21 +01:00
*/
2021-06-17 14:24:53 +01:00
import { uniq } from "lodash";
2025-02-05 13:25:06 +00:00
import { type Room, type MatrixEvent, EventType, ClientEvent, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
2021-10-22 17:23:32 -05:00
import { logger } from "matrix-js-sdk/src/logger";
2021-06-17 14:24:53 +01:00
2023-03-27 08:37:35 +02:00
import { filterValidMDirect } from "./dm/filterValidMDirect";
2016-09-06 16:39:21 +01:00
/**
* Class that takes a Matrix Client and flips the m.direct map
* so the operation of mapping a room ID to which user it's a DM
* with can be performed efficiently.
*
* With 'start', this can also keep itself up to date over time.
2016-09-06 16:39:21 +01:00
*/
export default class DMRoomMap {
2021-01-13 14:49:23 +00:00
private static sharedInstance: DMRoomMap;
// TODO: convert these to maps
private roomToUser: { [key: string]: string } | null = null;
private userToRooms: { [key: string]: string[] } | null = null;
2021-01-13 14:49:23 +00:00
private hasSentOutPatchDirectAccountDataPatch: boolean;
private mDirectEvent!: { [key: string]: string[] };
2021-01-13 14:49:23 +00:00
public constructor(private readonly matrixClient: MatrixClient) {
2021-01-13 14:49:23 +00:00
// see onAccountData
this.hasSentOutPatchDirectAccountDataPatch = false;
2023-03-27 08:37:35 +02:00
const mDirectRawContent = matrixClient.getAccountData(EventType.Direct)?.getContent() ?? {};
this.setMDirectFromContent(mDirectRawContent);
2016-09-06 16:39:21 +01:00
}
2016-09-27 09:56:31 +01:00
/**
* Makes and returns a new shared instance that can then be accessed
* with shared(). This returned instance is not automatically started.
*/
public static makeShared(matrixClient: MatrixClient): DMRoomMap {
DMRoomMap.sharedInstance = new DMRoomMap(matrixClient);
2021-01-13 14:49:23 +00:00
return DMRoomMap.sharedInstance;
2016-09-27 09:56:31 +01:00
}
2021-04-23 14:39:39 +01:00
/**
* Set the shared instance to the instance supplied
* Used by tests
* @param inst the new shared instance
*/
public static setShared(inst: DMRoomMap): void {
2021-04-23 14:39:39 +01:00
DMRoomMap.sharedInstance = inst;
}
/**
* Returns a shared instance of the class
* that uses the singleton matrix client
* The shared instance must be started before use.
*/
2021-01-13 15:37:49 +00:00
public static shared(): DMRoomMap {
2021-01-13 14:49:23 +00:00
return DMRoomMap.sharedInstance;
}
public start(): void {
2021-01-13 15:37:49 +00:00
this.populateRoomToUser();
this.matrixClient.on(ClientEvent.AccountData, this.onAccountData);
}
public stop(): void {
this.matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData);
}
2023-03-27 08:37:35 +02:00
/**
* Filter m.direct content to contain only valid data and then sets it.
* Logs if invalid m.direct content occurs.
* {@link filterValidMDirect}
*
* @param content - Raw m.direct content
*/
private setMDirectFromContent(content: unknown): void {
const { valid, filteredContent } = filterValidMDirect(content);
if (!valid) {
logger.warn("Invalid m.direct content occurred", content);
}
this.mDirectEvent = filteredContent;
}
private onAccountData = (ev: MatrixEvent): void => {
if (ev.getType() == EventType.Direct) {
2023-03-27 08:37:35 +02:00
this.setMDirectFromContent(ev.getContent());
this.userToRooms = null;
this.roomToUser = null;
}
2021-06-29 13:11:58 +01:00
};
2021-01-13 14:49:23 +00:00
/**
* some client bug somewhere is causing some DMs to be marked
* with ourself, not the other user. Fix it by guessing the other user and
* modifying userToRooms
*/
private patchUpSelfDMs(userToRooms: Record<string, string[]>): boolean {
const myUserId = this.matrixClient.getUserId()!;
const selfRoomIds = userToRooms[myUserId];
if (selfRoomIds) {
2018-08-30 12:36:53 +02:00
// any self-chats that should not be self-chats?
const guessedUserIdsThatChanged = selfRoomIds
.map((roomId) => {
const room = this.matrixClient.getRoom(roomId);
2018-08-30 12:36:53 +02:00
if (room) {
const userId = room.guessDMUserId();
if (userId && userId !== myUserId) {
2021-06-29 13:11:58 +01:00
return { userId, roomId };
2022-12-12 12:24:14 +01:00
}
2018-08-30 12:36:53 +02:00
}
2018-10-26 22:50:35 -05:00
})
.filter((ids) => !!ids) as { userId: string; roomId: string }[]; //filter out
2018-08-30 12:36:53 +02:00
// these are actually all legit self-chats
// bail out
if (!guessedUserIdsThatChanged.length) {
return false;
}
userToRooms[myUserId] = selfRoomIds.filter((roomId) => {
return !guessedUserIdsThatChanged.some((ids) => ids.roomId === roomId);
});
2021-06-29 13:11:58 +01:00
guessedUserIdsThatChanged.forEach(({ userId, roomId }) => {
2018-10-11 22:05:59 -05:00
const roomIds = userToRooms[userId];
if (!roomIds) {
2018-09-04 13:07:24 +02:00
userToRooms[userId] = [roomId];
} else {
roomIds.push(roomId);
2020-08-28 18:53:43 +01:00
userToRooms[userId] = uniq(roomIds);
}
});
2018-08-30 12:36:53 +02:00
return true;
}
return false;
}
public getDMRoomsForUserId(userId: string): string[] {
2016-09-09 17:35:35 +01:00
// Here, we return the empty list if there are no rooms,
// since the number of conversations you have with this user is zero.
2021-01-13 15:37:49 +00:00
return this.getUserToRooms()[userId] || [];
2016-09-06 16:39:21 +01:00
}
2020-01-14 23:32:00 -07:00
/**
* Gets the DM room which the given IDs share, if any.
* @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
* @returns {Room} The DM room which all IDs given share, or falsy if no common room.
2020-01-14 23:32:00 -07:00
*/
public getDMRoomForIdentifiers(ids: string[]): Room | null {
2020-01-14 23:32:00 -07:00
// TODO: [Canonical DMs] Handle lookups for email addresses.
// For now we'll pretend we only get user IDs and end up returning nothing for email addresses
let commonRooms = this.getDMRoomsForUserId(ids[0]);
for (let i = 1; i < ids.length; i++) {
const userRooms = this.getDMRoomsForUserId(ids[i]);
commonRooms = commonRooms.filter((r) => userRooms.includes(r));
}
const joinedRooms = commonRooms
.map((r) => this.matrixClient.getRoom(r))
.filter((r) => r && r.getMyMembership() === KnownMembership.Join);
2020-01-14 23:32:00 -07:00
return joinedRooms[0];
}
public getUserIdForRoomId(roomId: string): string | undefined {
2016-09-09 16:15:01 +01:00
if (this.roomToUser == null) {
// we lazily populate roomToUser so you can use
// this class just to call getDMRoomsForUserId
// which doesn't do very much, but is a fairly
// convenient wrapper and there's no point
// iterating through the map if getUserIdForRoomId()
// is never called.
2021-01-13 15:37:49 +00:00
this.populateRoomToUser();
2016-09-09 16:15:01 +01:00
}
2016-09-09 17:35:35 +01:00
// Here, we return undefined if the room is not in the map:
// the room ID you gave is not a DM room for any user.
if (this.roomToUser![roomId] === undefined) {
2016-09-12 18:32:44 +01:00
// no entry? if the room is an invite, look for the is_direct hint.
const room = this.matrixClient.getRoom(roomId);
if (room) {
2018-08-14 11:43:03 +02:00
return room.getDMInviter();
2016-09-12 18:32:44 +01:00
}
}
return this.roomToUser![roomId];
2016-09-06 16:39:21 +01:00
}
2016-09-09 16:15:01 +01:00
2021-01-13 15:37:49 +00:00
public getUniqueRoomsWithIndividuals(): { [userId: string]: Room } {
if (!this.roomToUser) return {}; // No rooms means no map.
// map roomToUser to valid rooms with two participants
2024-01-02 18:56:39 +00:00
return Object.keys(this.roomToUser).reduce(
(acc, roomId: string) => {
const userId = this.getUserIdForRoomId(roomId);
const room = this.matrixClient.getRoom(roomId);
const hasTwoMembers = room?.getInvitedAndJoinedMemberCount() === 2;
if (userId && room && hasTwoMembers) {
acc[userId] = room;
}
return acc;
},
{} as Record<string, Room>,
);
}
/**
* @returns all room Ids from m.direct
*/
public getRoomIds(): Set<string> {
return Object.values(this.mDirectEvent).reduce((prevRoomIds: Set<string>, roomIds: string[]): Set<string> => {
roomIds.forEach((roomId) => prevRoomIds.add(roomId));
return prevRoomIds;
}, new Set<string>());
}
2021-01-13 15:37:49 +00:00
private getUserToRooms(): { [key: string]: string[] } {
if (!this.userToRooms) {
const userToRooms = this.mDirectEvent;
const myUserId = this.matrixClient.getUserId()!;
const selfDMs = userToRooms[myUserId];
if (selfDMs?.length) {
2021-01-13 15:44:33 +00:00
const neededPatching = this.patchUpSelfDMs(userToRooms);
// to avoid multiple devices fighting to correct
// the account data, only try to send the corrected
// version once.
logger.warn(`Invalid m.direct account data detected (self-chats that shouldn't be), patching it up.`);
2021-01-13 14:49:23 +00:00
if (neededPatching && !this.hasSentOutPatchDirectAccountDataPatch) {
this.hasSentOutPatchDirectAccountDataPatch = true;
this.matrixClient.setAccountData(EventType.Direct, userToRooms);
}
}
this.userToRooms = userToRooms;
}
return this.userToRooms;
}
private populateRoomToUser(): void {
2016-09-09 16:15:01 +01:00
this.roomToUser = {};
2021-01-13 15:44:33 +00:00
for (const user of Object.keys(this.getUserToRooms())) {
for (const roomId of this.userToRooms![user]) {
2016-09-09 16:15:01 +01:00
this.roomToUser[roomId] = user;
}
}
}
2016-09-06 16:39:21 +01:00
}