events: Add support for processing modern presence event in web client.

This commit is contained in:
Vector73
2025-08-15 10:30:57 +00:00
committed by Tim Abbott
parent 396f4d004a
commit 92fbe47a3b
10 changed files with 72 additions and 69 deletions

View File

@@ -68,8 +68,9 @@ def get_event_checker(event: dict[str, Any]) -> Callable[[str, dict[str, Any]],
# Change to CamelCase # Change to CamelCase
name = name.replace("_", " ").title().replace(" ", "") name = name.replace("_", " ").title().replace(" ", "")
# Use EventModernPresence type to check "presence" events
if name == "Presence": if name == "Presence":
name = "Legacy" + name name = "Modern" + name
# And add the prefix. # And add the prefix.
name = "Event" + name name = "Event" + name

View File

@@ -227,11 +227,12 @@ export function initialize(opts: {narrow_by_email: (email: string) => void}): vo
activity.send_presence_to_server(); activity.send_presence_to_server();
} }
export function update_presence_info( export function update_presence_info(info: PresenceInfoFromEvent): void {
user_id: number, const presence_entry = Object.entries(info)[0];
info: PresenceInfoFromEvent, assert(presence_entry !== undefined);
server_time: number, const [user_id_string, presence_info] = presence_entry;
): void { const user_id = Number.parseInt(user_id_string, 10);
// There can be some case where the presence event // There can be some case where the presence event
// was set for an inaccessible user if // was set for an inaccessible user if
// CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE is // CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE is
@@ -241,7 +242,7 @@ export function update_presence_info(
return; return;
} }
presence.update_info_from_event(user_id, info, server_time); presence.update_info_from_event(user_id, presence_info);
redraw_user(user_id); redraw_user(user_id);
pm_list.update_private_messages(); pm_list.update_private_messages();
} }

View File

