-
Notifications
You must be signed in to change notification settings - Fork 76
Expand file tree
/
Copy pathactivity-tracker.ts
More file actions
131 lines (122 loc) · 5.39 KB
/
Copy pathactivity-tracker.ts
File metadata and controls
131 lines (122 loc) · 5.39 KB
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { MatrixClient, MatrixError } from "matrix-bot-sdk";
import { Logger } from ".."
const log = new Logger("ActivityTracker");
export interface ActivityTrackerOpts {
/**
* Matrix server name. Used for determining local and remote users.
* @example matrix.org
*/
serverName: string;
/**
* Should the tracker assume offline or online if it doesn't have enough information.
*/
defaultOnline: boolean;
/**
* Should presence be used. Set to false if the homeserver has presence disabled.
*/
usePresence?: boolean;
/**
* Run a function when the last active time for a user gets updated
*/
onLastActiveTimeUpdated?: (userId: string, ts: number) => void|Promise<void>
}
/**
* This class provides a "one stop shop" to determine if a user is online. It uses a combination of a
* local cache, presence endpoints and admin APIs in that order.
*/
export class ActivityTracker {
private lastActiveTime: Map<string, number>;
private canUseWhois: boolean|null = null;
constructor(private readonly client: MatrixClient, private opts: ActivityTrackerOpts) {
opts.usePresence = opts.usePresence !== undefined ? opts.usePresence : true;
this.lastActiveTime = new Map();
}
public get usingWhois(): boolean|null {
return this.canUseWhois;
}
/**
* Sets the last active time of the user to `ts`. By default this is the current time.
* @param userId The userId of a user who performed an action.
* @param ts The timestamp to set in milliseconds.
*/
public setLastActiveTime(userId: string, ts: number = Date.now()): void {
this.lastActiveTime.set(userId, ts);
(async () => {
if (!this.opts.onLastActiveTimeUpdated) {
return;
}
try {
await this.opts.onLastActiveTimeUpdated(userId, ts);
}
catch (ex) {
// Not considered fatal, but disappointing.
log.debug(`onLastActiveTimeUpdated failed to run for ${userId}`, ex);
}
})();
}
/**
* Determine if a user is online or offline using a range of metrics.
* @param userId The userId to check
* @param maxTimeMs The maximum time a user may be inactive for before they are considered offline.
* @param defaultOnline Should the user be online or offline if no data is found. Defaults to `opts.defaultOnline`
*/
public async isUserOnline(
userId: string, maxTimeMs: number, defaultOnline?: boolean): Promise<{online: boolean, inactiveMs: number}> {
defaultOnline = defaultOnline === undefined ? this.opts.defaultOnline : defaultOnline;
if (this.canUseWhois === null) {
try {
// HACK: Synapse exposes no way to directly determine if a user is an admin, so we use this auth check.
await this.client.doRequest("GET", "/_synapse/admin/v1/users/@foo:bar/admin");
this.canUseWhois = false; // This should never succeed, but prevent it from trying anyway.
}
catch (ex) {
// We expect this to fail
this.canUseWhois = ex instanceof MatrixError && (ex.statusCode === 200 || ex.statusCode === 400);
}
}
// First, check if the user has bumped recently.
const now = Date.now();
const lastActiveTime = this.lastActiveTime.get(userId);
if (lastActiveTime) {
if (now - lastActiveTime < maxTimeMs) {
// Return early, user has bumped recently.
return {online: true, inactiveMs: now - lastActiveTime};
}
}
// The user hasn't interacted with the bridge, or it was too long ago.
// Check the user's presence.
try {
if (this.opts.usePresence) {
const presence = await this.client.getPresenceStatusFor(userId);
if (presence.currentlyActive || presence.state === "online") {
return {online: true, inactiveMs: presence.lastActiveAgo || 0};
}
else if (presence.lastActiveAgo && presence.lastActiveAgo > maxTimeMs) {
return {online: false, inactiveMs: presence.lastActiveAgo};
} // Otherwise, we can't know conclusively.
}
}
catch {
// Failed to get presence, going to fallback to admin api.
}
const canUseWhois = this.canUseWhois && userId.split(":", 2)[1] === this.opts.serverName;
if (canUseWhois) {
try {
const whois = await this.client.adminApis.whoisUser(userId);
const connections = Object.values(whois.devices).flatMap(
(device) => device.sessions.flatMap((session => session.connections))
);
const bestConnection = connections.sort((conA, conB) => conB.last_seen - conA.last_seen)[0];
return {
online: (now - bestConnection.last_seen) < maxTimeMs, inactiveMs: now - bestConnection.last_seen
};
}
catch (ex) {
// Failed to use whois, fall back.
}
}
// The user is remote, we don't have any presence for them and they've
// not interacted with us so we are going to have to treat them as offline.
return {online: defaultOnline, inactiveMs: -1};
}
}