From df3570f359d2a1a6f2829ff21deadb4aa7debd47 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 1 Feb 2024 10:10:33 +0000 Subject: [PATCH 1/4] Track membership numbers across the conference space. --- src/Conference.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Conference.ts b/src/Conference.ts index 9ed4361..ac9817b 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -59,6 +59,9 @@ import { IScheduleBackend } from "./backends/IScheduleBackend"; import { PentaBackend } from "./backends/penta/PentaBackend"; import { setUnion } from "./utils/sets"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; +import { Gauge } from "prom-client"; + +const attendeeTotalGauge = new Gauge({ name: "confbot_attendee_total", help: "The number of attendees across all rooms."}); export class Conference { private rootSpace: Space | null; @@ -88,9 +91,21 @@ export class Conference { [personId: string]: IPerson; } = {}; + private membersInRooms: Record; + + private memberRecalculationPromise = Promise.resolve(); + private membershipRecalculationQueue: string[] = []; + constructor(public readonly backend: IScheduleBackend, public readonly id: string, public readonly client: ConferenceMatrixClient, private readonly config: IConfig) { this.client.on("room.event", async (roomId: string, event) => { - if (event['type'] === 'm.room.member' && event['content']?.['third_party_invite']) { + if (event.type !== 'm.room.member' && event.state_key !== undefined) { + return; + } + + // On any member event, recaulculate the membership. + this.enqueueRecalculateRoomMembershop(roomId); + + if (event['content']?.['third_party_invite']) { const emailInviteToken = event['content']['third_party_invite']['signed']?.['token']; const emailInvite = await this.client.getRoomStateEvent(roomId, "m.room.third_party_invite", emailInviteToken); if (emailInvite[RS_3PID_PERSON_ID]) { @@ -215,32 +230,38 @@ export class Conference { switch (locatorEvent[RSC_ROOM_KIND_FLAG]) { case RoomKind.ConferenceSpace: this.rootSpace = new Space(roomId, this.client); + this.recalculateRoomMembership(roomId); break; case RoomKind.ConferenceDb: this.dbRoom = new MatrixRoom(roomId, this.client, this); + this.recalculateRoomMembership(roomId); break; case RoomKind.Auditorium: const auditoriumId = locatorEvent[RSC_AUDITORIUM_ID]; if (this.backend.auditoriums.has(auditoriumId)) { this.auditoriums[auditoriumId] = new Auditorium(roomId, this.backend.auditoriums.get(auditoriumId)!, this.client, this); + this.recalculateRoomMembership(roomId); } break; case RoomKind.AuditoriumBackstage: const auditoriumBsId = locatorEvent[RSC_AUDITORIUM_ID]; if (this.backend.auditoriums.has(auditoriumBsId)) { this.auditoriumBackstages[auditoriumBsId] = new AuditoriumBackstage(roomId, this.backend.auditoriums.get(auditoriumBsId)!, this.client, this); + this.recalculateRoomMembership(roomId); } break; case RoomKind.Talk: const talkId = locatorEvent[RSC_TALK_ID]; if (this.backend.talks.has(talkId)) { this.talks[talkId] = new Talk(roomId, this.backend.talks.get(talkId)!, this.client, this); + this.recalculateRoomMembership(roomId); } break; case RoomKind.SpecialInterest: const interestId = locatorEvent[RSC_SPECIAL_INTEREST_ID]; if (this.backend.interestRooms.has(interestId)) { this.interestRooms[interestId] = new InterestRoom(roomId, this.client, this, interestId, this.config.conference.prefixes); + this.recalculateRoomMembership(roomId); } break; default: @@ -854,4 +875,41 @@ export class Conference { return []; } + + private async recalculateRoomMembership(roomId: string) { + try { + const myUserId = await this.client.getUserId(); + const members = await this.client.getAllRoomMembers(roomId); + const joinedOrLeftMembers = members.filter(m => m.effectiveMembership === "join" || m.effectiveMembership === "leave").map(m => m.stateKey); + this.membersInRooms[roomId] = joinedOrLeftMembers; + const total = new Set(Object.values(this.membersInRooms).flat()); + total.delete(myUserId); + total.delete(this.config.moderatorUserId); + attendeeTotalGauge.set(total.size); + } catch (ex) { + LogService.warn("Conference", `Failed to recalculate room membership for ${roomId}`, ex); + } + } + + private async enqueueRecalculateRoomMembershop(roomId: string) { + if (this.membershipRecalculationQueue.includes(roomId)) { + return; + } + + // Not interested in this room. + if (!this.membersInRooms[roomId]) { + return; + } + + this.membershipRecalculationQueue.unshift(roomId); + // We ensure that recalculations are linear. + return this.memberRecalculationPromise = this.memberRecalculationPromise.then(() => { + // Pop off whatever is first. + const queueRoomId = this.membershipRecalculationQueue.pop(); + if (!queueRoomId) { + return; + } + return this.recalculateRoomMembership(queueRoomId); + }) + } } From 9981066bf4eef6c9f030c793ec53072ba3c07f3a Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 1 Feb 2024 10:14:55 +0000 Subject: [PATCH 2/4] Make sure we initialise the array --- src/Conference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Conference.ts b/src/Conference.ts index ac9817b..f14c662 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -91,7 +91,7 @@ export class Conference { [personId: string]: IPerson; } = {}; - private membersInRooms: Record; + private membersInRooms: Record = {}; private memberRecalculationPromise = Promise.resolve(); private membershipRecalculationQueue: string[] = []; From a97b3b280964e096fb36bec3d4bde25902245ce7 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 1 Feb 2024 10:32:07 +0000 Subject: [PATCH 3/4] Documentation + simplify --- src/Conference.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Conference.ts b/src/Conference.ts index f14c662..cfa9d4c 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -94,7 +94,7 @@ export class Conference { private membersInRooms: Record = {}; private memberRecalculationPromise = Promise.resolve(); - private membershipRecalculationQueue: string[] = []; + private membershipRecalculationQueue = new Set(); constructor(public readonly backend: IScheduleBackend, public readonly id: string, public readonly client: ConferenceMatrixClient, private readonly config: IConfig) { this.client.on("room.event", async (roomId: string, event) => { @@ -103,7 +103,7 @@ export class Conference { } // On any member event, recaulculate the membership. - this.enqueueRecalculateRoomMembershop(roomId); + this.enqueueRecalculateRoomMembership(roomId); if (event['content']?.['third_party_invite']) { const emailInviteToken = event['content']['third_party_invite']['signed']?.['token']; @@ -876,6 +876,15 @@ export class Conference { return []; } + /** + * Recalculate the number of joined and left users in a room, + * and then update the total count for the conference. + * + * Prefer to call `enqueueRecalculateRoomMembership` as it will + * queue and debounce calls appropriately. + * + * @param roomId The roomId to recalculate. + */ private async recalculateRoomMembership(roomId: string) { try { const myUserId = await this.client.getUserId(); @@ -891,8 +900,13 @@ export class Conference { } } - private async enqueueRecalculateRoomMembershop(roomId: string) { - if (this.membershipRecalculationQueue.includes(roomId)) { + /** + * Queue up a call to `recalculateRoomMembership`. + * @param roomId The roomId to recalculate. + * @returns A promise that resolves when the call has been made. + */ + private async enqueueRecalculateRoomMembership(roomId: string) { + if (this.membershipRecalculationQueue.has(roomId)) { return; } @@ -901,15 +915,11 @@ export class Conference { return; } - this.membershipRecalculationQueue.unshift(roomId); + this.membershipRecalculationQueue.add(roomId); // We ensure that recalculations are linear. return this.memberRecalculationPromise = this.memberRecalculationPromise.then(() => { - // Pop off whatever is first. - const queueRoomId = this.membershipRecalculationQueue.pop(); - if (!queueRoomId) { - return; - } - return this.recalculateRoomMembership(queueRoomId); + this.membershipRecalculationQueue.delete(roomId); + return this.recalculateRoomMembership(roomId); }) } } From d857d301ab42530486de2c37fa2552862e57efc3 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 1 Feb 2024 10:32:55 +0000 Subject: [PATCH 4/4] Simplify further --- src/Conference.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Conference.ts b/src/Conference.ts index cfa9d4c..b869cc1 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -906,12 +906,8 @@ export class Conference { * @returns A promise that resolves when the call has been made. */ private async enqueueRecalculateRoomMembership(roomId: string) { - if (this.membershipRecalculationQueue.has(roomId)) { - return; - } - - // Not interested in this room. - if (!this.membersInRooms[roomId]) { + // We are already expecting to process this room OR are not interested in this room. + if (this.membershipRecalculationQueue.has(roomId) || !this.membersInRooms[roomId]) { return; }