"use strict"; const assert = require("node:assert/strict"); const {make_realm} = require("./lib/example_realm.cjs"); const {make_bot, make_user} = require("./lib/example_user.cjs"); const {mock_esm, zrequire} = require("./lib/namespace.cjs"); const {run_test} = require("./lib/test.cjs"); const {page_params} = require("./lib/zpage_params.cjs"); mock_esm("../src/settings_data", { user_can_access_all_other_users: () => true, }); const buddy_data = zrequire("buddy_data"); const people = zrequire("people"); const presence = zrequire("presence"); const {set_realm} = zrequire("state_data"); const {initialize_user_settings} = zrequire("user_settings"); const realm = make_realm(); set_realm(realm); const user_settings = {}; initialize_user_settings({user_settings}); const OFFLINE_THRESHOLD_SECS = 200; const me = make_user({ email: "me@zulip.com", user_id: 101, full_name: "Me Myself", }); const alice = make_user({ email: "alice@zulip.com", user_id: 1, full_name: "Alice Smith", }); const fred = make_user({ email: "fred@zulip.com", user_id: 2, full_name: "Fred Flintstone", }); const sally = make_user({ email: "sally@example.com", user_id: 3, full_name: "Sally Jones", }); const zoe = make_user({ email: "zoe@example.com", user_id: 6, full_name: "Zoe Yang", // User created via the API who never logged in. // In production, such users have no date_joined field. date_joined: undefined, }); const bot = make_bot({ email: "bot@zulip.com", user_id: 7, full_name: "The Bot", }); const john = make_user({ email: "john@zulip.com", user_id: 8, full_name: "John Doe", // Second 77. date_joined: "1970-01-01 00:01:15 UTC", }); const jane = make_user({ email: "jane@zulip.com", user_id: 9, full_name: "Jane Doe", }); people.add_active_user(me); people.add_active_user(alice); people.add_active_user(fred); people.add_active_user(sally); people.add_active_user(zoe); people.add_active_user(bot); people.add_active_user(john); people.add_active_user(jane); const inaccessible_user_id = 9999; const inaccessible_user = people.add_inaccessible_user(inaccessible_user_id); inaccessible_user.is_inaccessible_user = true; people.initialize_current_user(me.user_id); function test(label, f) { run_test(label, (helpers) => { helpers.override( realm, "server_presence_offline_threshold_seconds", OFFLINE_THRESHOLD_SECS, ); helpers.override(user_settings, "presence_enabled", true); presence.clear_internal_data(); f(helpers); }); } test("my user", () => { assert.equal(presence.get_status(me.user_id), "active"); }); test("status_from_raw", () => { const status_from_raw = presence.status_from_raw; const now = 5000; let raw; raw = { server_timestamp: now, active_timestamp: now - OFFLINE_THRESHOLD_SECS / 2, }; assert.deepEqual(status_from_raw(raw, alice), { status: "active", last_active: raw.active_timestamp, }); raw = { server_timestamp: now, active_timestamp: now - OFFLINE_THRESHOLD_SECS * 2, }; assert.deepEqual(status_from_raw(raw, alice), { status: "offline", last_active: raw.active_timestamp, }); raw = { server_timestamp: now, idle_timestamp: now - OFFLINE_THRESHOLD_SECS / 2, }; assert.deepEqual(status_from_raw(raw, alice), { status: "idle", last_active: raw.idle_timestamp, }); const user = people.get_by_user_id(alice.user_id); user.date_joined = new Date((now - OFFLINE_THRESHOLD_SECS * 200) * 1000); raw = { server_timestamp: now, active_timestamp: now - OFFLINE_THRESHOLD_SECS * 200, idle_timestamp: now - OFFLINE_THRESHOLD_SECS * 100, }; assert.deepEqual(status_from_raw(raw, alice), { status: "offline", last_active: raw.active_timestamp, }); }); test("sort_users", () => { const user_ids = [alice.user_id, fred.user_id, jane.user_id]; const now = 5000; const presences = { [alice.user_id.toString()]: {active_timestamp: now}, [fred.user_id.toString()]: {active_timestamp: now}, [jane.user_id.toString()]: {active_timestamp: now}, }; presence.initialize({presences, server_timestamp: now}); assert.deepEqual(user_ids, [alice.user_id, fred.user_id, jane.user_id]); presence.presence_info.delete(alice.user_id); buddy_data.sort_users(user_ids, new Set()); assert.deepEqual(user_ids, [fred.user_id, jane.user_id, alice.user_id]); }); test("set_presence_info", () => { const now = 5000; const recent = now + 1 - OFFLINE_THRESHOLD_SECS; const a_while_ago = now - OFFLINE_THRESHOLD_SECS * 2; const unknown_user_id = 999; const presences = { [alice.user_id.toString()]: {active_timestamp: recent}, [fred.user_id.toString()]: {active_timestamp: a_while_ago, idle_timestamp: now}, [me.user_id.toString()]: {active_timestamp: now}, [sally.user_id.toString()]: {active_timestamp: a_while_ago}, [john.user_id.toString()]: {idle_timestamp: a_while_ago}, [jane.user_id.toString()]: {idle_timestamp: now}, // Unknown user ids can also be in the presence data. [unknown_user_id.toString()]: {idle_timestamp: now}, [inaccessible_user_id.toString()]: {idle_timestamp: now}, }; presence.initialize({presences, server_timestamp: now}); assert.deepEqual(presence.presence_info.get(alice.user_id), { status: "active", last_active: recent, }); assert.equal(presence.get_status(alice.user_id), "active"); assert.deepEqual(presence.last_active_date(alice.user_id), new Date(recent * 1000)); assert.deepEqual(presence.presence_info.get(fred.user_id), {status: "idle", last_active: now}); assert.equal(presence.get_status(fred.user_id), "idle"); assert.deepEqual(presence.presence_info.get(me.user_id), {status: "active", last_active: now}); assert.equal(presence.get_status(me.user_id), "active"); assert.deepEqual(presence.presence_info.get(sally.user_id), { status: "offline", last_active: a_while_ago, }); assert.equal(presence.get_status(sally.user_id), "offline"); assert.deepEqual(presence.presence_info.get(zoe.user_id), { status: "offline", last_active: undefined, }); assert.equal(presence.get_status(zoe.user_id), "offline"); assert.equal(presence.last_active_date(zoe.user_id), undefined); assert.strictEqual(zoe.date_joined, undefined); assert.strictEqual(presence.presence_info.get(zoe.user_id).last_active, undefined); assert.ok(!presence.presence_info.has(bot.user_id)); assert.equal(presence.get_status(bot.user_id), "offline"); assert.deepEqual(presence.presence_info.get(john.user_id), { status: "offline", // Fall back to date_joined, which we set to 75 seconds after the epoch above. last_active: 75, }); assert.equal(presence.get_status(john.user_id), "offline"); assert.deepEqual(presence.presence_info.get(jane.user_id), {status: "idle", last_active: now}); assert.equal(presence.get_status(jane.user_id), "idle"); assert.deepEqual(presence.presence_info.get(unknown_user_id), { status: "idle", last_active: now, }); assert.equal(presence.get_status(unknown_user_id), "idle"); assert.equal(presence.presence_info.get(inaccessible_user_id), undefined); }); test("get_status", ({override}) => { page_params.realm_users = []; const current_user = me; presence.presence_info.set(alice.user_id, {status: "active"}); presence.presence_info.set(fred.user_id, {status: "active"}); presence.presence_info.set(sally.user_id, {status: "idle"}); presence.presence_info.set(zoe.user_id, {status: "active"}); assert.equal(presence.get_status(alice.user_id), "active"); assert.equal(presence.get_status(sally.user_id), "idle"); assert.equal(presence.get_status(fred.user_id), "active"); override(user_settings, "presence_enabled", false); assert.equal(presence.get_status(current_user.user_id), "offline"); override(user_settings, "presence_enabled", true); assert.equal(presence.get_status(current_user.user_id), "active"); presence.presence_info.delete(zoe.user_id); assert.equal(presence.get_status(zoe.user_id), "offline"); presence.presence_info.set(alice.user_id, {status: "whatever"}); assert.equal(presence.get_status(alice.user_id), "whatever"); }); test("missing values", () => { /* When a user does not have a relevant active timestamp, the server just leaves off the `active_timestamp` field to save bandwidth, which looks like `undefined` to us if we try to dereference it. */ const now = 2000000; const a_bit_ago = now - 5; const presences = {[zoe.user_id.toString()]: {idle_timestamp: a_bit_ago}}; presence.set_info(presences, now); assert.deepEqual(presence.presence_info.get(zoe.user_id), { status: "idle", last_active: a_bit_ago, }); presences[zoe.user_id.toString()] = {}; presence.set_info(presences, now); assert.deepEqual(presence.presence_info.get(zoe.user_id), { status: "offline", // This shouldn't happen in reality, but covers not crashing // if we don't have a last_active_time. last_active: 0, }); }); test("big realms", ({override_rewire}) => { const now = 5000; const presences = {[sally.user_id.toString()]: {active_timestamp: now}}; // Make it seem like realm has a lot of people, in // which case we will not provide default values for // users that aren't in our presences payload. override_rewire(people, "get_active_human_count", () => 1000); presence.set_info(presences, now); assert.ok(presence.presence_info.has(sally.user_id)); assert.ok(!presence.presence_info.has(zoe.user_id)); }); test("last_active_date", () => { const unknown_id = 42; presence.presence_info.clear(); presence.presence_info.set(alice.user_id, {last_active: 500}); presence.presence_info.set(fred.user_id, {}); assert.equal(presence.last_active_date(unknown_id), undefined); assert.equal(presence.last_active_date(fred.user_id), undefined); assert.deepEqual(presence.last_active_date(alice.user_id), new Date(500 * 1000)); }); test("update_info_from_event", () => { let info; info = { active_timestamp: 500, idle_timestamp: 500, }; presence.presence_info.delete(alice.user_id); presence.update_info_from_event(alice.user_id, info, 500); assert.deepEqual(presence.presence_info.get(alice.user_id), { status: "active", last_active: 500, }); info = { active_timestamp: 500, idle_timestamp: 500, }; presence.update_info_from_event(alice.user_id, info, 510); assert.deepEqual(presence.presence_info.get(alice.user_id), { status: "active", last_active: 500, }); info = { active_timestamp: 500, idle_timestamp: 1000, }; presence.update_info_from_event(alice.user_id, info, 1000); assert.deepEqual(presence.presence_info.get(alice.user_id), { status: "idle", last_active: 1000, }); }); test("user_last_seen_response_schema", () => { // Modern format with both timestamps. const with_both_timestamps = { result: "success", msg: "", server_timestamp: 1656958540, presence: { active_timestamp: 1656958520, idle_timestamp: 1656958530, }, }; const parsed_both = presence.user_last_seen_response_schema.safeParse(with_both_timestamps); assert.ok(parsed_both.success); assert.equal(parsed_both.data.server_timestamp, 1656958540); assert.equal(parsed_both.data.presence?.active_timestamp, 1656958520); assert.equal(parsed_both.data.presence?.idle_timestamp, 1656958530); // Only active_timestamp present; idle_timestamp may be omitted // if the user has only ever been active. const with_active_only = { result: "success", msg: "", presence: {active_timestamp: 1656958520}, }; const parsed_active = presence.user_last_seen_response_schema.safeParse(with_active_only); assert.ok(parsed_active.success); assert.equal(parsed_active.data.presence?.active_timestamp, 1656958520); assert.equal(parsed_active.data.presence?.idle_timestamp, undefined); // No presence data. const without_presence = {result: "success", msg: ""}; const parsed_none = presence.user_last_seen_response_schema.safeParse(without_presence); assert.ok(parsed_none.success); assert.equal(parsed_none.data.presence, undefined); });