-
Notifications
You must be signed in to change notification settings - Fork 73
/
user-activity.ts
157 lines (139 loc) · 5.34 KB
/
user-activity.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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as logging from "./logging";
const log = logging.get("UserActTracker");
interface UserActivityMetadata {
/**
* The user is active in "private" rooms. Undefined if not.
*/
private?: true;
/**
* The user was previously active, so we don't have a grace period.
*/
active?: true;
}
export interface UserActivitySet {
users: {[userId: string]: UserActivity};
}
// eslint-disable-next-line @typescript-eslint/no-namespace,no-redeclare
export namespace UserActivitySet {
export const DEFAULT: UserActivitySet = {
users: {}
};
}
export interface UserActivity {
ts: number[];
metadata: UserActivityMetadata;
}
export interface UserActivityTrackerConfig {
inactiveAfterDays: number;
minUserActiveDays: number;
}
// eslint-disable-next-line @typescript-eslint/no-namespace,no-redeclare
export namespace UserActivityTrackerConfig {
export const DEFAULT: UserActivityTrackerConfig = {
inactiveAfterDays: 31,
minUserActiveDays: 3,
};
}
export interface UserActivityState {
dataSet: UserActivitySet;
changed: string[];
activeUsers: number;
}
type ChangesCallback = (state: UserActivityState) => void;
const ONE_DAY = 24 * 60 * 60 * 1000;
/**
* Track user activity and produce summaries thereof.
*
* This stores (manually entered through `updateUserActivity()`) timestamps of user activity,
* with optional metadata - which is stored once per user, not timestamped,
* and overwritten upon each update.
*
* Only one timestamp is kept per day, rounded to 12 AM UTC.
* Only the last 31 timestamps are kept, with older ones being dropped.
*
* In metadata, `active` is a reserved key that must not be used
* to not interfere with UserActivityTracker's operations.
*/
export class UserActivityTracker {
constructor(
private readonly config: UserActivityTrackerConfig,
private readonly dataSet: UserActivitySet,
private readonly onChanges?: ChangesCallback,
) { }
public updateUserActivity(userId: string, metadata?: UserActivityMetadata, dateOverride?: Date): void {
let userObject = this.dataSet.users[userId];
if (!userObject) {
userObject = {
ts: [],
metadata: {},
};
}
// Only store it if there are actual keys.
userObject.metadata = { ...userObject.metadata, ...metadata };
const date = dateOverride || new Date();
/** @var newTs Timestamp in seconds of the current UTC day at 12 AM UTC. */
const newTs = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0, 0) / 1000;
if (!userObject.ts.includes(newTs)) {
// Always insert at the start.
userObject.ts.unshift(newTs);
// Slice after 31 days
userObject.ts = userObject.ts.sort((a, b) => b-a).slice(0, 31);
}
if (!userObject.metadata.active) {
/** @var activeSince A unix timestamp in seconds since when the user was active. */
const activeSince = (date.getTime() - (this.config.minUserActiveDays * ONE_DAY)) / 1000;
const active = userObject.ts.filter((ts) => ts >= activeSince).length >= this.config.minUserActiveDays;
if (active) {
userObject.metadata.active = true;
}
}
this.dataSet.users[userId] = userObject;
setImmediate(() => {
log.debug("Notifying the listener of RMAU changes");
this.onChanges?.({
changed: [userId],
dataSet: this.dataSet,
activeUsers: this.countActiveUsers().allUsers,
});
});
}
/**
* Return the number of users active within the number of days specified in `config.inactiveAfterDays`.
*
* It returns the total number of active users under `allUsers` in the returned object.
* `privateUsers` represents those users with their `metadata.private` set to `true`
*/
public countActiveUsers(dateNow?: Date): {allUsers: number; privateUsers: number;} {
let allUsers = 0;
let privateUsers = 0;
const activeSince = ((dateNow?.getTime() || Date.now()) - this.config.inactiveAfterDays * ONE_DAY) / 1000;
for (const user of Object.values(this.dataSet.users)) {
if (!user.metadata.active) {
continue;
}
const tsAfterSince = user.ts.filter((ts) => ts >= activeSince);
if (tsAfterSince.length > 0) {
allUsers += 1;
if (user.metadata?.private === true) {
privateUsers += 1;
}
}
}
return {allUsers, privateUsers};
}
public getUserData(userId: string): UserActivity {
return this.dataSet.users[userId];
}
}