@@ -15,14 +15,13 @@ export type PresenceStatus = {
last_active?: number | undefined; last_active?: number | undefined;
}; };
export const presence_info_from_event_schema = z.object({ export const presence_info_from_event_schema = z.record(
website: z.object({ z.string(),
client: z.literal("website"), z.object({
status: z.enum(["idle", "active"]), active_timestamp: z.number(),
timestamp: z.number(), idle_timestamp: z.number(),
pushable: z.boolean(),
}), }),
}); );
export type PresenceInfoFromEvent = z.output<typeof presence_info_from_event_schema>; export type PresenceInfoFromEvent = z.output<typeof presence_info_from_event_schema>;
export const user_last_seen_response_schema = z.object({ export const user_last_seen_response_schema = z.object({
@@ -163,18 +162,16 @@ export function status_from_raw(raw: RawPresence, user: User | undefined): Prese
export function update_info_from_event( export function update_info_from_event(
user_id: number, user_id: number,
info: PresenceInfoFromEvent | null, info: z.infer<typeof presence_schema> | null,
server_timestamp: number, server_timestamp: number | undefined = undefined,
): void { ): void {
/* /*
Example of `info`: Example of `info`:
{ {
website: { "10": {
client: 'website', active_timestamp: 1585745133,
pushable: false, idle_timestamp: 1585745091
status: 'active',
timestamp: 1585745225
} }
} }
@@ -190,16 +187,16 @@ export function update_info_from_event(
server_timestamp: 0, server_timestamp: 0,
}; };
raw.server_timestamp = server_timestamp; if (server_timestamp !== undefined) {
// The event itself doesn't contain a server_timestamp. But
// since the event should be newer than our last polling
// response from the server, it should be safe to use that.
raw.server_timestamp = server_timestamp;
}
for (const rec of Object.values(info ?? {})) { if (info !== null) {
if (rec.status === "active" && rec.timestamp > (raw.active_timestamp ?? 0)) { raw.active_timestamp = info.active_timestamp;
raw.active_timestamp = rec.timestamp; raw.idle_timestamp = info.idle_timestamp;
}
if (rec.status === "idle" && rec.timestamp > (raw.idle_timestamp ?? 0)) {
raw.idle_timestamp = rec.timestamp;
}
} }
raw_info.set(user_id, raw); raw_info.set(user_id, raw);

View File

@@ -230,7 +230,7 @@ export function dispatch_normal_event(event) {
break; break;
case "presence": case "presence":
activity_ui.update_presence_info(event.user_id, event.presence, event.server_timestamp); activity_ui.update_presence_info(event.presences);
break; break;
case "restart": case "restart":

View File

@@ -694,11 +694,10 @@ test("update_presence_info", ({override, override_rewire}) => {
override(realm, "server_presence_ping_interval_seconds", 60); override(realm, "server_presence_ping_interval_seconds", 60);
override(realm, "server_presence_offline_threshold_seconds", 200); override(realm, "server_presence_offline_threshold_seconds", 200);
const server_time = 500; let info = {
const info = { [me.user_id]: {
website: { active_timestamp: 500,
status: "active", idle_timestamp: 500,
timestamp: server_time,
}, },
}; };
@@ -711,12 +710,19 @@ test("update_presence_info", ({override, override_rewire}) => {
}); });
presence.presence_info.delete(me.user_id); presence.presence_info.delete(me.user_id);
activity_ui.update_presence_info(me.user_id, info, server_time); activity_ui.update_presence_info(info);
assert.ok(inserted); assert.ok(inserted);
assert.deepEqual(presence.presence_info.get(me.user_id).status, "active"); assert.deepEqual(presence.presence_info.get(me.user_id).status, "active");
info = {
[alice.user_id]: {
active_timestamp: 500,
idle_timestamp: 500,
},
};
presence.presence_info.delete(alice.user_id); presence.presence_info.delete(alice.user_id);
activity_ui.update_presence_info(alice.user_id, info, server_time); activity_ui.update_presence_info(info);
assert.ok(inserted); assert.ok(inserted);
const expected = {status: "active", last_active: 500}; const expected = {status: "active", last_active: 500};
@@ -724,7 +730,13 @@ test("update_presence_info", ({override, override_rewire}) => {
// Test invalid and inaccessible user IDs. // Test invalid and inaccessible user IDs.
const invalid_user_id = 99; const invalid_user_id = 99;
activity_ui.update_presence_info(invalid_user_id, info, server_time); info = {
[invalid_user_id]: {
active_timestamp: 500,
idle_timestamp: 500,
},
};
activity_ui.update_presence_info(info);
assert.equal(presence.presence_info.get(invalid_user_id), undefined); assert.equal(presence.presence_info.get(invalid_user_id), undefined);
const inaccessible_user_id = 10; const inaccessible_user_id = 10;
@@ -735,7 +747,13 @@ test("update_presence_info", ({override, override_rewire}) => {
"Unknown user", "Unknown user",
); );
people._add_user(inaccessible_user); people._add_user(inaccessible_user);
activity_ui.update_presence_info(inaccessible_user_id, info, server_time); info = {
[inaccessible_user_id]: {
active_timestamp: 500,
idle_timestamp: 500,
},
};
activity_ui.update_presence_info(info);
assert.equal(presence.presence_info.get(inaccessible_user_id), undefined); assert.equal(presence.presence_info.get(inaccessible_user_id), undefined);
}); });

View File

@@ -461,10 +461,8 @@ test("level", ({override}) => {
const server_time = 9999; const server_time = 9999;
const info = { const info = {
website: { active_timestamp: 9999,
status: "active", idle_timestamp: 9999,
timestamp: server_time,
},
}; };
presence.update_info_from_event(me.user_id, info, server_time); presence.update_info_from_event(me.user_id, info, server_time);
presence.update_info_from_event(selma.user_id, info, server_time); presence.update_info_from_event(selma.user_id, info, server_time);

View File

@@ -461,10 +461,8 @@ run_test("presence", ({override}) => {
override(activity_ui, "update_presence_info", stub.f); override(activity_ui, "update_presence_info", stub.f);
dispatch(event); dispatch(event);
assert.equal(stub.num_calls, 1); assert.equal(stub.num_calls, 1);
const args = stub.get_args("user_id", "presence", "server_time"); const args = stub.get_args("presences");
assert_same(args.user_id, event.user_id); assert_same(args.presences, event.presences);
assert_same(args.presence, event.presence);
assert_same(args.server_time, event.server_timestamp);
}); });
run_test("reaction", ({override}) => { run_test("reaction", ({override}) => {

View File

@@ -328,17 +328,12 @@ exports.fixtures = {
presence: { presence: {
type: "presence", type: "presence",
email: "alice@example.com", presences: {
user_id: 42, 42: {
presence: { active_timestamp: fake_now,
electron: { idle_timestamp: fake_now,
status: "active",
timestamp: fake_now,
client: "electron",
pushable: false,
}, },
}, },
server_timestamp: fake_now,
}, },
reaction__add: { reaction__add: {

View File

@@ -315,10 +315,8 @@ test("update_info_from_event", () => {
let info; let info;
info = { info = {
website: { active_timestamp: 500,
status: "active", idle_timestamp: 500,
timestamp: 500,
},
}; };
presence.presence_info.delete(alice.user_id); presence.presence_info.delete(alice.user_id);
@@ -330,10 +328,8 @@ test("update_info_from_event", () => {
}); });
info = { info = {
mobile: { active_timestamp: 500,
status: "idle", idle_timestamp: 500,
timestamp: 510,
},
}; };
presence.update_info_from_event(alice.user_id, info, 510); presence.update_info_from_event(alice.user_id, info, 510);
@@ -343,10 +339,8 @@ test("update_info_from_event", () => {
}); });
info = { info = {
mobile: { active_timestamp: 500,
status: "idle", idle_timestamp: 1000,
timestamp: 1000,
},
}; };
presence.update_info_from_event(alice.user_id, info, 1000); presence.update_info_from_event(alice.user_id, info, 1000);

View File

@@ -105,6 +105,7 @@ def build_page_params_for_home_page_load(
include_deactivated_groups=True, include_deactivated_groups=True,
archived_channels=True, archived_channels=True,
empty_topic_name=True, empty_topic_name=True,
simplified_presence_events=True,
) )
if user_profile is not None: if user_profile is not None: