From 1049592b46ecd4819bf4421746d3f3e862814fb4 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Sat, 3 Feb 2024 12:07:05 +0000 Subject: [PATCH 1/5] Count number of sent invites --- src/commands/InviteCommand.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/commands/InviteCommand.ts b/src/commands/InviteCommand.ts index 37b5617..68d3e96 100644 --- a/src/commands/InviteCommand.ts +++ b/src/commands/InviteCommand.ts @@ -30,7 +30,7 @@ export class InviteCommand implements ICommand { constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference, private readonly config: IConfig) {} - private async createInvites(people: IPerson[], alias: string) { + private async createInvites(people: IPerson[], alias: string): Promise { const resolved = await resolveIdentifiers(this.client, people); let targetRoomId; @@ -40,7 +40,7 @@ export class InviteCommand implements ICommand { catch (error) { throw Error(`Error resolving room id for ${alias}`, {cause: error}) } - await this.ensureInvited(targetRoomId, resolved); + return await this.ensureInvited(targetRoomId, resolved); } public async run(managementRoomId: string, event: any, args: string[]) { @@ -51,6 +51,8 @@ export class InviteCommand implements ICommand { // in it. We don't remove anyone and don't care about extras - we just want to make sure // that a subset of people are joined. + let invitesSent = 0; + if (args[0] && args[0] === "speakers-support") { let people: IPerson[] = []; for (const aud of this.conference.storedAuditoriumBackstages) { @@ -63,7 +65,7 @@ export class InviteCommand implements ICommand { newPeople.push(p); } }); - await this.createInvites(newPeople, this.config.conference.supportRooms.speakers); + invitesSent += await this.createInvites(newPeople, this.config.conference.supportRooms.speakers); } else if (args[0] && args[0] === "coordinators-support") { let people: IPerson[] = []; for (const aud of this.conference.storedAuditoriums) { @@ -84,24 +86,27 @@ export class InviteCommand implements ICommand { newPeople.push(p); } }); - await this.createInvites(newPeople, this.config.conference.supportRooms.coordinators); + invitesSent += await this.createInvites(newPeople, this.config.conference.supportRooms.coordinators); } else if (args[0] && args[0] === "si-support") { const people: IPerson[] = []; for (const sir of this.conference.storedInterestRooms) { people.push(...await this.conference.getInviteTargetsForInterest(sir)); } - await this.createInvites(people, this.config.conference.supportRooms.specialInterest); + invitesSent += await this.createInvites(people, this.config.conference.supportRooms.specialInterest); } else { - await runRoleCommand((_client, room, people) => this.ensureInvited(room, people), this.conference, this.client, managementRoomId, event, args); + await runRoleCommand(async (_client, room, people) => { + invitesSent += await this.ensureInvited(room, people); + }, this.conference, this.client, managementRoomId, event, args); } - await this.client.sendNotice(managementRoomId, "Invites sent!"); + await this.client.sendNotice(managementRoomId, `${invitesSent} invites sent!`); } - public async ensureInvited(roomId: string, people: ResolvedPersonIdentifier[]) { + public async ensureInvited(roomId: string, people: ResolvedPersonIdentifier[]): Promise { // We don't want to invite anyone we have already invited or that has joined though, so // avoid those people. We do this by querying the room state and filtering. - let state; + let invitesSent = 0; + let state: any[]; try { state = await this.client.getRoomState(roomId); } @@ -116,10 +121,13 @@ export class InviteCommand implements ICommand { if (emailInvitePersonIds.includes(target.person.id)) continue; try { await invitePersonToRoom(this.client, target, roomId, this.config); + ++invitesSent; } catch (e) { LogService.error("InviteCommand", e); await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${target.mxid}/${target.emails} / ${target.person.id} to ${roomId} - ignoring: ${e.message ?? e.statusMessage ?? '(see logs)'}`, this.client); } } + + return invitesSent; } } From 821267895f9378e719dbb36cdb195f271e71850a Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Sat, 3 Feb 2024 12:12:56 +0000 Subject: [PATCH 2/5] Retry after being ratelimited --- src/commands/InviteCommand.ts | 21 +++++++++++++++------ src/utils.ts | 4 ++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/commands/InviteCommand.ts b/src/commands/InviteCommand.ts index 68d3e96..6952cfe 100644 --- a/src/commands/InviteCommand.ts +++ b/src/commands/InviteCommand.ts @@ -24,6 +24,7 @@ import { logMessage } from "../LogProxy"; import { IPerson, Role } from "../models/schedule"; import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; import { IConfig } from "../config"; +import { sleep } from "../utils"; export class InviteCommand implements ICommand { public readonly prefixes = ["invite", "inv"]; @@ -119,12 +120,20 @@ export class InviteCommand implements ICommand { for (const target of people) { if (target.mxid && effectiveJoinedUserIds.includes(target.mxid)) continue; if (emailInvitePersonIds.includes(target.person.id)) continue; - try { - await invitePersonToRoom(this.client, target, roomId, this.config); - ++invitesSent; - } catch (e) { - LogService.error("InviteCommand", e); - await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${target.mxid}/${target.emails} / ${target.person.id} to ${roomId} - ignoring: ${e.message ?? e.statusMessage ?? '(see logs)'}`, this.client); + for (let attempt = 0; attempt < 3; ++attempt) { + try { + await invitePersonToRoom(this.client, target, roomId, this.config); + ++invitesSent; + } catch (e) { + if (e.statusCode === 429) { + // HACK Retry after ratelimits + await sleep(301_000); + continue; + } + LogService.error("InviteCommand", e); + await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${target.mxid}/${target.emails} / ${target.person.id} to ${roomId} - ignoring: ${e.message ?? e.statusMessage ?? '(see logs)'}`, this.client); + } + break; } } diff --git a/src/utils.ts b/src/utils.ts index 25bde5a..2c927ee 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -245,3 +245,7 @@ export function jsonReplacerMapToObject(_key: any, input: any): any { } return input; } + +export function sleep(millis: number): Promise { + return new Promise(resolve => setTimeout(resolve, millis)); +} From 9e55595bfe04e1a61880b34b3c2421ee57fd4095 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Sat, 3 Feb 2024 12:52:34 +0000 Subject: [PATCH 3/5] (add docstring to invitePersonToRoom) --- src/invites.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invites.ts b/src/invites.ts index 7a5ea17..f5960f1 100644 --- a/src/invites.ts +++ b/src/invites.ts @@ -79,6 +79,11 @@ export async function resolveIdentifiers(client: ConferenceMatrixClient, people: return resolved; } +/** + * Invites a person to a room idempotently. + * + * Raises an exception when we don't have information to invite the user, or there is some Matrix or network error preventing us from doing so. + */ export async function invitePersonToRoom(client: ConferenceMatrixClient, resolvedPerson: ResolvedPersonIdentifier, roomId: string, config: IConfig): Promise { if (resolvedPerson.mxid) { if (config.dry_run_enabled) { From f875bf3eb94a198cf1a943dc0599709ed82e497c Mon Sep 17 00:00:00 2001 From: reivilibre Date: Mon, 3 Jun 2024 11:57:11 +0100 Subject: [PATCH 4/5] Update src/invites.ts Co-authored-by: Will Hunt --- src/invites.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invites.ts b/src/invites.ts index f5960f1..7cf57ca 100644 --- a/src/invites.ts +++ b/src/invites.ts @@ -82,7 +82,7 @@ export async function resolveIdentifiers(client: ConferenceMatrixClient, people: /** * Invites a person to a room idempotently. * - * Raises an exception when we don't have information to invite the user, or there is some Matrix or network error preventing us from doing so. + * @throws an exception when we don't have information to invite the user, or there is some Matrix or network error preventing us from doing so. */ export async function invitePersonToRoom(client: ConferenceMatrixClient, resolvedPerson: ResolvedPersonIdentifier, roomId: string, config: IConfig): Promise { if (resolvedPerson.mxid) { From eb928ed3d5da65535782d9d4662825f85a366578 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Fri, 7 Jun 2024 12:32:21 +0100 Subject: [PATCH 5/5] Use retryAfterMs if available --- src/commands/InviteCommand.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/commands/InviteCommand.ts b/src/commands/InviteCommand.ts index 6952cfe..b15d35e 100644 --- a/src/commands/InviteCommand.ts +++ b/src/commands/InviteCommand.ts @@ -126,8 +126,13 @@ export class InviteCommand implements ICommand { ++invitesSent; } catch (e) { if (e.statusCode === 429) { - // HACK Retry after ratelimits - await sleep(301_000); + // Retry after ratelimits + // Add 1 second to the ratelimit just to ensure we don't retry too quickly + // due to clock drift or a very small requested wait. + // If no retry time set, use 5 minutes. + let delay = (e.retryAfterMs ?? 300_000) + 1_000; + + await sleep(delay); continue; } LogService.error("InviteCommand", e);