Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prometheus metric for attendee numbers #215

Merged
merged 4 commits into from
Feb 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion src/Conference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,9 +91,21 @@ export class Conference {
[personId: string]: IPerson;
} = {};

private membersInRooms: Record<string, string[]> = {};

private memberRecalculationPromise = Promise.resolve();
private membershipRecalculationQueue = new Set<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.enqueueRecalculateRoomMembership(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]) {
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -854,4 +875,47 @@ 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();
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);
}
}

/**
* 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) {
// We are already expecting to process this room OR are not interested in this room.
if (this.membershipRecalculationQueue.has(roomId) || !this.membersInRooms[roomId]) {
return;
}

this.membershipRecalculationQueue.add(roomId);
// We ensure that recalculations are linear.
return this.memberRecalculationPromise = this.memberRecalculationPromise.then(() => {
this.membershipRecalculationQueue.delete(roomId);
return this.recalculateRoomMembership(roomId);
})
}
}
Loading