-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
Copy pathSortMembers.ts
112 lines (100 loc) · 5.07 KB
/
SortMembers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { groupBy, mapValues, maxBy, minBy, sumBy, takeRight } from "lodash";
import { type MatrixClient, type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { type Member } from "./direct-messages";
import DMRoomMap from "./DMRoomMap";
export const compareMembers =
(
activityScores: Record<string, IActivityScore | undefined>,
memberScores: Record<string, IMemberScore | undefined>,
) =>
(a: Member | RoomMember, b: Member | RoomMember): number => {
const aActivityScore = activityScores[a.userId]?.score ?? 0;
const aMemberScore = memberScores[a.userId]?.score ?? 0;
const aScore = aActivityScore + aMemberScore;
const aNumRooms = memberScores[a.userId]?.numRooms ?? 0;
const bActivityScore = activityScores[b.userId]?.score ?? 0;
const bMemberScore = memberScores[b.userId]?.score ?? 0;
const bScore = bActivityScore + bMemberScore;
const bNumRooms = memberScores[b.userId]?.numRooms ?? 0;
if (aScore === bScore) {
if (aNumRooms === bNumRooms) {
// If there is no activity between members,
// keep the order received from the user directory search results
return 0;
}
return bNumRooms - aNumRooms;
}
return bScore - aScore;
};
function joinedRooms(cli: MatrixClient): Room[] {
return (
cli
.getRooms()
.filter((r) => r.getMyMembership() === KnownMembership.Join)
// Skip low priority rooms and DMs
.filter((r) => !DMRoomMap.shared().getUserIdForRoomId(r.roomId))
.filter((r) => !Object.keys(r.tags).includes("m.lowpriority"))
);
}
interface IActivityScore {
lastSpoke: number;
score: number;
}
// Score people based on who have sent messages recently, as a way to improve the quality of suggestions.
// We do this by checking every room to see who has sent a message in the last few hours, and giving them
// a score which correlates to the freshness of their message. In theory, this results in suggestions
// which are closer to "continue this conversation" rather than "this person exists".
export function buildActivityScores(cli: MatrixClient): { [userId: string]: IActivityScore } {
const now = new Date().getTime();
const earliestAgeConsidered = now - 60 * 60 * 1000; // 1 hour ago
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
const events = joinedRooms(cli)
.flatMap((room) => takeRight(room.getLiveTimeline().getEvents(), maxMessagesConsidered))
.filter((ev) => ev.getTs() > earliestAgeConsidered);
const senderEvents = groupBy(events, (ev) => ev.getSender());
// If the iteratee in mapValues returns undefined that key will be removed from the resultant object
return mapValues(senderEvents, (events) => {
if (!events.length) return;
const lastEvent = maxBy(events, (ev) => ev.getTs())!;
const distanceFromNow = Math.abs(now - lastEvent.getTs()); // abs to account for slight future messages
const inverseTime = now - earliestAgeConsidered - distanceFromNow;
return {
lastSpoke: lastEvent.getTs(),
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
// score we'll try and award at least 1.0 for making the list, with 4.0 being
// an approximate maximum for being selected.
score: Math.max(1, inverseTime / (15 * 60 * 1000)), // 15min segments to keep scores sane
};
}) as { [key: string]: IActivityScore };
}
interface IMemberScore {
member: RoomMember;
score: number;
numRooms: number;
}
export function buildMemberScores(cli: MatrixClient): { [userId: string]: IMemberScore } {
const maxConsideredMembers = 200;
const consideredRooms = joinedRooms(cli).filter((room) => room.getJoinedMemberCount() < maxConsideredMembers);
const memberPeerEntries = consideredRooms.flatMap((room) =>
room.getJoinedMembers().map((member) => ({ member, roomSize: room.getJoinedMemberCount() })),
);
const userMeta = groupBy(memberPeerEntries, ({ member }) => member.userId);
// If the iteratee in mapValues returns undefined that key will be removed from the resultant object
return mapValues(userMeta, (roomMemberships) => {
if (!roomMemberships.length) return;
const maximumPeers = maxConsideredMembers * roomMemberships.length;
const totalPeers = sumBy(roomMemberships, (entry) => entry.roomSize);
return {
member: minBy(roomMemberships, (entry) => entry.roomSize)!.member,
numRooms: roomMemberships.length,
score: Math.max(0, Math.pow(1 - totalPeers / maximumPeers, 5)),
};
}) as { [userId: string]: IMemberScore };
}