diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 00000000..73cf97e4 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,79 @@ +name: E2E tests + +on: + push: + branches: ["master"] + pull_request: + workflow_dispatch: + +jobs: + build-homerunner: + runs-on: ubuntu-latest + outputs: + homerunnersha: ${{ steps.gitsha.outputs.sha }} + steps: + - name: Checkout matrix-org/complement + uses: actions/checkout@v3 + with: + repository: matrix-org/complement + - name: Get complement git sha + id: gitsha + run: echo sha=`git rev-parse --short HEAD` >> "$GITHUB_OUTPUT" + - name: Cache homerunner + id: cached + uses: actions/cache@v3 + with: + path: homerunner + key: ${{ runner.os }}-homerunner-${{ steps.gitsha.outputs.sha }} + - name: "Set Go Version" + if: ${{ steps.cached.outputs.cache-hit != 'true' }} + run: | + echo "$GOROOT_1_18_X64/bin" >> $GITHUB_PATH + echo "~/go/bin" >> $GITHUB_PATH + # Build and install homerunner + - name: Install Complement Dependencies + if: ${{ steps.cached.outputs.cache-hit != 'true' }} + run: | + sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev + - name: Build homerunner + if: ${{ steps.cached.outputs.cache-hit != 'true' }} + run: | + go build ./cmd/homerunner + + + integration-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: + - build-homerunner + steps: + - name: Install Complement Dependencies + run: | + sudo apt-get update && sudo apt-get install -y libolm3 + - name: Load cached homerunner bin + uses: actions/cache@v3 + with: + path: homerunner + key: ${{ runner.os }}-homerunner-${{ needs.build-synapse.outputs.homerunnersha }} + fail-on-cache-miss: true # Shouldn't happen, we build this in the needs step. + - name: Checkout conference-bot + uses: actions/checkout@v3 + with: + path: conference-bot + # Setup node & run tests + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: conference-bot/.node-version + - name: Run Homerunner tests + timeout-minutes: 10 + env: + HOMERUNNER_IMAGE: ghcr.io/matrix-org/synapse/complement-synapse:latest + HOMERUNNER_SPAWN_HS_TIMEOUT_SECS: 100 + NODE_OPTIONS: --dns-result-order ipv4first + run: | + docker pull $HOMERUNNER_IMAGE + cd conference-bot + yarn --strict-semver --frozen-lockfile + ../homerunner & + bash -ic 'yarn test:e2e' diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..3c032078 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18 diff --git a/package.json b/package.json index a8ba1b5b..53f3b875 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "build:ts": "tsc", "build:web": "webpack", "run:merge-roles": "yarn build && node lib/scripts/merge-roles.js", - "test": "jest" + "test": "jest src/__tests__", + "test:e2e": "jest spec/" }, "dependencies": { "@tsconfig/node18": "^1.0.1", @@ -33,7 +34,7 @@ "js-yaml": "^3.14.1", "jsrsasign": "^10.1.4", "liquidjs": "^9.19.0", - "matrix-bot-sdk": "^0.6.3", + "matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.7-element.1", "matrix-widget-api": "^0.1.0-beta.11", "moment": "^2.29.4", "node-fetch": "^2.6.1", @@ -48,12 +49,13 @@ "@types/pg": "^7.14.7", "clean-webpack-plugin": "^3.0.0", "css-loader": "^5.0.1", + "homerunner-client": "^0.0.6", "html-webpack-plugin": "^4.5.1", - "jest": "^29.3.1", + "jest": "^29.7.0", "json-schema-to-typescript": "^11.0.2", "postcss-loader": "^4.1.0", "style-loader": "^2.0.0", - "ts-jest": "^29.0.3", + "ts-jest": "^29.1.1", "ts-loader": "^9.4.2", "typescript": "^4.9.4", "webpack": "^5.12.1", diff --git a/spec/basic.spec.ts b/spec/basic.spec.ts new file mode 100644 index 00000000..b088fa16 --- /dev/null +++ b/spec/basic.spec.ts @@ -0,0 +1,48 @@ +import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test"; +import { describe, it, beforeEach, afterEach, expect } from "@jest/globals"; + + +describe('Basic test setup', () => { + let testEnv: E2ETestEnv; + beforeEach(async () => { + testEnv = await E2ETestEnv.createTestEnv({ + fixture: 'basic-conference', + }); + await testEnv.setUp(); + }, E2ESetupTestTimeout); + afterEach(() => { + return testEnv?.tearDown(); + }); + it('should start up successfully', async () => { + const { event } = await testEnv.sendAdminCommand('!conference status'); + console.log(event.content.body); + // Check that we're generally okay. + expect(event.content.body).toMatch('Scheduled tasks yet to run: 0'); + expect(event.content.body).toMatch('Schedule source healthy: true'); + }); + it('should be able to build successfully', async () => { + let spaceBuilt, supportRoomsBuilt, conferenceBuilt = false; + const waitForFinish = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error( + `Build incomplete. spaceBuild: ${spaceBuilt}, supportRoomsBuilt: ${supportRoomsBuilt}, conferenceBuilt: ${conferenceBuilt}` + )), 5000); + testEnv.adminClient.on('room.message', (_, event) => { + if (event.content.body.includes("Your conference's space is at")) { + spaceBuilt = true; + } else if (event.content.body.includes("Support rooms have been created")) { + supportRoomsBuilt = true; + } else if (event.content.body.includes("CONFERENCE BUILT")) { + conferenceBuilt = true; + } + + if (spaceBuilt && supportRoomsBuilt && conferenceBuilt) { + resolve(); + clearTimeout(timeout); + } + }) + }); + await testEnv.sendAdminCommand('!conference build'); + await waitForFinish; + // TODO: Now test that all the expected rooms are there. + }, 7000); +}); diff --git a/spec/fixtures/basic-conference.json b/spec/fixtures/basic-conference.json new file mode 100644 index 00000000..376559aa --- /dev/null +++ b/spec/fixtures/basic-conference.json @@ -0,0 +1,19 @@ +{ + "title": "Testing Conference", + "streams": [{ + "stream_name": "Main Stream", + "talks": [{ + "id": 1, + "title": "Main Talk 1", + "description": "First talk", + "start": "2019-10-12T07:20:50.52Z", + "end": "2019-10-12T07:21:50.52Z", + "tracks": ["main_track"], + "speakers": [{ + "display_name": "Alice", + "matrix_id": "@alice:example.com", + "email": "@alice:example.com" + }] + }] + }] +} \ No newline at end of file diff --git a/spec/util/e2e-test.ts b/spec/util/e2e-test.ts new file mode 100644 index 00000000..cd924d49 --- /dev/null +++ b/spec/util/e2e-test.ts @@ -0,0 +1,235 @@ +import { ComplementHomeServer, createHS, destroyHS } from "./homerunner"; +import { MatrixClient, PowerLevelsEventContent, RoomEvent, TextualMessageEventContent } from "matrix-bot-sdk"; +import dns from 'node:dns'; +import { mkdtemp, rm } from "node:fs/promises"; +import { IConfig } from "../../src/config"; +import { ConferenceBot } from "../../src/index"; +import path from "node:path"; + +const WAIT_EVENT_TIMEOUT = 10000; +export const E2ESetupTestTimeout = 60000; + +interface Opts { + matrixLocalparts?: string[]; + fixture: string; + timeout?: number; + config?: Partial, + traceToFile?: boolean, +} + +export class E2ETestMatrixClient extends MatrixClient { + + public async waitForPowerLevel( + roomId: string, expected: Partial, + ): Promise<{roomId: string, data: { + sender: string, type: string, state_key?: string, content: PowerLevelsEventContent, event_id: string, + }}> { + return this.waitForEvent('room.event', (eventRoomId: string, eventData: { + sender: string, type: string, content: Record, event_id: string, state_key: string, + }) => { + if (eventRoomId !== roomId) { + return undefined; + } + + if (eventData.type !== "m.room.power_levels") { + return undefined; + } + + if (eventData.state_key !== "") { + return undefined; + } + + // Check only the keys we care about + for (const [key, value] of Object.entries(expected)) { + const evValue = eventData.content[key] ?? undefined; + const sortOrder = value !== null && typeof value === "object" ? Object.keys(value).sort() : undefined; + const jsonLeft = JSON.stringify(evValue, sortOrder); + const jsonRight = JSON.stringify(value, sortOrder); + if (jsonLeft !== jsonRight) { + return undefined; + } + } + + console.info( + // eslint-disable-next-line max-len + `${eventRoomId} ${eventData.event_id} ${eventData.sender}` + ); + return {roomId: eventRoomId, data: eventData}; + }, `Timed out waiting for powerlevel from in ${roomId}`) + } + + public async waitForRoomEvent>( + opts: {eventType: string, sender: string, roomId?: string, stateKey?: string, body?: string} + ): Promise<{roomId: string, data: { + sender: string, type: string, state_key?: string, content: T, event_id: string, + }}> { + const {eventType, sender, roomId, stateKey} = opts; + return this.waitForEvent('room.event', (eventRoomId: string, eventData: { + sender: string, type: string, state_key?: string, content: T, event_id: string, + }) => { + if (eventData.sender !== sender) { + return undefined; + } + if (eventData.type !== eventType) { + return undefined; + } + if (roomId && eventRoomId !== roomId) { + return undefined; + } + if (stateKey !== undefined && eventData.state_key !== stateKey) { + return undefined; + } + const body = 'body' in eventData.content && eventData.content.body; + if (opts.body && body !== opts.body) { + return undefined; + } + console.info( + // eslint-disable-next-line max-len + `${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? body ?? ''}` + ); + return {roomId: eventRoomId, data: eventData}; + }, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`) + } + + public async waitForRoomInvite( + opts: {sender: string, roomId?: string} + ): Promise<{roomId: string, data: unknown}> { + const {sender, roomId} = opts; + return this.waitForEvent('room.invite', (eventRoomId: string, eventData: { + sender: string + }) => { + const inviteSender = eventData.sender; + console.info(`Got invite to ${eventRoomId} from ${inviteSender}`); + if (eventData.sender !== sender) { + return undefined; + } + if (roomId && eventRoomId !== roomId) { + return undefined; + } + return {roomId: eventRoomId, data: eventData}; + }, `Timed out waiting for invite to ${roomId || "any room"} from ${sender}`) + } + + public async waitForEvent( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + emitterType: string, filterFn: (...args: any[]) => T|undefined, timeoutMsg: string) + : Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line prefer-const + let timer: NodeJS.Timeout; + const fn = (...args: unknown[]) => { + const data = filterFn(...args); + if (data) { + clearTimeout(timer); + resolve(data); + } + }; + timer = setTimeout(() => { + this.removeListener(emitterType, fn); + reject(new Error(timeoutMsg)); + }, WAIT_EVENT_TIMEOUT); + this.on(emitterType, fn) + }); + } +} + +export class E2ETestEnv { + static async createTestEnv(opts): Promise { + const workerID = parseInt(process.env.JEST_WORKER_ID ?? '0'); + const { matrixLocalparts, config: providedConfig } = opts; + const tmpDir = await mkdtemp('confbot-test'); + + // Configure homeserver and bots + const homeserver = await createHS(["conf_bot", "admin", "modbot", ...matrixLocalparts || []], workerID); + const confBotOpts = homeserver.users.find(u => u.userId === `@conf_bot:${homeserver.domain}`); + if (!confBotOpts) { + throw Error('No conf_bot setup on homeserver'); + } + const adminUser = homeserver.users.find(u => u.userId === `@admin:${homeserver.domain}`); + if (!adminUser) { + throw Error('No admin setup on homeserver'); + } + const mgmntRoom = await confBotOpts.client.createRoom({ invite: [adminUser.userId]}); + await adminUser.client.joinRoom(mgmntRoom); + + // Configure JSON schedule + const scheduleDefinition = path.resolve(__dirname, '..', 'fixtures', opts.fixture + ".json"); + + const config = { + ...providedConfig, + conference: { + id: 'test-conf', + name: 'Test Conf', + supportRooms: { + speakers: `#speakers:${homeserver.domain}`, + coordinators: `#coordinators:${homeserver.domain}`, + specialInterest: `#specialInterest:${homeserver.domain}`, + }, + prefixes: { + auditoriumRooms: ["D."], + interestRooms: ["S.", "B."], + aliases: "", + displayNameSuffixes: {}, + suffixes: {}, + }, + schedule: { + backend: 'json', + scheduleDefinition, + }, + subspaces: { + mysubspace: { + displayName: 'My Subspace', + alias: 'mysubspace', + prefixes: [] + } + }, + }, + moderatorUserId: `@modbot:${homeserver.domain}`, + webserver: { + additionalAssetsPath: '/dev/null' + }, + ircBridge: null, + homeserverUrl: homeserver.url, + accessToken: confBotOpts.accessToken, + userId: confBotOpts.userId, + dataPath: tmpDir, + managementRoom: mgmntRoom, + } as IConfig; + const conferenceBot = await ConferenceBot.start(config); + return new E2ETestEnv(homeserver, conferenceBot, adminUser.client, opts, tmpDir, config); + } + + private constructor( + public readonly homeserver: ComplementHomeServer, + public confBot: ConferenceBot, + public readonly adminClient: MatrixClient, + public readonly opts: Opts, + private readonly dataDir: string, + private readonly config: IConfig, + ) { } + + public async setUp(): Promise { + await this.confBot.main(); + } + + public async tearDown(): Promise { + await this.confBot.stop(); + this.homeserver.users.forEach(u => u.client.stop()); + await destroyHS(this.homeserver.id); + await rm(this.dataDir, { recursive: true, force: true }) + } + + public async sendAdminCommand(cmd: string) { + const response = new Promise<{roomId: string, event: RoomEvent}>((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Timed out waiting for admin response")), 5000); + this.adminClient.on('room.message', (roomId, event) => { + if (event.sender === this.config.userId) { + resolve({roomId, event: new RoomEvent(event)}); + clearTimeout(timeout); + } + }); + }); + await this.adminClient.sendText(this.config.managementRoom, cmd); + return response; + } +} diff --git a/spec/util/homerunner.ts b/spec/util/homerunner.ts new file mode 100644 index 00000000..bc71946f --- /dev/null +++ b/spec/util/homerunner.ts @@ -0,0 +1,119 @@ +import { MatrixClient } from "matrix-bot-sdk"; +import { createHash, createHmac } from "crypto"; +import { Homerunner } from "homerunner-client"; +import { default as fetch } from 'node-fetch'; +import { E2ETestMatrixClient } from "./e2e-test"; + +const HOMERUNNER_IMAGE = process.env.HOMERUNNER_IMAGE || 'complement-synapse'; +export const DEFAULT_REGISTRATION_SHARED_SECRET = ( + process.env.REGISTRATION_SHARED_SECRET || 'complement' +); + +const homerunner = new Homerunner.Client(); + +export interface ComplementHomeServer { + id: string, + url: string, + domain: string, + users: {userId: string, accessToken: string, deviceId: string, client: E2ETestMatrixClient}[] +} + +// Ensure we don't clash with other tests. + +async function waitForHomerunner() { + + // Check if port is in use. + + // Needs https://github.com/matrix-org/complement/issues/398 + let attempts = 0; + do { + attempts++; + // Not ready yet. + console.log(`Waiting for homerunner to be ready (${attempts}/100)`); + try { + await homerunner.health(); + break; + } + catch (ex) { + await new Promise(r => setTimeout(r, 1000)); + } + } while (attempts < 100) + if (attempts === 100) { + throw Error('Homerunner was not ready after 100 attempts'); + } +} + +export async function createHS(localparts: string[] = [], workerId: number): Promise { + const appPort = 49152 + workerId; + await waitForHomerunner(); + const blueprint = `confbot_integration_test_${Date.now()}`; + + const blueprintResponse = await homerunner.create({ + base_image_uri: HOMERUNNER_IMAGE, + blueprint: { + Name: blueprint, + Homeservers: [{ + Name: 'confbot', + Users: localparts.map(localpart => ({Localpart: localpart, DisplayName: localpart})), + }], + } + }); + const [homeserverName, homeserver] = Object.entries(blueprintResponse.homeservers)[0]; + const users = Object.entries(homeserver.AccessTokens).map(([userId, accessToken]) => ({ + userId: userId, + accessToken, + deviceId: homeserver.DeviceIDs[userId], + client: new E2ETestMatrixClient(homeserver.BaseURL, accessToken), + })); + + // Start syncing proactively. + await Promise.all(users.map(u => u.client.start())); + return { + users, + id: blueprint, + url: homeserver.BaseURL, + domain: homeserverName + }; +} + +export function destroyHS( + id: string +): Promise { + return homerunner.destroy(id); +} + +export async function registerUser( + homeserverUrl: string, + user: { username: string, admin: boolean }, + sharedSecret = DEFAULT_REGISTRATION_SHARED_SECRET, +): Promise<{mxid: string, client: MatrixClient}> { + const registerUrl: string = (() => { + const url = new URL(homeserverUrl); + url.pathname = '/_synapse/admin/v1/register'; + return url.toString(); + })(); + + const nonce = await fetch(registerUrl, { method: 'GET' }).then(res => res.json()).then(res => res.nonce); + const password = createHash('sha256') + .update(user.username) + .update(sharedSecret) + .digest('hex'); + const hmac = createHmac('sha1', sharedSecret) + .update(nonce).update("\x00") + .update(user.username).update("\x00") + .update(password).update("\x00") + .update(user.admin ? 'admin' : 'notadmin') + .digest('hex'); + return await fetch(registerUrl, { method: "POST", body: JSON.stringify( + { + nonce, + username: user.username, + password, + admin: user.admin, + mac: hmac, + } + )}).then(res => res.json()).then(res => ({ + mxid: res.user_id, + client: new MatrixClient(homeserverUrl, res.access_token), + })).catch(err => { console.log(err.response.body); throw new Error(`Failed to register user: ${err}`); }); +} diff --git a/src/CheckInMap.ts b/src/CheckInMap.ts index 51848282..73dae6bd 100644 --- a/src/CheckInMap.ts +++ b/src/CheckInMap.ts @@ -16,10 +16,9 @@ limitations under the License. import { LogService, MatrixClient } from "matrix-bot-sdk"; import AwaitLock from "await-lock"; -import { Conference } from "./Conference"; import {promises as fs} from "fs"; import * as path from "path"; -import config from "./config"; +import { IConfig } from "./config"; interface ICheckin { expires: number; @@ -31,7 +30,7 @@ export class CheckInMap { private checkedIn: { [userId: string]: ICheckin } = {}; private lock = new AwaitLock(); - constructor(private client: MatrixClient, private conference: Conference) { + constructor(client: MatrixClient, private readonly config: IConfig) { client.on('room.event', async (roomId: string, event: any) => { if (!this.checkedIn[event['sender']]) return; @@ -49,13 +48,13 @@ export class CheckInMap { } private async persist() { - await fs.writeFile(path.join(config.dataPath, "checkins.json"), JSON.stringify(this.checkedIn), "utf-8"); + await fs.writeFile(path.join(this.config.dataPath, "checkins.json"), JSON.stringify(this.checkedIn), "utf-8"); } private async load() { try { await this.lock.acquireAsync(); - const str = await fs.readFile(path.join(config.dataPath, "checkins.json"), "utf-8"); + const str = await fs.readFile(path.join(this.config.dataPath, "checkins.json"), "utf-8"); this.checkedIn = JSON.parse(str || "{}"); } catch (e) { LogService.error("CheckInMap", e); diff --git a/src/Conference.ts b/src/Conference.ts index 01a0230b..9091b7fe 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { LogLevel, LogService, MatrixClient, RoomAlias, Space } from "matrix-bot-sdk"; +import { LogLevel, LogService, RoomAlias, Space } from "matrix-bot-sdk"; import { AUDITORIUM_BACKSTAGE_CREATION_TEMPLATE, AUDITORIUM_CREATION_TEMPLATE, @@ -43,9 +43,9 @@ import { RS_STORED_PERSON, RS_STORED_SUBSPACE, } from "./models/room_state"; -import { makeDisplayName, objectFastClone, safeCreateRoom } from "./utils"; -import { addAndDeleteManagedAliases, applyAllAliasPrefixes, assignAliasVariations, calculateAliasVariations, makeLocalpart } from "./utils/aliases"; -import config from "./config"; +import { applySuffixRules, objectFastClone, safeCreateRoom } from "./utils"; +import { addAndDeleteManagedAliases, applyAllAliasPrefixes, assignAliasVariations, calculateAliasVariations } from "./utils/aliases"; +import { IConfig } from "./config"; import { MatrixRoom } from "./models/MatrixRoom"; import { Auditorium, AuditoriumBackstage } from "./models/Auditorium"; import { Talk } from "./models/Talk"; @@ -58,6 +58,7 @@ import { logMessage } from "./LogProxy"; import { IScheduleBackend } from "./backends/IScheduleBackend"; import { PentaBackend } from "./backends/penta/PentaBackend"; import { setUnion } from "./utils/sets"; +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; export class Conference { private rootSpace: Space | null; @@ -87,7 +88,7 @@ export class Conference { [personId: string]: IPerson; } = {}; - constructor(public readonly backend: IScheduleBackend, public readonly id: string, public readonly client: MatrixClient) { + 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']) { const emailInviteToken = event['content']['third_party_invite']['signed']?.['token']; @@ -116,19 +117,19 @@ export class Conference { const aud = this.storedAuditoriums.find(a => a.roomId === roomId); if (aud) { const mods = await this.getModeratorsForAuditorium(aud); - const resolved = await resolveIdentifiers(mods); + const resolved = await resolveIdentifiers(this.client, mods); await PermissionsCommand.ensureModerator(this.client, roomId, resolved); } else { const audBackstage = this.storedAuditoriumBackstages.find(a => a.roomId === roomId); if (audBackstage) { const mods = await this.getModeratorsForAuditorium(audBackstage); - const resolved = await resolveIdentifiers(mods); + const resolved = await resolveIdentifiers(this.client, mods); await PermissionsCommand.ensureModerator(this.client, roomId, resolved); } else { const talk = this.storedTalks.find(a => a.roomId === roomId); if (talk) { const mods = await this.getModeratorsForTalk(talk); - const resolved = await resolveIdentifiers(mods); + const resolved = await resolveIdentifiers(this.client, mods); await PermissionsCommand.ensureModerator(this.client, roomId, resolved); } } @@ -239,7 +240,7 @@ export class Conference { 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.interestRooms[interestId] = new InterestRoom(roomId, this.client, this, interestId, this.config.conference.prefixes); } break; default: @@ -252,12 +253,12 @@ export class Conference { } // Resolve pre-existing interest rooms - for (const interestId in config.conference.existingInterestRooms) { + for (const interestId in this.config.conference.existingInterestRooms) { if (interestId in this.interestRooms) { continue; } - const roomIdOrAlias = config.conference.existingInterestRooms[interestId]; + const roomIdOrAlias = this.config.conference.existingInterestRooms[interestId]; let roomId: string; try { roomId = await this.client.resolveRoom(roomIdOrAlias); @@ -266,7 +267,7 @@ export class Conference { continue; } - this.interestRooms[interestId] = new InterestRoom(roomId, this.client, this, interestId); + this.interestRooms[interestId] = new InterestRoom(roomId, this.client, this, interestId, this.config.conference.prefixes); } // Locate other metadata in the room @@ -296,11 +297,11 @@ export class Conference { if (! this.rootSpace) { const space = await this.client.createSpace({ isPublic: true, - localpart: config.conference.id, - name: config.conference.name, + localpart: this.config.conference.id, + name: this.config.conference.name, }); - const spaceLocator = makeRootSpaceLocator(config.conference.id); + const spaceLocator = makeRootSpaceLocator(this.config.conference.id); await this.client.sendStateEvent(space.roomId, spaceLocator.type, spaceLocator.state_key, spaceLocator.content); // Ensure that the space can be viewed by guest users. @@ -350,9 +351,9 @@ export class Conference { */ public async createSupportRooms() { const roomAliases = [ - config.conference.supportRooms.speakers, - config.conference.supportRooms.specialInterest, - config.conference.supportRooms.coordinators, + this.config.conference.supportRooms.speakers, + this.config.conference.supportRooms.specialInterest, + this.config.conference.supportRooms.coordinators, ]; const rootSpace = await this.getSpace(); @@ -372,7 +373,7 @@ export class Conference { this.client, mergeWithCreationTemplate(AUDITORIUM_BACKSTAGE_CREATION_TEMPLATE, { room_alias_name: (new RoomAlias(alias)).localpart, - invite: [config.moderatorUserId], + invite: [this.config.moderatorUserId], }), ); await rootSpace.addChildRoom(roomId); @@ -389,7 +390,7 @@ export class Conference { } /** - * Creates a subspace defined in the bot config. + * Creates a subspace defined in the bot this.config. * @param subspaceId The id of the subspace. * @param name The display name of the subspace. * @param aliasLocalpart The localpart of the subspace's alias. @@ -411,14 +412,15 @@ export class Conference { subspace = await this.client.createSpace({ isPublic: true, name: name, - invites: [config.moderatorUserId], + invites: [this.config.moderatorUserId], }); this.subspaces[subspaceId] = subspace; await assignAliasVariations( this.client, subspace.roomId, - applyAllAliasPrefixes("space-" + aliasLocalpart, config.conference.prefixes.aliases) + applyAllAliasPrefixes("space-" + aliasLocalpart, this.config.conference.prefixes.aliases), + this.config.conference.prefixes.suffixes, ); await this.client.sendStateEvent(this.dbRoom.roomId, RS_STORED_SUBSPACE, subspaceId, { @@ -427,7 +429,7 @@ export class Conference { // Grants PL100 to the moderator in the subspace. // We can't do this directly with `createSpace` unfortunately, as we could for plain rooms. - await this.client.setUserPowerLevel(config.moderatorUserId, subspace.roomId, 100); + await this.client.setUserPowerLevel(this.config.moderatorUserId, subspace.roomId, 100); } else { subspace = this.subspaces[subspaceId]; } @@ -449,16 +451,16 @@ export class Conference { public async createInterestRoom(interestRoom: IInterestRoom): Promise { let roomId: string; if (!this.interestRooms[interestRoom.id]) { - if (interestRoom.id in config.conference.existingInterestRooms) { + if (interestRoom.id in this.config.conference.existingInterestRooms) { // Resolve a pre-existing room that has been created after the bot started up. - const roomIdOrAlias = config.conference.existingInterestRooms[interestRoom.id]; + const roomIdOrAlias = this.config.conference.existingInterestRooms[interestRoom.id]; roomId = await this.client.resolveRoom(roomIdOrAlias); this.interestRooms[interestRoom.id] = new InterestRoom( - roomId, this.client, this, interestRoom.id + roomId, this.client, this, interestRoom.id, this.config.conference.prefixes ); } else { // Create a new interest room. - roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(SPECIAL_INTEREST_CREATION_TEMPLATE, { + roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(SPECIAL_INTEREST_CREATION_TEMPLATE(this.config.moderatorUserId), { creation_content: { [RSC_CONFERENCE_ID]: this.id, [RSC_SPECIAL_INTEREST_ID]: interestRoom.id, @@ -467,12 +469,14 @@ export class Conference { makeInterestLocator(this.id, interestRoom.id), ], })); - await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(interestRoom.name, config.conference.prefixes.aliases), interestRoom.id); + await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(interestRoom.name, this.config.conference.prefixes.aliases), + this.config.conference.prefixes.suffixes, interestRoom.id); this.interestRooms[interestRoom.id] = new InterestRoom( roomId, this.client, this, interestRoom.id, + this.config.conference.prefixes, ); } } else { @@ -493,7 +497,7 @@ export class Conference { await parentSpace.addChildRoom(roomId, { order: `interest-${interestRoom.id}` }); // In the future we may want to ensure that aliases are set in accordance with the - // config. + // this.config. return this.interestRooms[interestRoom.id]; } @@ -519,13 +523,16 @@ export class Conference { try { audSpace = await this.client.createSpace({ isPublic: true, - name: makeDisplayName(auditorium.name, auditorium.id), + name: applySuffixRules( + auditorium.name, auditorium.id, this.config.conference.prefixes.displayNameSuffixes + ), }); await assignAliasVariations( this.client, audSpace.roomId, - applyAllAliasPrefixes("space-" + auditorium.slug, config.conference.prefixes.aliases), + applyAllAliasPrefixes("space-" + auditorium.slug, this.config.conference.prefixes.aliases), + this.config.conference.prefixes.suffixes, auditorium.id ); @@ -537,13 +544,13 @@ export class Conference { {guest_access:"can_join"}, ); } catch (e) { - await logMessage(LogLevel.ERROR, "utils", `Can't create auditorium space for ${auditorium.slug}: ${e}!`); + await logMessage(LogLevel.ERROR, "utils", `Can't create auditorium space for ${auditorium.slug}: ${e}!`, this.client); throw e; } await parentSpace.addChildSpace(audSpace, { order: `auditorium-${auditorium.id}` }); - const roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(AUDITORIUM_CREATION_TEMPLATE, { + const roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(AUDITORIUM_CREATION_TEMPLATE(this.config.moderatorUserId), { creation_content: { [RSC_CONFERENCE_ID]: this.id, [RSC_AUDITORIUM_ID]: auditorium.id, @@ -554,7 +561,8 @@ export class Conference { ], name: auditorium.name, })); - await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(auditorium.slug, config.conference.prefixes.aliases), auditorium.id); + await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(auditorium.slug, this.config.conference.prefixes.aliases), + this.config.conference.prefixes.suffixes, auditorium.id); this.auditoriums[auditorium.id] = new Auditorium(roomId, auditorium, this.client, this); // TODO: Send widgets after room creation @@ -584,7 +592,8 @@ export class Conference { makeAuditoriumBackstageLocator(this.id, auditorium.id), ], })); - await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(auditorium.slug + "-backstage", config.conference.prefixes.aliases), auditorium.id); + await assignAliasVariations(this.client, roomId, applyAllAliasPrefixes(auditorium.slug + "-backstage", this.config.conference.prefixes.aliases), + this.config.conference.prefixes.suffixes, auditorium.id); this.auditoriumBackstages[auditorium.id] = new AuditoriumBackstage(roomId, auditorium, this.client, this); return this.auditoriumBackstages[auditorium.id]; @@ -599,7 +608,7 @@ export class Conference { } if (!this.talks[talk.id]) { - roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(TALK_CREATION_TEMPLATE, { + roomId = await safeCreateRoom(this.client, mergeWithCreationTemplate(TALK_CREATION_TEMPLATE(this.config.moderatorUserId), { name: talk.title, creation_content: { [RSC_CONFERENCE_ID]: this.id, @@ -623,9 +632,10 @@ export class Conference { (await auditorium.getSlug()) + '-' + talk.slug, 'talk-' + talk.id, ]; - const wantedPrefixedNames = wantedBaseNames.flatMap(baseName => applyAllAliasPrefixes(baseName, config.conference.prefixes.aliases)); + const wantedPrefixedNames = wantedBaseNames.flatMap(baseName => applyAllAliasPrefixes(baseName, this.config.conference.prefixes.aliases)); const allAliasVariantsToAssign = wantedPrefixedNames - .map(a => calculateAliasVariations(a)) + .map(a => calculateAliasVariations(a, + this.config.conference.prefixes.suffixes)) .reduce((setLeft, setRight) => setUnion(setLeft, setRight)); await addAndDeleteManagedAliases(this.client, roomId, allAliasVariantsToAssign); @@ -666,7 +676,7 @@ export class Conference { const id = auditoriumOrInterestRoom.id; - for (const [subspaceId, subspaceConfig] of Object.entries(config.conference.subspaces)) { + for (const [subspaceId, subspaceConfig] of Object.entries(this.config.conference.subspaces)) { for (const prefix of subspaceConfig.prefixes) { if (id.startsWith(prefix)) { if (!(subspaceId in this.subspaces)) { @@ -795,7 +805,7 @@ export class Conference { // we'll be unable to do promotions/demotions in the future. const pls = await this.client.getRoomStateEvent(roomId, "m.room.power_levels", ""); pls['users'][await this.client.getUserId()] = 100; - pls['users'][config.moderatorUserId] = 100; + pls['users'][this.config.moderatorUserId] = 100; for (const userId of mxids) { if (pls['users'][userId]) continue; pls['users'][userId] = 50; diff --git a/src/ConferenceMatrixClient.ts b/src/ConferenceMatrixClient.ts new file mode 100644 index 00000000..db2577b6 --- /dev/null +++ b/src/ConferenceMatrixClient.ts @@ -0,0 +1,25 @@ +import { IStorageProvider, IdentityClient, MatrixClient } from "matrix-bot-sdk"; +import { IConfig } from "./config"; + +export class ConferenceMatrixClient extends MatrixClient { + static async create(confConfig: IConfig, storage?: IStorageProvider) { + let idClient: IdentityClient|undefined; + if (confConfig.idServerDomain) { + idClient = await new MatrixClient(confConfig.homeserverUrl, confConfig.accessToken).getIdentityServerClient(confConfig.idServerDomain); + await idClient.acceptAllTerms(); + if (confConfig.idServerBrand) { + idClient.brand = confConfig.idServerBrand; + } + } + return new ConferenceMatrixClient(confConfig.homeserverUrl, confConfig.accessToken, idClient, confConfig.managementRoom, storage); + } + + constructor( + homeserverUrl: string, + accessToken: string, + public readonly identityClient: IdentityClient|undefined, + public readonly managementRoom: string, + storage?: IStorageProvider) { + super(homeserverUrl, accessToken, storage); + } +} \ No newline at end of file diff --git a/src/IRCBridge.ts b/src/IRCBridge.ts index 3bb7bc4b..d2306669 100644 --- a/src/IRCBridge.ts +++ b/src/IRCBridge.ts @@ -18,8 +18,8 @@ import { MatrixClient, MatrixEvent } from "matrix-bot-sdk"; import * as irc from "irc-upd"; import { Auditorium } from "./models/Auditorium"; import { InterestRoom } from "./models/InterestRoom"; -import config from "./config"; import { makeLocalpart } from "./utils/aliases"; +import { IConfig } from "./config"; export interface IRCBridgeOpts { botNick: string; @@ -45,10 +45,16 @@ export class IRCBridge { private botRoomId?: string; private ircClient: any; - constructor(private readonly config: IRCBridgeOpts, private readonly mxClient: MatrixClient) { - if (!config.botNick || !config.botUserId || !config.channelPrefix || !config.port || !config.serverName) { + private readonly config: IRCBridgeOpts; + constructor(private readonly rootConfig: IConfig, private readonly mxClient: MatrixClient) { + if (!rootConfig.ircBridge) { + throw Error('Missing IRC bridge config'); + } + this.config = rootConfig.ircBridge; + if (!this.config.botNick || !this.config.botUserId || !this.config.channelPrefix || !this.config.port || !this.config.serverName) { throw Error('Missing configuration options for IRC bridge'); } + } public get botUserId() { @@ -64,7 +70,7 @@ export class IRCBridge { } public async deriveChannelNameSI(interest: InterestRoom) { - const name = makeLocalpart(await interest.getName(), await interest.getId()); + const name = makeLocalpart(await interest.getName(), this.rootConfig.conference.prefixes.suffixes, await interest.getId()); if (!name) { throw Error('Special interest name is empty'); } diff --git a/src/LogProxy.ts b/src/LogProxy.ts index f4838c91..98b577bd 100644 --- a/src/LogProxy.ts +++ b/src/LogProxy.ts @@ -17,9 +17,9 @@ limitations under the License. // Borrowed from Mjolnir import { LogLevel, LogService, TextualMessageEventContent } from "matrix-bot-sdk"; -import config from "./config"; import { replaceRoomIdsWithPills } from "./utils"; import * as htmlEscape from "escape-html"; +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; const levelToFn = { [LogLevel.DEBUG.toString()]: LogService.debug, @@ -28,17 +28,16 @@ const levelToFn = { [LogLevel.ERROR.toString()]: LogService.error, }; -export async function logMessage(level: LogLevel, module: string, message: string | any, additionalRoomIds: string[] | string | null = null, isRecursive = false) { +export async function logMessage(level: LogLevel, module: string, message: string | any, client: ConferenceMatrixClient, additionalRoomIds: string[] | string | null = null, isRecursive = false) { if (!additionalRoomIds) additionalRoomIds = []; if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; - if (config.RUNTIME.client && LogLevel.INFO.includes(level)) { + if (LogLevel.INFO.includes(level)) { let clientMessage = message; if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; - const roomIds = [config.managementRoom, ...additionalRoomIds]; - const client = config.RUNTIME.client; + const roomIds = [client.managementRoom, ...additionalRoomIds]; let evContent: TextualMessageEventContent = { body: message, @@ -50,7 +49,7 @@ export async function logMessage(level: LogLevel, module: string, message: strin evContent = await replaceRoomIdsWithPills(client, clientMessage, roomIds, "m.notice"); } - await client.sendMessage(config.managementRoom, evContent); + await client.sendMessage(client.managementRoom, evContent); } levelToFn[level.toString()](module, message); diff --git a/src/Scheduler.ts b/src/Scheduler.ts index c8d2c5d2..bfac5a75 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -17,14 +17,16 @@ limitations under the License. import { Conference } from "./Conference"; import AwaitLock from "await-lock"; import { logMessage } from "./LogProxy"; -import config from "./config"; -import { LogLevel, LogService, MatrixClient, MentionPill } from "matrix-bot-sdk"; +import { LogLevel, LogService, MentionPill } from "matrix-bot-sdk"; import { makeRoomPublic } from "./utils"; import { Scoreboard } from "./Scoreboard"; import { LiveWidget } from "./models/LiveWidget"; import { ResolvedPersonIdentifier, resolveIdentifiers } from "./invites"; import { ITalk, Role } from "./models/schedule"; import { Talk } from "./models/Talk"; +import { CheckInMap } from "./CheckInMap"; +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; +import { IConfig } from "./config"; export enum ScheduledTaskType { TalkStart = "talk_start", @@ -143,8 +145,13 @@ export class Scheduler { private inAuditoriums: string[] = []; private pending: { [taskId: string]: ITask } = {}; private lock = new AwaitLock(); + private nextTaskTimeout?: NodeJS.Timeout; - constructor(private client: MatrixClient, private conference: Conference, private scoreboard: Scoreboard) { + constructor(private readonly client: ConferenceMatrixClient, + private readonly conference: Conference, + private readonly scoreboard: Scoreboard, + private readonly checkins: CheckInMap, + private readonly config: IConfig) { } public async prepare() { @@ -156,7 +163,7 @@ export class Scheduler { this.inAuditoriums.push(...(schedulerData?.inAuditoriums || [])); if (this.inAuditoriums.length) { - await this.client.sendNotice(config.managementRoom, `Running schedule in auditoriums: ${this.inAuditoriums.join(', ')}`); + await this.client.sendNotice(this.config.managementRoom, `Running schedule in auditoriums: ${this.inAuditoriums.join(', ')}`); } await this.runTasks(); @@ -190,7 +197,7 @@ export class Scheduler { await this.lock.acquireAsync(); LogService.info("Scheduler", "Scheduling tasks"); try { - const minVar = config.conference.lookaheadMinutes; + const minVar = this.config.conference.lookaheadMinutes; try { // Refresh upcoming parts of our schedule to ensure it's really up to date. // Rationale: Sometimes schedules get changed at short notice, so we try our best to accommodate that. @@ -237,7 +244,7 @@ export class Scheduler { } catch (e) { LogService.error("Scheduler", e); try { - await logMessage(LogLevel.ERROR, "Scheduler", `Error scheduling tasks: ${e?.message || 'unknown error'}`); + await logMessage(LogLevel.ERROR, "Scheduler", `Error scheduling tasks: ${e?.message || 'unknown error'}`, this.client); } catch (e) { LogService.error("Scheduler", e); } @@ -273,7 +280,7 @@ export class Scheduler { await this._execute(task); } catch (e) { LogService.error("Scheduler", e); - await logMessage(LogLevel.ERROR, "Scheduler", `Error running task ${taskId}: ${e?.message || 'unknown error'}`); + await logMessage(LogLevel.ERROR, "Scheduler", `Error running task ${taskId}: ${e?.message || 'unknown error'}`, this.client); } delete this.pending[taskId]; this.completedIds.push(taskId); @@ -283,7 +290,7 @@ export class Scheduler { } catch (e) { LogService.error("Scheduler", e); try { - await logMessage(LogLevel.ERROR, "Scheduler", `Error running tasks: ${e?.message || 'unknown error'}`); + await logMessage(LogLevel.ERROR, "Scheduler", `Error running tasks: ${e?.message || 'unknown error'}`, this.client); } catch (e) { LogService.error("Scheduler", e); } @@ -294,7 +301,7 @@ export class Scheduler { } catch (e) { LogService.error("Scheduler", e); } - setTimeout(() => this.runTasks(), RUN_INTERVAL_MS); + this.nextTaskTimeout = setTimeout(() => this.runTasks(), RUN_INTERVAL_MS); } /** @@ -379,9 +386,9 @@ export class Scheduler { } else if (task.type === ScheduledTaskType.TalkEnd) { if (confTalk !== undefined) { await this.client.sendHtmlText(confTalk.roomId, `

Your talk has ended - opening up this room to all attendees.

@room - They won't see the history in this room.

`); - const widget = await LiveWidget.forTalk(confTalk, this.client); + const widget = await LiveWidget.forTalk(confTalk, this.client, this.config.livestream.widgetAvatar, this.config.webserver.publicBaseUrl); const layout = await LiveWidget.layoutForTalk(widget, null); - const scoreboard = await LiveWidget.scoreboardForTalk(confTalk, this.client); + const scoreboard = await LiveWidget.scoreboardForTalk(confTalk, this.client, this.conference, this.config.livestream.widgetAvatar, this.config.webserver.publicBaseUrl); await this.client.sendStateEvent(confTalk.roomId, widget.type, widget.state_key, widget.content); await this.client.sendStateEvent(confTalk.roomId, scoreboard.type, scoreboard.state_key, {}); await this.client.sendStateEvent(confTalk.roomId, layout.type, layout.state_key, layout.content); @@ -401,8 +408,8 @@ export class Scheduler { await this.client.sendHtmlText(confTalk.roomId, `

Your talk starts in about 1 hour

Please say something (anything) in this room to check in.

`); const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = (await resolveIdentifiers(userIds)).filter(p => p.mxid).map(p => p.mxid!); - await config.RUNTIME.checkins.expectCheckinFrom(resolved); + const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!); + await this.checkins.expectCheckinFrom(resolved); } } else if (task.type === ScheduledTaskType.TalkStart5M && confTalk !== undefined) { if (!task.talk.prerecorded) { @@ -444,7 +451,7 @@ export class Scheduler { if (!task.talk.prerecorded) return; const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = await resolveIdentifiers(userIds); + const resolved = await resolveIdentifiers(this.client, userIds); const speakers = resolved.filter(p => p.person.role === Role.Speaker); const hosts = resolved.filter(p => p.person.role === Role.Host); const coordinators = resolved.filter(p => p.person.role === Role.Coordinator); @@ -454,10 +461,10 @@ export class Scheduler { for (const person of required) { if (!person.mxid) { missing.push(person); - } else if (!config.RUNTIME.checkins.isCheckedIn(person.mxid)) { + } else if (!this.checkins.isCheckedIn(person.mxid)) { missing.push(person); } else { - await config.RUNTIME.checkins.extendCheckin(person.mxid); + await this.checkins.extendCheckin(person.mxid); } } if (missing.length > 0) { @@ -473,15 +480,15 @@ export class Scheduler { await this.client.sendHtmlText(confTalk.roomId, `

Your talk starts in about 45 minutes

${pills.join(', ')} - Please say something (anything) in this room to check in.

`); const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = (await resolveIdentifiers(userIds)).filter(p => p.mxid).map(p => p.mxid!); - await config.RUNTIME.checkins.expectCheckinFrom(resolved); + const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!); + await this.checkins.expectCheckinFrom(resolved); } } else if (task.type === ScheduledTaskType.TalkCheckin30M && confTalk !== undefined) { // TODO This is skipped entirely for physical talks, but do we want to ensure coordinators are checked-in? if (!task.talk.prerecorded) return; const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = await resolveIdentifiers(userIds); + const resolved = await resolveIdentifiers(this.client, userIds); const speakers = resolved.filter(p => p.person.role === Role.Speaker); const hosts = resolved.filter(p => p.person.role === Role.Host); const coordinators = resolved.filter(p => p.person.role === Role.Coordinator); @@ -491,10 +498,10 @@ export class Scheduler { for (const person of required) { if (!person.mxid) { missing.push(person); - } else if (!config.RUNTIME.checkins.isCheckedIn(person.mxid)) { + } else if (!this.checkins.isCheckedIn(person.mxid)) { missing.push(person); } else { - await config.RUNTIME.checkins.extendCheckin(person.mxid); + await this.checkins.extendCheckin(person.mxid); } } if (missing.length > 0) { @@ -510,15 +517,15 @@ export class Scheduler { await this.client.sendHtmlText(confAudBackstage.roomId, `

Required persons not checked in for upcoming talk

Please track down the speakers for ${await confTalk.getName()}.

Missing: ${pills.join(', ')}

`); const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = (await resolveIdentifiers(userIds)).filter(p => p.mxid).map(p => p.mxid!); - await config.RUNTIME.checkins.expectCheckinFrom(resolved); + const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!); + await this.checkins.expectCheckinFrom(resolved); } // else no complaints } else if (task.type === ScheduledTaskType.TalkCheckin15M && confTalk !== undefined) { // TODO This is skipped entirely for physical talks, but do we want to ensure coordinators are checked-in? if (!task.talk.prerecorded) return; const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = await resolveIdentifiers(userIds); + const resolved = await resolveIdentifiers(this.client, userIds); const speakers = resolved.filter(p => p.person.role === Role.Speaker); const hosts = resolved.filter(p => p.person.role === Role.Host); const coordinators = resolved.filter(p => p.person.role === Role.Coordinator); @@ -528,10 +535,10 @@ export class Scheduler { for (const person of required) { if (!person.mxid) { missing.push(person); - } else if (!config.RUNTIME.checkins.isCheckedIn(person.mxid)) { + } else if (!this.checkins.isCheckedIn(person.mxid)) { missing.push(person); } else { - await config.RUNTIME.checkins.extendCheckin(person.mxid); + await this.checkins.extendCheckin(person.mxid); } } if (missing.length > 0) { @@ -544,16 +551,16 @@ export class Scheduler { } } const roomPill = await MentionPill.forRoom(confTalk.roomId, this.client); - await this.client.sendHtmlText(config.managementRoom, `

Talk is missing speakers

${roomPill.html} is missing one or more speakers: ${pills.join(', ')}

The talk starts in about 15 minutes.

`); + await this.client.sendHtmlText(this.config.managementRoom, `

Talk is missing speakers

${roomPill.html} is missing one or more speakers: ${pills.join(', ')}

The talk starts in about 15 minutes.

`); await this.client.sendHtmlText(confTalk.roomId, `

@room - please check in.

${pills.join(', ')} - It does not appear as though you are present for your talk. Please say something in this room. The conference staff have been notified.

`); await this.client.sendHtmlText(confAudBackstage.roomId, `

Required persons not checked in for upcoming talk

Please track down the speakers for ${await confTalk.getName()}. The conference staff have been notified.

Missing: ${pills.join(', ')}

`); const userIds = await this.conference.getInviteTargetsForTalk(confTalk); - const resolved = (await resolveIdentifiers(userIds)).filter(p => p.mxid).map(p => p.mxid!); - await config.RUNTIME.checkins.expectCheckinFrom(resolved); + const resolved = (await resolveIdentifiers(this.client, userIds)).filter(p => p.mxid).map(p => p.mxid!); + await this.checkins.expectCheckinFrom(resolved); } // else no complaints } else { - await logMessage(LogLevel.WARN, "Scheduler", `Unknown task type for execute(): ${task.type}`); + await logMessage(LogLevel.WARN, "Scheduler", `Unknown task type for execute(): ${task.type}`, this.client); } } @@ -595,6 +602,9 @@ export class Scheduler { public async stop() { await this.lock.acquireAsync(); LogService.warn("Scheduler", "Stopping scheduler..."); + if (this.nextTaskTimeout) { + clearTimeout(this.nextTaskTimeout); + } try { this.pending = {}; this.inAuditoriums = []; diff --git a/src/Scoreboard.ts b/src/Scoreboard.ts index 0f7cdf97..d9b87d17 100644 --- a/src/Scoreboard.ts +++ b/src/Scoreboard.ts @@ -19,7 +19,7 @@ import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk"; import AwaitLock from "await-lock"; import {promises as fs} from "fs"; import * as path from "path"; -import config from "./config"; +import { IConfig } from "./config"; import { isEmojiVariant } from "./utils"; export interface RoomMessage { @@ -77,7 +77,7 @@ export class Scoreboard { private domain: string; private lock = new AwaitLock(); - constructor(private conference: Conference, private client: MatrixClient) { + constructor(private conference: Conference, private client: MatrixClient, config: IConfig) { this.path = path.join(config.dataPath, 'scoreboard.json'); // We expect the `MatrixClient` to only start / resume syncing after diff --git a/src/__tests__/backends/penta/CachingBackend.test.ts b/src/__tests__/backends/penta/CachingBackend.test.ts index 7fdd1f9b..c2153a73 100644 --- a/src/__tests__/backends/penta/CachingBackend.test.ts +++ b/src/__tests__/backends/penta/CachingBackend.test.ts @@ -30,12 +30,6 @@ const prefixConfig: IPrefixConfig = { }, }; -const backendConfig: IPentaScheduleBackendConfig = { - backend: "penta", - database: {} as any as IPentaDbConfig, - scheduleDefinition: "xyz.xml" -}; - const actualUtils = jest.requireActual("../../../utils") as any; jest.mock("../../../utils"); @@ -70,7 +64,7 @@ test("the cache should restore the same talks that were saved", async () => { } as any as PentaDb; async function newPentaBackend(): Promise { - const b = new PentaBackend(backendConfig, parser, fakeDb); + const b = new PentaBackend(parser, fakeDb); await b.init(); return b; } diff --git a/src/__tests__/backends/penta/PentaBackend.test.ts b/src/__tests__/backends/penta/PentaBackend.test.ts index 02dca338..452be583 100644 --- a/src/__tests__/backends/penta/PentaBackend.test.ts +++ b/src/__tests__/backends/penta/PentaBackend.test.ts @@ -27,12 +27,6 @@ const prefixConfig: IPrefixConfig = { }, }; -const backendConfig: IPentaScheduleBackendConfig = { - backend: "penta", - database: {} as any as IPentaDbConfig, - scheduleDefinition: "xyz.xml" -}; - jest.mock('../../../backends/penta/db/PentaDb'); test("talks should be rehydrated from the database", async () => { @@ -65,7 +59,7 @@ test("talks should be rehydrated from the database", async () => { findAllPeopleForTalk: jest.fn(PentaDb.prototype.findAllPeopleForTalk).mockResolvedValue([]), } as any as PentaDb; - const b = new PentaBackend(backendConfig, parser, fakeDb); + const b = new PentaBackend(parser, fakeDb); await b.init(); const talk = b.talks.get("E002")!; diff --git a/src/backends/json/JsonScheduleBackend.ts b/src/backends/json/JsonScheduleBackend.ts index 80808a36..6eb53812 100644 --- a/src/backends/json/JsonScheduleBackend.ts +++ b/src/backends/json/JsonScheduleBackend.ts @@ -1,5 +1,4 @@ -import { rename } from "fs"; -import config, { IJsonScheduleBackendConfig } from "../../config"; +import { IJsonScheduleBackendConfig } from "../../config"; import { IConference, ITalk, IAuditorium, IInterestRoom } from "../../models/schedule"; import { AuditoriumId, InterestId, IScheduleBackend, TalkId } from "../IScheduleBackend"; import { JsonScheduleLoader } from "./JsonScheduleLoader"; @@ -9,7 +8,7 @@ import { LogService } from "matrix-bot-sdk"; import { readJsonFileAsync, writeJsonFileAsync } from "../../utils"; export class JsonScheduleBackend implements IScheduleBackend { - constructor(private loader: JsonScheduleLoader, private cfg: IJsonScheduleBackendConfig, private wasFromCache: boolean) { + constructor(private loader: JsonScheduleLoader, private cfg: IJsonScheduleBackendConfig, private wasFromCache: boolean, public readonly dataPath: string) { } @@ -17,11 +16,11 @@ export class JsonScheduleBackend implements IScheduleBackend { return this.wasFromCache; } - private static async loadConferenceFromCfg(cfg: IJsonScheduleBackendConfig, allowUseCache: boolean): Promise<{loader: JsonScheduleLoader, cached: boolean}> { + private static async loadConferenceFromCfg(dataPath: string, cfg: IJsonScheduleBackendConfig, allowUseCache: boolean): Promise<{loader: JsonScheduleLoader, cached: boolean}> { let jsonDesc; let cached = false; - const cachedSchedulePath = path.join(config.dataPath, 'cached_schedule.json'); + const cachedSchedulePath = path.join(dataPath, 'cached_schedule.json'); try { if (cfg.scheduleDefinition.startsWith("http")) { @@ -60,13 +59,13 @@ export class JsonScheduleBackend implements IScheduleBackend { return {loader: new JsonScheduleLoader(jsonDesc), cached}; } - static async new(cfg: IJsonScheduleBackendConfig): Promise { - const loader = await JsonScheduleBackend.loadConferenceFromCfg(cfg, true); - return new JsonScheduleBackend(loader.loader, cfg, loader.cached); + static async new(dataPath: string, cfg: IJsonScheduleBackendConfig): Promise { + const loader = await JsonScheduleBackend.loadConferenceFromCfg(dataPath, cfg, true); + return new JsonScheduleBackend(loader.loader, cfg, loader.cached, dataPath); } async refresh(): Promise { - this.loader = (await JsonScheduleBackend.loadConferenceFromCfg(this.cfg, false)).loader; + this.loader = (await JsonScheduleBackend.loadConferenceFromCfg(this.dataPath, this.cfg, false)).loader; // If we managed to load anything, this isn't from the cache anymore. this.wasFromCache = false; } diff --git a/src/backends/penta/PentaBackend.ts b/src/backends/penta/PentaBackend.ts index 0088100f..e6b987b9 100644 --- a/src/backends/penta/PentaBackend.ts +++ b/src/backends/penta/PentaBackend.ts @@ -1,4 +1,4 @@ -import config, { IPentaScheduleBackendConfig } from "../../config"; +import { IConfig, IPentaScheduleBackendConfig } from "../../config"; import { IConference, ITalk, IAuditorium, IInterestRoom, IPerson } from "../../models/schedule"; import { AuditoriumId, InterestId, IScheduleBackend, TalkId } from "../IScheduleBackend"; import { PentaDb } from "./db/PentaDb"; @@ -9,7 +9,7 @@ import { IDbTalk } from "./db/DbTalk"; export class PentaBackend implements IScheduleBackend { - constructor(private cfg: IPentaScheduleBackendConfig, parser: PentabarfParser, public db: PentaDb) { + constructor(parser: PentabarfParser, public db: PentaDb) { this.updateFromParser(parser); } @@ -118,17 +118,18 @@ export class PentaBackend implements IScheduleBackend { return false; } - static async new(cfg: IPentaScheduleBackendConfig): Promise { - const xml = await fetch(cfg.scheduleDefinition).then(async r => { + static async new(config: IConfig): Promise { + const pentaConfig = config.conference.schedule as IPentaScheduleBackendConfig; + const xml = await fetch(pentaConfig.scheduleDefinition).then(async r => { if (! r.ok) { throw new Error("Penta XML fetch not OK: " + r.status + "; " + await r.text()) } return await r.text(); }); const parsed = new PentabarfParser(xml, config.conference.prefixes); - const db = new PentaDb(cfg.database); + const db = new PentaDb(config); await db.connect(); - const backend = new PentaBackend(cfg, parsed, db); + const backend = new PentaBackend(parsed, db); await backend.init(); return backend; } diff --git a/src/backends/penta/db/PentaDb.ts b/src/backends/penta/db/PentaDb.ts index 8a440df1..684a9920 100644 --- a/src/backends/penta/db/PentaDb.ts +++ b/src/backends/penta/db/PentaDb.ts @@ -15,36 +15,40 @@ limitations under the License. */ import { Pool } from "pg"; -import config, { IPentaDbConfig } from "../../../config"; +import { IConfig, IPentaDbConfig, IPentaScheduleBackendConfig } from "../../../config"; import { dbPersonToPerson, IDbPerson } from "./DbPerson"; import { LogService, UserID } from "matrix-bot-sdk"; import { objectFastClone } from "../../../utils"; import { IDbTalk, IRawDbTalk } from "./DbTalk"; import { IPerson, Role } from "../../../models/schedule"; -const PEOPLE_SELECT = "SELECT event_id::text, person_id::text, event_role::text, name::text, email::text, matrix_id::text, conference_room::text, remark::text FROM " + config.conference.schedule.database?.tblPeople; -const NONEVENT_PEOPLE_SELECT = "SELECT DISTINCT 'ignore' AS event_id, person_id::text, event_role::text, name::text, email::text, matrix_id::text, conference_room::text FROM " + config.conference.schedule.database?.tblPeople; +const PEOPLE_SELECT = "SELECT event_id::text, person_id::text, event_role::text, name::text, email::text, matrix_id::text, conference_room::text, remark::text FROM "; +const NONEVENT_PEOPLE_SELECT = "SELECT DISTINCT 'ignore' AS event_id, person_id::text, event_role::text, name::text, email::text, matrix_id::text, conference_room::text FROM "; const START_QUERY = "start_datetime AT TIME ZONE $1 AT TIME ZONE 'UTC'"; const QA_START_QUERY = "(start_datetime + presentation_length) AT TIME ZONE $1 AT TIME ZONE 'UTC'"; const END_QUERY = "(start_datetime + duration) AT TIME ZONE $1 AT TIME ZONE 'UTC'"; -const SCHEDULE_SELECT = `SELECT DISTINCT event_id::text, conference_room::text, EXTRACT(EPOCH FROM ${START_QUERY}) * 1000 AS start_datetime, EXTRACT(EPOCH FROM duration) AS duration_seconds, EXTRACT(EPOCH FROM presentation_length) AS presentation_length_seconds, EXTRACT(EPOCH FROM ${END_QUERY}) * 1000 AS end_datetime, EXTRACT(EPOCH FROM ${QA_START_QUERY}) * 1000 AS qa_start_datetime, prerecorded FROM ` + config.conference.schedule.database?.tblSchedule; +const SCHEDULE_SELECT = `SELECT DISTINCT event_id::text, conference_room::text, EXTRACT(EPOCH FROM ${START_QUERY}) * 1000 AS start_datetime, EXTRACT(EPOCH FROM duration) AS duration_seconds, EXTRACT(EPOCH FROM presentation_length) AS presentation_length_seconds, EXTRACT(EPOCH FROM ${END_QUERY}) * 1000 AS end_datetime, EXTRACT(EPOCH FROM ${QA_START_QUERY}) * 1000 AS qa_start_datetime, prerecorded FROM `; export class PentaDb { private client: Pool; private isConnected = false; + pentaConfig: IPentaDbConfig; - constructor(private readonly config: IPentaDbConfig) { + constructor(private readonly config: IConfig) { + // TODO: Make generic + const scheduleConfig = config.conference.schedule as IPentaScheduleBackendConfig; + this.pentaConfig = scheduleConfig.database; this.client = new Pool({ - host: this.config.host, - port: this.config.port, - user: this.config.username, - password: this.config.password, - database: this.config.database, + host: this.pentaConfig.host, + port: this.pentaConfig.port, + user: this.pentaConfig.username, + password: this.pentaConfig.password, + database: this.pentaConfig.database, // sslmode parsing is largely interpreted from pg-connection-string handling - ssl: this.config.sslmode === 'disable' ? false : { - rejectUnauthorized: this.config.sslmode === 'no-verify', + ssl: this.pentaConfig.sslmode === 'disable' ? false : { + rejectUnauthorized: this.pentaConfig.sslmode === 'no-verify', }, }); } @@ -70,36 +74,36 @@ export class PentaDb { public async findPeopleWithId(personId: string): Promise { const numericPersonId = Number(personId); if (Number.isSafeInteger(numericPersonId)) { - const result = await this.client.query(`${PEOPLE_SELECT} WHERE person_id = $1 OR person_id = $2`, [personId, numericPersonId]); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople} ${this.pentaConfig.tblPeople} WHERE person_id = $1 OR person_id = $2`, [personId, numericPersonId]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } else { - const result = await this.client.query(`${PEOPLE_SELECT} WHERE person_id = $1`, [personId]); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople} WHERE person_id = $1`, [personId]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } } public async findAllPeople(): Promise { - const result = await this.client.query(`${PEOPLE_SELECT}`); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople}`); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } public async findAllPeopleForAuditorium(auditoriumId: string): Promise { - const result = await this.client.query(`${NONEVENT_PEOPLE_SELECT} WHERE conference_room = $1`, [auditoriumId]); + const result = await this.client.query(`${NONEVENT_PEOPLE_SELECT} ${this.pentaConfig.tblPeople} WHERE conference_room = $1`, [auditoriumId]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } public async findAllPeopleForTalk(talkId: string): Promise { - const result = await this.client.query(`${PEOPLE_SELECT} WHERE event_id = $1`, [talkId]); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople} WHERE event_id = $1`, [talkId]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } public async findAllPeopleWithRole(role: Role): Promise { - const result = await this.client.query(`${PEOPLE_SELECT} WHERE event_role = $1`, [role]); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople} WHERE event_role = $1`, [role]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } public async findAllPeopleWithRemark(remark: string): Promise { - const result = await this.client.query(`${PEOPLE_SELECT} WHERE remark = $1`, [remark]); + const result = await this.client.query(`${PEOPLE_SELECT} ${this.pentaConfig.tblPeople} WHERE remark = $1`, [remark]); return this.sanitizeRecords(result.rows).map(dbPersonToPerson); } @@ -148,21 +152,21 @@ export class PentaDb { */ public async getTalk(talkId: string): Promise { const result = await this.client.query( - `${SCHEDULE_SELECT} WHERE event_id::text = $2`, - [config.conference.timezone, talkId]); + `${SCHEDULE_SELECT} ${this.pentaConfig.tblSchedule} WHERE event_id::text = $2`, + [this.config.conference.timezone, talkId]); return result.rowCount > 0 ? this.postprocessDbTalk(result.rows[0]) : null; } private async getTalksWithin(timeQuery: string, inNextMinutes: number, minBefore: number): Promise { const now = "NOW() AT TIME ZONE 'UTC'"; const result = await this.client.query( - `${SCHEDULE_SELECT} WHERE ${timeQuery} >= (${now} - MAKE_INTERVAL(mins => $2)) AND ${timeQuery} <= (${now} + MAKE_INTERVAL(mins => $3))`, - [config.conference.timezone, minBefore, inNextMinutes]); + `${SCHEDULE_SELECT} ${this.pentaConfig.tblSchedule} WHERE ${timeQuery} >= (${now} - MAKE_INTERVAL(mins => $2)) AND ${timeQuery} <= (${now} + MAKE_INTERVAL(mins => $3))`, + [this.config.conference.timezone, minBefore, inNextMinutes]); return this.postprocessDbTalks(result.rows); } private postprocessDbTalk(talk: IRawDbTalk): IDbTalk { - const qaStartDatetime = talk.qa_start_datetime + this.config.schedulePreBufferSeconds * 1000; + const qaStartDatetime = talk.qa_start_datetime + this.pentaConfig.schedulePreBufferSeconds * 1000; let livestreamStartDatetime: number; if (talk.prerecorded) { // For prerecorded talks, a preroll is shown, followed by the talk recording, then an @@ -170,9 +174,9 @@ export class PentaDb { livestreamStartDatetime = qaStartDatetime; } else { // For live talks, both the preroll and interroll are shown, followed by the live talk. - livestreamStartDatetime = talk.start_datetime + this.config.schedulePreBufferSeconds * 1000; + livestreamStartDatetime = talk.start_datetime + this.pentaConfig.schedulePreBufferSeconds * 1000; } - const livestreamEndDatetime = talk.end_datetime - this.config.schedulePostBufferSeconds * 1000; + const livestreamEndDatetime = talk.end_datetime - this.pentaConfig.schedulePostBufferSeconds * 1000; return { ...talk, diff --git a/src/commands/AttendanceCommand.ts b/src/commands/AttendanceCommand.ts index 5b277733..26298fc9 100644 --- a/src/commands/AttendanceCommand.ts +++ b/src/commands/AttendanceCommand.ts @@ -20,12 +20,15 @@ import { Conference } from "../Conference"; import { resolveIdentifiers } from "../invites"; import { COLOR_GREEN, COLOR_RED } from "../models/colors"; import { IPerson } from "../models/schedule"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export class AttendanceCommand implements ICommand { public readonly prefixes = ["attendance"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - await client.sendNotice(roomId, "Calculating..."); + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference) {} + + public async run(roomId: string, event: any, args: string[]) { + await this.client.sendNotice(roomId, "Calculating..."); let totalEmails = 0; let totalJoined = 0; @@ -46,9 +49,9 @@ export class AttendanceCommand implements ICommand { let html = "
    "; const append = async (invitePeople: IPerson[], bsPeople: IPerson[] | null, name: string, roomId: string, bsRoomId: string | null, withHtml: boolean) => { - const inviteTargets = await resolveIdentifiers(invitePeople); + const inviteTargets = await resolveIdentifiers(this.client, invitePeople); - const joinedMembers = await client.getJoinedRoomMembers(roomId); + const joinedMembers = await this.client.getJoinedRoomMembers(roomId); const emailInvites = inviteTargets.filter(i => !i.mxid).length; const joined = inviteTargets.filter(i => i.mxid && joinedMembers.includes(i.mxid)).length; @@ -66,8 +69,8 @@ export class AttendanceCommand implements ICommand { if (!bsPeople) { throw new Error("bsRoomId set but bsPeople isn't!"); } - const bsInviteTargets = await resolveIdentifiers(bsPeople); - const bsJoinedMembers = await client.getJoinedRoomMembers(bsRoomId); + const bsInviteTargets = await resolveIdentifiers(this.client, bsPeople); + const bsJoinedMembers = await this.client.getJoinedRoomMembers(bsRoomId); const bsEmailInvites = bsInviteTargets.filter(i => !i.mxid).length; const bsJoined = bsInviteTargets.filter(i => i.mxid && bsJoinedMembers.includes(i.mxid)).length; const bsAcceptedPct = Math.round((bsJoined / bsInviteTargets.length) * 100); @@ -82,16 +85,16 @@ export class AttendanceCommand implements ICommand { if (withHtml) html += ""; }; - for (const auditorium of conference.storedAuditoriums) { + for (const auditorium of this.conference.storedAuditoriums) { const doAppend = !!targetAudId && (targetAudId === "all" || targetAudId === await auditorium.getId()); - const bs = conference.getAuditoriumBackstage(await auditorium.getId()); - const inviteTargets = await conference.getInviteTargetsForAuditorium(auditorium); - const bsInviteTargets = await conference.getInviteTargetsForAuditorium(auditorium, true); + const bs = this.conference.getAuditoriumBackstage(await auditorium.getId()); + const inviteTargets = await this.conference.getInviteTargetsForAuditorium(auditorium); + const bsInviteTargets = await this.conference.getInviteTargetsForAuditorium(auditorium, true); await append(inviteTargets, bsInviteTargets, await auditorium.getId(), auditorium.roomId, bs.roomId, doAppend); } - for (const spiRoom of conference.storedInterestRooms) { + for (const spiRoom of this.conference.storedInterestRooms) { const doAppend = !!targetAudId && (targetAudId === "all" || targetAudId === await spiRoom.getId()); - const inviteTargets = await conference.getInviteTargetsForInterest(spiRoom); + const inviteTargets = await this.conference.getInviteTargetsForInterest(spiRoom); await append(inviteTargets, null, await spiRoom.getId(), spiRoom.roomId, null, doAppend); } html += "
"; @@ -105,6 +108,6 @@ export class AttendanceCommand implements ICommand { html = `Summary: ${htmlNum(acceptedPct)} have joined, ${htmlNum(emailPct, true)} have pending emails. ${targetAudId ? '
' : ''}${html}`; - await client.replyHtmlNotice(roomId, event, html); + await this.client.replyHtmlNotice(roomId, event, html); } } diff --git a/src/commands/BuildCommand.ts b/src/commands/BuildCommand.ts index 722f6388..e794c2a2 100644 --- a/src/commands/BuildCommand.ts +++ b/src/commands/BuildCommand.ts @@ -15,76 +15,79 @@ limitations under the License. */ import { ICommand } from "./ICommand"; -import { LogLevel, LogService, MatrixClient, MentionPill, RichReply } from "matrix-bot-sdk"; +import { LogLevel, LogService, MentionPill, RichReply } from "matrix-bot-sdk"; import { Auditorium } from "../models/Auditorium"; import { ITalk } from "../models/schedule"; -import config from "../config"; +import { IConfig } from "../config"; import { Conference } from "../Conference"; import { logMessage } from "../LogProxy"; import { editNotice } from "../utils"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export class BuildCommand implements ICommand { public readonly prefixes = ["build", "b"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference, private readonly config: IConfig) {} + + public async run(roomId: string, event: any, args: string[]) { if (!args) args = []; - await client.sendReadReceipt(roomId, event['event_id']); + await this.client.sendReadReceipt(roomId, event['event_id']); - const backend = conference.backend; + const backend = this.conference.backend; try { // Try to refresh the schedule first, to ensure we don't miss any updates. await backend.refresh(); } catch (error) { - await client.sendNotice(roomId, `Failed to refresh schedule: ${error.toString()}`) + await this.client.sendNotice(roomId, `Failed to refresh schedule: ${error.toString()}`) return; } try { // Try to reset our view of the state first, to ensure we don't miss anything (e.g. if we got invited to a room since bot startup). - await conference.construct(); + await this.conference.construct(); } catch (error) { - await client.sendNotice(roomId, `Failed to reset conference state: ${error.toString()}`) + await this.client.sendNotice(roomId, `Failed to reset conference state: ${error.toString()}`) return; } - if (!conference.isCreated) { - await conference.createRootSpace(); - await conference.createDb(backend.conference); + if (!this.conference.isCreated) { + await this.conference.createRootSpace(); + await this.conference.createDb(backend.conference); } - const spacePill = await MentionPill.forRoom((await conference.getSpace())!.roomId, client); + const spacePill = await MentionPill.forRoom((await this.conference.getSpace())!.roomId, this.client); const messagePrefix = "Conference prepared! Making rooms for later use (this will take a while)..."; const reply = RichReply.createFor(roomId, event, messagePrefix + "\n\nYour conference's space is at " + spacePill.text, messagePrefix + "

Your conference's space is at " + spacePill.html); reply["msgtype"] = "m.notice"; - await client.sendMessage(roomId, reply); + await this.client.sendMessage(roomId, reply); // Create subspaces let subspacesCreated = 0; - const subspacesConfig = Object.entries(config.conference.subspaces); - const statusEventId = await client.sendNotice( + const subspacesConfig = Object.entries(this.config.conference.subspaces); + const statusEventId = await this.client.sendNotice( roomId, `0/${subspacesConfig.length} subspaces have been created`, ); for (const [subspaceId, subspaceConfig] of subspacesConfig) { try { - await conference.createSubspace( + await this.conference.createSubspace( subspaceId, subspaceConfig.displayName, subspaceConfig.alias ); subspacesCreated++; await editNotice( - client, + this.client, roomId, statusEventId, `${subspacesCreated}/${subspacesConfig.length} subspaces have been created`, ); } catch (error) { LogService.error("BuildCommand", JSON.stringify(error)); - await client.sendNotice(roomId, `Failed to build subspace '${subspaceId}': ${error.toString()}`) + await this.client.sendNotice(roomId, `Failed to build subspace '${subspaceId}': ${error.toString()}`) } } @@ -94,42 +97,42 @@ export class BuildCommand implements ICommand { const talkId = args[2]; const pentaAud = backend.auditoriums.get(audId); - if (!pentaAud) return await logMessage(LogLevel.ERROR, "BuildCommand", `Cannot find auditorium: ${audId}`); + if (!pentaAud) return await logMessage(LogLevel.ERROR, "BuildCommand", `Cannot find auditorium: ${audId}`, this.client); if (pentaAud.isPhysical) { // Physical auditoriums don't have any talk rooms - return await logMessage(LogLevel.ERROR, "BuildCommand", `Auditorium '${audId}' is physical and does not have talk rooms.`); + return await logMessage(LogLevel.ERROR, "BuildCommand", `Auditorium '${audId}' is physical and does not have talk rooms.`, this.client); } const pentaTalk = pentaAud.talks.get(talkId); - if (!pentaTalk) return await logMessage(LogLevel.ERROR, "BuildCommand", `Cannot find talk in room: ${audId} ${talkId}`); + if (!pentaTalk) return await logMessage(LogLevel.ERROR, "BuildCommand", `Cannot find talk in room: ${audId} ${talkId}`, this.client); - await conference.createAuditoriumBackstage(pentaAud); - const aud = await conference.createAuditorium(pentaAud); - await conference.createTalk(pentaTalk, aud); + await this.conference.createAuditoriumBackstage(pentaAud); + const aud = await this.conference.createAuditorium(pentaAud); + await this.conference.createTalk(pentaTalk, aud); - await client.sendNotice(roomId, "Talk room created"); + await this.client.sendNotice(roomId, "Talk room created"); return; } else if (args[0] === "interest") { const interestId = args[1]; const interestRoom = backend.interestRooms.get(interestId); if (interestRoom) { - await conference.createInterestRoom(interestRoom); - await client.sendNotice(roomId, "Interest room created"); + await this.conference.createInterestRoom(interestRoom); + await this.client.sendNotice(roomId, "Interest room created"); } else { - await client.sendNotice(roomId, `Cannot find interest room ${interestId} in schedule`); + await this.client.sendNotice(roomId, `Cannot find interest room ${interestId} in schedule`); } return; } // Create support rooms - await conference.createSupportRooms(); - await client.sendNotice(roomId, "Support rooms have been created"); + await this.conference.createSupportRooms(); + await this.client.sendNotice(roomId, "Support rooms have been created"); if (!args.includes("sionly")) { let auditoriumsCreated = 0; - const statusEventId = await client.sendNotice( + const statusEventId = await this.client.sendNotice( roomId, `0/${backend.auditoriums.size} auditoriums have been created`, ); @@ -137,10 +140,10 @@ export class BuildCommand implements ICommand { // Create auditorium backstages for (const auditorium of backend.auditoriums.values()) { try { - await conference.createAuditoriumBackstage(auditorium); + await this.conference.createAuditoriumBackstage(auditorium); auditoriumsCreated++; editNotice( - client, + this.client, roomId, statusEventId, `${auditoriumsCreated}/${backend.auditoriums.size} auditoriums have been created`, @@ -158,10 +161,10 @@ export class BuildCommand implements ICommand { const talks: [ITalk, Auditorium][] = []; for (const auditorium of backend.auditoriums.values()) { try { - const confAud = await conference.createAuditorium(auditorium); + const confAud = await this.conference.createAuditorium(auditorium); auditoriumsCreated++; editNotice( - client, + this.client, roomId, statusEventId, `${auditoriumsCreated}/${backend.auditoriums.size} auditoriums have been created`, @@ -184,16 +187,16 @@ export class BuildCommand implements ICommand { if (!args.includes("notalks")) { // Create talk rooms let talksCreated = 0; - const statusEventId = await client.sendNotice( + const statusEventId = await this.client.sendNotice( roomId, `0/${talks.length} talks have been created`, ); for (const [talk, auditorium] of talks) { try { - await conference.createTalk(talk, auditorium); + await this.conference.createTalk(talk, auditorium); talksCreated++; editNotice( - client, + this.client, roomId, statusEventId, `${talksCreated}/${talks.length} talks have been created`, @@ -212,13 +215,13 @@ export class BuildCommand implements ICommand { if (!args.includes("nosi")) { // Create special interest rooms let specialInterestRoomsCreated = 0; - const statusEventId = await client.sendNotice(roomId, `0/${backend.interestRooms.size} interest rooms have been created`); + const statusEventId = await this.client.sendNotice(roomId, `0/${backend.interestRooms.size} interest rooms have been created`); for (const siRoom of backend.interestRooms.values()) { try { - await conference.createInterestRoom(siRoom); + await this.conference.createInterestRoom(siRoom); specialInterestRoomsCreated++; await editNotice( - client, + this.client, roomId, statusEventId, `${specialInterestRoomsCreated}/${backend.interestRooms.size} interest rooms have been created`, @@ -231,10 +234,10 @@ export class BuildCommand implements ICommand { } } } else { - await client.sendNotice(roomId, "Skipped special interest rooms"); + await this.client.sendNotice(roomId, "Skipped special interest rooms"); } - await client.sendHtmlNotice(roomId, "" + + await this.client.sendHtmlNotice(roomId, "" + "

Conference built

" + "

Now it's time to import your participants & team.

" ); diff --git a/src/commands/CopyModeratorsCommand.ts b/src/commands/CopyModeratorsCommand.ts index 1f9e356f..bbcb8bdd 100644 --- a/src/commands/CopyModeratorsCommand.ts +++ b/src/commands/CopyModeratorsCommand.ts @@ -17,20 +17,20 @@ limitations under the License. import { ICommand } from "./ICommand"; import { MatrixClient, MembershipEvent } from "matrix-bot-sdk"; import { Conference } from "../Conference"; -import { LiveWidget } from "../models/LiveWidget"; -import { invitePersonToRoom, ResolvedPersonIdentifier } from "../invites"; export class CopyModeratorsCommand implements ICommand { public readonly prefixes = ["copymods", "copymoderators", "copy_mods", "copy_moderators"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient) {} + + public async run(roomId: string, event: any, args: string[]) { if (args.length < 2) { - return client.replyNotice(roomId, event, "Please specify two rooms"); + return this.client.replyNotice(roomId, event, "Please specify two rooms"); } - const fromRoomId = await client.resolveRoom(args[0]); - const toRoomId = await client.resolveRoom(args[1]); - const fromPl: {"users"?: Record} = await client.getRoomStateEvent(fromRoomId, "m.room.power_levels", ""); - let toPl = await client.getRoomStateEvent(toRoomId, "m.room.power_levels", ""); + const fromRoomId = await this.client.resolveRoom(args[0]); + const toRoomId = await this.client.resolveRoom(args[1]); + const fromPl: {"users"?: Record} = await this.client.getRoomStateEvent(fromRoomId, "m.room.power_levels", ""); + let toPl = await this.client.getRoomStateEvent(toRoomId, "m.room.power_levels", ""); if (!toPl) toPl = {}; if (!toPl['users']) toPl['users'] = {}; @@ -42,17 +42,17 @@ export class CopyModeratorsCommand implements ICommand { } } - await client.sendStateEvent(toRoomId, "m.room.power_levels", "", toPl); + await this.client.sendStateEvent(toRoomId, "m.room.power_levels", "", toPl); - const state = await client.getRoomState(toRoomId); + const state = await this.client.getRoomState(toRoomId); const members = state.filter(s => s.type === "m.room.member").map(s => new MembershipEvent(s)); const effectiveJoinedUserIds = members.filter(m => m.effectiveMembership === "join").map(m => m.membershipFor); for (const userId of Object.keys(toPl['users'])) { if (!effectiveJoinedUserIds.includes(userId)) { - await client.inviteUser(userId, toRoomId); + await this.client.inviteUser(userId, toRoomId); } } - await client.replyNotice(roomId, event, "Moderators copied and invited"); + await this.client.replyNotice(roomId, event, "Moderators copied and invited"); } } diff --git a/src/commands/DevCommand.ts b/src/commands/DevCommand.ts index b0a0af48..208ab658 100644 --- a/src/commands/DevCommand.ts +++ b/src/commands/DevCommand.ts @@ -22,10 +22,12 @@ import { IPerson, Role } from "../models/schedule"; export class DevCommand implements ICommand { public readonly prefixes = ["dev"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient, private readonly conference: Conference) {} + + public async run(roomId: string, event: any, args: string[]) { let people: IPerson[] = []; - for (const aud of conference.storedAuditoriums) { - const inviteTargets = await conference.getInviteTargetsForAuditorium(aud, true); + for (const aud of this.conference.storedAuditoriums) { + const inviteTargets = await this.conference.getInviteTargetsForAuditorium(aud, true); people.push(...inviteTargets.filter(i => i.role === Role.Coordinator)); } const newPeople: IPerson[] = []; @@ -34,6 +36,6 @@ export class DevCommand implements ICommand { newPeople.push(p); } }); - await client.sendNotice(roomId, `Total people: ${newPeople.length}`); + await this.client.sendNotice(roomId, `Total people: ${newPeople.length}`); } } diff --git a/src/commands/FDMCommand.ts b/src/commands/FDMCommand.ts index 46976d14..697f945d 100644 --- a/src/commands/FDMCommand.ts +++ b/src/commands/FDMCommand.ts @@ -20,19 +20,22 @@ import { Conference } from "../Conference"; import { invitePersonToRoom, resolveIdentifiers } from "../invites"; import { logMessage } from "../LogProxy"; import { IPerson } from "../models/schedule"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export class FDMCommand implements ICommand { public readonly prefixes = ["fdm"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - const spi = conference.getInterestRoom("I.infodesk"); - const infBackstage = await client.resolveRoom("#infodesk-backstage:fosdem.org"); - const vol = await client.resolveRoom("#volunteers:fosdem.org"); - const volBackstage = await client.resolveRoom("#volunteers-backstage:fosdem.org"); + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference) {} - const db = await conference.getPentaDb(); + public async run(roomId: string, event: any, args: string[]) { + const spi = this.conference.getInterestRoom("I.infodesk"); + const infBackstage = await this.client.resolveRoom("#infodesk-backstage:fosdem.org"); + const vol = await this.client.resolveRoom("#volunteers:fosdem.org"); + const volBackstage = await this.client.resolveRoom("#volunteers-backstage:fosdem.org"); + + const db = await this.conference.getPentaDb(); if (db === null) { - await client.replyNotice(roomId, event, "Command not available as PentaDb is not enabled."); + await this.client.replyNotice(roomId, event, "Command not available as PentaDb is not enabled."); return; } @@ -51,39 +54,39 @@ export class FDMCommand implements ICommand { html += `
  • ${person.name}
  • `; } html += ""; - await client.sendHtmlNotice(roomId, html); + await this.client.sendHtmlNotice(roomId, html); } else if (args[0] === 'invite') { - const infodesk = await conference.getInviteTargetsForInterest(spi); - const infodeskResolved = await resolveIdentifiers(infodesk); - const inBsJoined = await client.getJoinedRoomMembers(infBackstage); - const volJoined = await client.getJoinedRoomMembers(vol); - const volBsJoined = await client.getJoinedRoomMembers(volBackstage); + const infodesk = await this.conference.getInviteTargetsForInterest(spi); + const infodeskResolved = await resolveIdentifiers(this.client, infodesk); + const inBsJoined = await this.client.getJoinedRoomMembers(infBackstage); + const volJoined = await this.client.getJoinedRoomMembers(vol); + const volBsJoined = await this.client.getJoinedRoomMembers(volBackstage); for (const person of infodeskResolved) { try { if (person.mxid && inBsJoined.includes(person.mxid)) continue; - await invitePersonToRoom(person, infBackstage); + await invitePersonToRoom(this.client, person, infBackstage); } catch (e) { - await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${infBackstage} - ignoring`); + await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${infBackstage} - ignoring`, this.client); } } - const volResolved = await resolveIdentifiers(volunteers); + const volResolved = await resolveIdentifiers(this.client, volunteers); for (const person of volResolved) { try { if (person.mxid && volJoined.includes(person.mxid)) continue; - await invitePersonToRoom(person, vol); + await invitePersonToRoom(this.client, person, vol); } catch (e) { - await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${vol} - ignoring`); + await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${vol} - ignoring`, this.client); } try { if (person.mxid && volBsJoined.includes(person.mxid)) continue; - await invitePersonToRoom(person, volBackstage); + await invitePersonToRoom(this.client, person, volBackstage); } catch (e) { - await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${volBackstage} - ignoring`); + await logMessage(LogLevel.ERROR, "InviteCommand", `Error inviting ${person.mxid} / ${person.person.id} to ${volBackstage} - ignoring`, this.client); } } - await client.sendNotice(roomId, "Invites sent"); + await this.client.sendNotice(roomId, "Invites sent"); } else { - await client.replyNotice(roomId, event, "Unknown command"); + await this.client.replyNotice(roomId, event, "Unknown command"); } } } diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index 748c7e0b..7238369e 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -21,7 +21,9 @@ import { Conference } from "../Conference"; export class HelpCommand implements ICommand { public readonly prefixes = ["help", "?"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient) { } + + public async run(roomId: string, event: any, args: string[]) { const htmlHelp = "" + "

    Conference bot help

    " + "Hint: For all commands, instead of !conference you can also use a tab-completed mention pill of the bot's name!\n" + @@ -64,6 +66,6 @@ export class HelpCommand implements ICommand { "!conference widgets <aud> - Creates all widgets for the auditorium and its talks.\n" + "" + ""; - return client.replyHtmlNotice(roomId, event, htmlHelp); + return this.client.replyHtmlNotice(roomId, event, htmlHelp); } } diff --git a/src/commands/ICommand.ts b/src/commands/ICommand.ts index 6d1ad06d..51fd9c67 100644 --- a/src/commands/ICommand.ts +++ b/src/commands/ICommand.ts @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "matrix-bot-sdk"; -import { Conference } from "../Conference"; - export interface ICommand { prefixes: string[]; - run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]); + run(roomId: string, event: any, args: string[]); } diff --git a/src/commands/InviteCommand.ts b/src/commands/InviteCommand.ts index 9aca575b..1e0602c5 100644 --- a/src/commands/InviteCommand.ts +++ b/src/commands/InviteCommand.ts @@ -20,22 +20,25 @@ import { Conference } from "../Conference"; import { invitePersonToRoom, ResolvedPersonIdentifier, resolveIdentifiers } from "../invites"; import { RS_3PID_PERSON_ID } from "../models/room_state"; import { runRoleCommand } from "./actions/roles"; -import config from "../config"; import { logMessage } from "../LogProxy"; import { IPerson, Role } from "../models/schedule"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; +import { IConfig } from "../config"; export class InviteCommand implements ICommand { public readonly prefixes = ["invite", "inv"]; - private async createInvites(client: MatrixClient, people: IPerson[], alias: string) { - const resolved = await resolveIdentifiers(people); + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference, private readonly config: IConfig) {} - const targetRoomId = await client.resolveRoom(alias); - await InviteCommand.ensureInvited(client, targetRoomId, resolved); + private async createInvites(people: IPerson[], alias: string) { + const resolved = await resolveIdentifiers(this.client, people); + + const targetRoomId = await this.client.resolveRoom(alias); + await this.ensureInvited(targetRoomId, resolved); } - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - await client.replyNotice(roomId, event, "Sending invites to participants. This might take a while."); + public async run(roomId: string, event: any, args: string[]) { + await this.client.replyNotice(roomId, event, "Sending invites to participants. This might take a while."); // This is called invite but it's really membership sync in a way. We're iterating over // every possible room the bot knows about and making sure that we have the right people @@ -44,8 +47,8 @@ export class InviteCommand implements ICommand { if (args[0] && args[0] === "speakers-support") { let people: IPerson[] = []; - for (const aud of conference.storedAuditoriumBackstages) { - people.push(...await conference.getInviteTargetsForAuditorium(aud, true)); + for (const aud of this.conference.storedAuditoriumBackstages) { + people.push(...await this.conference.getInviteTargetsForAuditorium(aud, true)); } people = people.filter(p => p.role === Role.Speaker); const newPeople: IPerson[] = []; @@ -54,18 +57,18 @@ export class InviteCommand implements ICommand { newPeople.push(p); } }); - await this.createInvites(client, newPeople, config.conference.supportRooms.speakers); + await this.createInvites(newPeople, this.config.conference.supportRooms.speakers); } else if (args[0] && args[0] === "coordinators-support") { let people: IPerson[] = []; - for (const aud of conference.storedAuditoriums) { + for (const aud of this.conference.storedAuditoriums) { if (!(await aud.getId()).startsWith("D.")) { // HACK: Only invite coordinators for D.* auditoriums. // TODO: Make invitations for support rooms more configurable. - // https://github.com/matrix-org/conference-bot/issues/76 + // https://github.com/matrix-org/this.conference-bot/issues/76 continue; } - const inviteTargets = await conference.getInviteTargetsForAuditorium(aud, true); + const inviteTargets = await this.conference.getInviteTargetsForAuditorium(aud, true); people.push(...inviteTargets.filter(i => i.role === Role.Coordinator)); } const newPeople: IPerson[] = []; @@ -74,24 +77,24 @@ export class InviteCommand implements ICommand { newPeople.push(p); } }); - await this.createInvites(client, newPeople, config.conference.supportRooms.coordinators); + await this.createInvites(newPeople, this.config.conference.supportRooms.coordinators); } else if (args[0] && args[0] === "si-support") { const people: IPerson[] = []; - for (const sir of conference.storedInterestRooms) { - people.push(...await conference.getInviteTargetsForInterest(sir)); + for (const sir of this.conference.storedInterestRooms) { + people.push(...await this.conference.getInviteTargetsForInterest(sir)); } - await this.createInvites(client, people, config.conference.supportRooms.specialInterest); + await this.createInvites(people, this.config.conference.supportRooms.specialInterest); } else { - await runRoleCommand(InviteCommand.ensureInvited, conference, client, roomId, event, args); + await runRoleCommand((_client,_room,people) => this.ensureInvited(roomId, people), this.conference, this.client, roomId, event, args); } - await client.sendNotice(roomId, "Invites sent!"); + await this.client.sendNotice(roomId, "Invites sent!"); } - public static async ensureInvited(client: MatrixClient, roomId: string, people: ResolvedPersonIdentifier[]) { + public async ensureInvited(roomId: string, people: ResolvedPersonIdentifier[]) { // 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. - const state = await client.getRoomState(roomId); + const state = await this.client.getRoomState(roomId); const emailInvitePersonIds = state.filter(s => s.type === "m.room.third_party_invite").map(s => s.content?.[RS_3PID_PERSON_ID]).filter(i => !!i); const members = state.filter(s => s.type === "m.room.member").map(s => new MembershipEvent(s)); const effectiveJoinedUserIds = members.filter(m => m.effectiveMembership === "join").map(m => m.membershipFor); @@ -99,10 +102,10 @@ export class InviteCommand implements ICommand { if (target.mxid && effectiveJoinedUserIds.includes(target.mxid)) continue; if (emailInvitePersonIds.includes(target.person.id)) continue; try { - await invitePersonToRoom(target, roomId); + await invitePersonToRoom(this.client, target, roomId); } 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)'}`); + 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); } } } diff --git a/src/commands/InviteMeCommand.ts b/src/commands/InviteMeCommand.ts index f803488b..f4fc010d 100644 --- a/src/commands/InviteMeCommand.ts +++ b/src/commands/InviteMeCommand.ts @@ -15,23 +15,27 @@ limitations under the License. */ import { ICommand } from "./ICommand"; -import { LogLevel, MatrixClient } from "matrix-bot-sdk"; +import { LogLevel } from "matrix-bot-sdk"; import { Conference } from "../Conference"; import { logMessage } from "../LogProxy"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export class InviteMeCommand implements ICommand { + + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference) {} + public readonly prefixes = ["inviteme", "inviteto"]; - private async inviteTo(client: MatrixClient, invitee: string, room: string): Promise { - const members = await client.getJoinedRoomMembers(room); + private async inviteTo(invitee: string, room: string): Promise { + const members = await this.client.getJoinedRoomMembers(room); if (members.includes(invitee)) return; - await client.inviteUser(invitee, room); + await this.client.inviteUser(invitee, room); } /** * Returns a map of room 'groups'. These are named groups of rooms corresponding to various roles. */ - public async roomGroups(conference: Conference): Promise>> { + public async roomGroups(): Promise>> { const groups: Map> = new Map(); function addToGroup(groupName: string, roomId: string) { @@ -41,7 +45,7 @@ export class InviteMeCommand implements ICommand { groups.get(groupName)!.add(roomId); } - for (const aud of conference.storedAuditoriums) { + for (const aud of this.conference.storedAuditoriums) { addToGroup("auditorium", aud.roomId); const audSlug = await aud.getSlug(); addToGroup(audSlug + ":*", aud.roomId); @@ -58,7 +62,7 @@ export class InviteMeCommand implements ICommand { addToGroup("*", space.roomId); } - for (const audBack of conference.storedAuditoriumBackstages) { + for (const audBack of this.conference.storedAuditoriumBackstages) { addToGroup("auditorium_backstage", audBack.roomId); const audSlug = await audBack.getSlug(); addToGroup(audSlug + ":*", audBack.roomId); @@ -67,9 +71,9 @@ export class InviteMeCommand implements ICommand { addToGroup("*", audBack.roomId); } - for (const talk of conference.storedTalks) { + for (const talk of this.conference.storedTalks) { addToGroup("talk", talk.roomId); - const audSlug = await conference.getAuditorium(await talk.getAuditoriumId()).getSlug(); + const audSlug = await this.conference.getAuditorium(await talk.getAuditoriumId()).getSlug(); addToGroup(audSlug + ":talk", talk.roomId); addToGroup(audSlug + ":*", talk.roomId); addToGroup(audSlug + ":private", talk.roomId); @@ -77,7 +81,7 @@ export class InviteMeCommand implements ICommand { addToGroup("*", talk.roomId); } - for (const spi of conference.storedInterestRooms) { + for (const spi of this.conference.storedInterestRooms) { addToGroup("interest", spi.roomId); addToGroup("public", spi.roomId); addToGroup("*", spi.roomId); @@ -110,32 +114,32 @@ export class InviteMeCommand implements ICommand { }).join("\n") + ""; } - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - const roomGroups = await this.roomGroups(conference); + public async run(roomId: string, event: any, args: string[]) { + const roomGroups = await this.roomGroups(); if (!args.length) { - return client.replyHtmlNotice(roomId, event, "Please specify a room ID or alias, or one of the room groups:\n" + this.prettyGroupNameList(roomGroups)); + return this.client.replyHtmlNotice(roomId, event, "Please specify a room ID or alias, or one of the room groups:\n" + this.prettyGroupNameList(roomGroups)); } const userId = args[1] || event['sender']; if (roomGroups.has(args[0])) { const group = roomGroups.get(args[0])!; - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], 'Joining ' + group.size); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], 'Joining ' + group.size); for (const roomId of group) { try { - await this.inviteTo(client, userId, roomId); + await this.inviteTo(userId, roomId); } catch (e) { - await logMessage(LogLevel.WARN, "InviteMeCommand", `Error inviting ${userId} to ${roomId}: ${e?.message || e?.body?.message}`); + await logMessage(LogLevel.WARN, "InviteMeCommand", `Error inviting ${userId} to ${roomId}: ${e?.message || e?.body?.message}`, this.client); } } - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } else { // Invite to one particular room. - const targetRoomId = await client.resolveRoom(args[0]); - await client.inviteUser(userId, targetRoomId); - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + const targetRoomId = await this.client.resolveRoom(args[0]); + await this.client.inviteUser(userId, targetRoomId); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } } } diff --git a/src/commands/IrcPlumbCommand.ts b/src/commands/IrcPlumbCommand.ts index f3faacb0..322459e4 100644 --- a/src/commands/IrcPlumbCommand.ts +++ b/src/commands/IrcPlumbCommand.ts @@ -20,35 +20,36 @@ import { Conference } from "../Conference"; import { IRCBridge } from "../IRCBridge"; import { logMessage } from "../LogProxy"; import { KickPowerLevel } from "../models/room_kinds"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; const PLUMB_WAIT_MS = 1000; export class IrcPlumbCommand implements ICommand { public readonly prefixes = ["plumb-irc"]; - constructor(private readonly ircBridge: IRCBridge) { + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference, private readonly ircBridge: IRCBridge) { } - private async plumbAll(conference: Conference, client: MatrixClient, roomId: string) { - for (const auditorium of conference.storedAuditoriums) { + private async plumbAll(roomId: string) { + for (const auditorium of this.conference.storedAuditoriums) { const channelName = await this.ircBridge.deriveChannelName(auditorium); try { - await this.plumbOne(client, channelName, auditorium.roomId); + await this.plumbOne(this.client, channelName, auditorium.roomId); // Wait before plumbing the next one so as to not overwhelm the poor bridge. await new Promise(r => setTimeout(r, PLUMB_WAIT_MS)); } catch (ex) { - await logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel ${channelName} to ${auditorium.roomId}`); + await logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel ${channelName} to ${auditorium.roomId}`, this.client); LogService.warn("IrcPlumbCommand", `Could not plumb channel ${channelName} to ${auditorium.roomId}:`, ex); } } - for (const interest of conference.storedInterestRooms) { + for (const interest of this.conference.storedInterestRooms) { const channelName = await this.ircBridge.deriveChannelNameSI(interest); try { - await this.plumbOne(client, channelName, interest.roomId); + await this.plumbOne(this.client, channelName, interest.roomId); // Wait before plumbing the next one so as to not overwhelm the poor bridge. await new Promise(r => setTimeout(r, PLUMB_WAIT_MS)); } catch (ex) { - await logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel ${channelName} to ${interest.roomId}`); + await logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel ${channelName} to ${interest.roomId}`, this.client); LogService.warn("IrcPlumbCommand", `Could not plumb channel ${channelName} to ${interest.roomId}:`, ex); } } @@ -66,7 +67,7 @@ export class IrcPlumbCommand implements ICommand { await this.ircBridge.plumbChannelToRoom(channel, resolvedRoomId); } catch (ex) { LogService.warn("IrcPlumbCommand", ex); - return logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel to room ${resolvedRoomId}`); + return logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel to room ${resolvedRoomId}`, this.client); } try { @@ -74,39 +75,39 @@ export class IrcPlumbCommand implements ICommand { await client.setUserPowerLevel(this.ircBridge.botUserId, resolvedRoomId, KickPowerLevel); } catch (ex) { LogService.warn("IrcPlumbCommand", ex); - return logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel to room ${resolvedRoomId}: could not set AS power level`); + return logMessage(LogLevel.WARN, "IrcPlumbCommand", `Could not plumb channel to room ${resolvedRoomId}: could not set AS power level`, this.client); } - logMessage(LogLevel.INFO,"IrcPlumbCommand", `Plumbed channel ${channel} to ${resolvedRoomId}`); + logMessage(LogLevel.INFO,"IrcPlumbCommand", `Plumbed channel ${channel} to ${resolvedRoomId}`, this.client); } - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - await client.sendReadReceipt(roomId, event['event_id']); + public async run(roomId: string, event: any, args: string[]) { + await this.client.sendReadReceipt(roomId, event['event_id']); const [channel, requestedRoomIdOrAlias] = args; if (channel === 'all') { try { - await this.plumbAll(conference, client, roomId); + await this.plumbAll(roomId); } catch (ex) { - return client.sendNotice(roomId, "Failed to bridge all rooms, see logs"); + return this.client.sendNotice(roomId, "Failed to bridge all rooms, see logs"); } - await client.sendNotice(roomId, "Rooms bridged to IRC"); + await this.client.sendNotice(roomId, "Rooms bridged to IRC"); return; } if (!this.ircBridge.isChannelAllowed(channel)) { - return client.sendNotice(roomId, "Sorry, that channel is not allowed"); + return this.client.sendNotice(roomId, "Sorry, that channel is not allowed"); } let resolvedRoomId: string; try { - resolvedRoomId = await client.resolveRoom(requestedRoomIdOrAlias); + resolvedRoomId = await this.client.resolveRoom(requestedRoomIdOrAlias); } catch (ex) { - return client.sendNotice(roomId, "Sorry, that alias could not be resolved"); + return this.client.sendNotice(roomId, "Sorry, that alias could not be resolved"); } try { - await client.joinRoom(requestedRoomIdOrAlias); + await this.client.joinRoom(requestedRoomIdOrAlias); } catch (ex) { - return client.sendNotice(roomId, "Could not join that room, is the bot invited?"); + return this.client.sendNotice(roomId, "Could not join that room, is the bot invited?"); } - return this.plumbOne(client, resolvedRoomId, channel); + return this.plumbOne(this.client, resolvedRoomId, channel); } } diff --git a/src/commands/JoinRoomCommand.ts b/src/commands/JoinRoomCommand.ts index c4353887..8281f376 100644 --- a/src/commands/JoinRoomCommand.ts +++ b/src/commands/JoinRoomCommand.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { ICommand } from "./ICommand"; -import { LogLevel, MatrixClient } from "matrix-bot-sdk"; +import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../Conference"; import { invitePersonToRoom, ResolvedPersonIdentifier } from "../invites"; import { logMessage } from "../LogProxy"; @@ -23,15 +23,17 @@ import { logMessage } from "../LogProxy"; export class JoinCommand implements ICommand { public readonly prefixes = ["join"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient) {} + + public async run(roomId: string, event: any, args: string[]) { if (!args.length) { - return client.replyNotice(roomId, event, "Please specify a room ID or alias"); + return this.client.replyNotice(roomId, event, "Please specify a room ID or alias"); } - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '⌛️'); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '⌛️'); - await client.joinRoom(args[0], []); + await this.client.joinRoom(args[0], []); - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } } diff --git a/src/commands/PermissionsCommand.ts b/src/commands/PermissionsCommand.ts index a161bb2b..dd516923 100644 --- a/src/commands/PermissionsCommand.ts +++ b/src/commands/PermissionsCommand.ts @@ -19,20 +19,23 @@ import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../Conference"; import { ResolvedPersonIdentifier } from "../invites"; import { runRoleCommand } from "./actions/roles"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export class PermissionsCommand implements ICommand { public readonly prefixes = ["permissions", "perms"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - await client.replyNotice(roomId, event, "Updating member permissions. This might take a while."); + constructor(private readonly client: ConferenceMatrixClient, private readonly conference: Conference) {} + + public async run(roomId: string, event: any, args: string[]) { + await this.client.replyNotice(roomId, event, "Updating member permissions. This might take a while."); // Much like the invite command, we iterate over pretty much every room and promote anyone // we think should be promoted. We don't remove people from power levels (that's left to the // existing room moderators/admins to deal with). - await runRoleCommand(PermissionsCommand.ensureModerator, conference, client, roomId, event, args, false); + await runRoleCommand(PermissionsCommand.ensureModerator, this.conference, this.client, roomId, event, args, false); - await client.sendNotice(roomId, "Member permissions updated"); + await this.client.sendNotice(roomId, "Member permissions updated"); } public static async ensureModerator(client: MatrixClient, roomId: string, people: ResolvedPersonIdentifier[]) { diff --git a/src/commands/RunCommand.ts b/src/commands/RunCommand.ts index d22e1947..0424399b 100644 --- a/src/commands/RunCommand.ts +++ b/src/commands/RunCommand.ts @@ -17,23 +17,24 @@ limitations under the License. import { ICommand } from "./ICommand"; import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../Conference"; -import config from "../config"; -import { ScheduledTaskType } from "../Scheduler"; +import { Scheduler } from "../Scheduler"; export class RunCommand implements ICommand { public readonly prefixes = ["run"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient, private readonly conference: Conference, private readonly scheduler: Scheduler) {} + + public async run(roomId: string, event: any, args: string[]) { const audId = args[0]; if (audId === "all") { - await config.RUNTIME.scheduler.addAuditorium("all"); + await this.scheduler.addAuditorium("all"); } else { - const aud = conference.getAuditorium(audId); - if (!aud) return await client.replyHtmlNotice(roomId, event, "Unknown auditorium"); + const aud = this.conference.getAuditorium(audId); + if (!aud) return await this.client.replyHtmlNotice(roomId, event, "Unknown auditorium"); - await config.RUNTIME.scheduler.addAuditorium(await aud.getId()); + await this.scheduler.addAuditorium(await aud.getId()); } - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } } diff --git a/src/commands/ScheduleCommand.ts b/src/commands/ScheduleCommand.ts index 5a2e3408..f1ed586c 100644 --- a/src/commands/ScheduleCommand.ts +++ b/src/commands/ScheduleCommand.ts @@ -15,24 +15,25 @@ limitations under the License. */ import { ICommand } from "./ICommand"; +import { Scheduler, getStartTime, sortTasks } from "../Scheduler"; +import moment = require("moment"); import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../Conference"; -import config from "../config"; -import { getStartTime, ScheduledTaskType, sortTasks } from "../Scheduler"; -import moment = require("moment"); export class ScheduleCommand implements ICommand { public readonly prefixes = ["schedule"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient, private readonly conference: Conference, private readonly scheduler: Scheduler) {} + + public async run(roomId: string, event: any, args: string[]) { if (args[0] === 'reset') { - await config.RUNTIME.scheduler.reset(); - await client.sendNotice(roomId, "Schedule processing has been reset."); + await this.scheduler.reset(); + await this.client.sendNotice(roomId, "Schedule processing has been reset."); } else if (args[0] === 'view') { - const upcoming = sortTasks(config.RUNTIME.scheduler.inspect()); + const upcoming = sortTasks(this.scheduler.inspect()); let html = "Upcoming tasks:
      "; for (const task of upcoming) { - const talkRoom = conference.getTalk(task.talk.id); + const talkRoom = this.conference.getTalk(task.talk.id); if (!talkRoom) continue; const taskStart = moment(getStartTime(task)); const formattedTimestamp = taskStart.format("YYYY-MM-DD HH:mm:ss [UTC]ZZ"); @@ -40,19 +41,19 @@ export class ScheduleCommand implements ICommand { if (html.length > 20000) { // chunk up the message so we don't fail to send one very large event. html += "
    "; - await client.sendHtmlNotice(roomId, html); + await this.client.sendHtmlNotice(roomId, html); html = "…
      "; } html += `
    • ${formattedTimestamp}: ${task.type} on ${await talkRoom.getName()} (${task.id}) ${taskStart.fromNow()}
    • `; } html += "
    "; - await client.sendHtmlNotice(roomId, html); + await this.client.sendHtmlNotice(roomId, html); } else if (args[0] === 'execute') { - await config.RUNTIME.scheduler.execute(args[1]); - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + await this.scheduler.execute(args[1]); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } else { - await client.sendNotice(roomId, "Unknown schedule command."); + await this.client.sendNotice(roomId, "Unknown schedule command."); } } } diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index 2041166c..de06fab1 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -18,17 +18,18 @@ import { ICommand } from "./ICommand"; import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../Conference"; import { Scheduler } from "../Scheduler"; -import config from "../config"; export class StatusCommand implements ICommand { public readonly prefixes = ["status", "stat", "refresh"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient, private readonly conference: Conference, private readonly scheduler: Scheduler) {} + + public async run(roomId: string, event: any, args: string[]) { let html = "

    Conference Bot Status

    "; - await client.sendReadReceipt(roomId, event['event_id']); + await this.client.sendReadReceipt(roomId, event['event_id']); - const backend = conference.backend; + const backend = this.conference.backend; let scheduleRefreshOk = false; try { @@ -40,14 +41,14 @@ export class StatusCommand implements ICommand { let roomStateBotResetOk = false; try { // Try to reset our view of the state first, to ensure we don't miss anything (e.g. if we got invited to a room since bot startup). - await conference.construct(); + await this.conference.construct(); roomStateBotResetOk = true; } catch (error) {} //////////////////////////////////////// html += "
    Schedule
      "; html += `
    • Schedule source healthy: ${(! backend.wasLoadedFromCache()) && scheduleRefreshOk}
    • `; - html += `
    • Conference ID: ${conference.id}
    • `; + html += `
    • Conference ID: ${this.conference.id}
    • `; html += "
    "; @@ -55,23 +56,22 @@ export class StatusCommand implements ICommand { //////////////////////////////////////// html += "
    Rooms
      "; html += `
    • State reconstruct healthy: ${roomStateBotResetOk}
    • `; - html += `
    • Conference space located: ${conference.hasRootSpace}
    • `; - html += `
    • Conference 'database room' located: ${conference.hasDbRoom}
    • `; - html += `
    • № auditoriums located: ${conference.storedAuditoriums.length}
    • `; - html += `
    • № auditorium backstages located: ${conference.storedAuditoriumBackstages.length}
    • `; - html += `
    • № talk rooms located: ${conference.storedTalks.length}
    • `; + html += `
    • Conference space located: ${this.conference.hasRootSpace}
    • `; + html += `
    • Conference 'database room' located: ${this.conference.hasDbRoom}
    • `; + html += `
    • № auditoriums located: ${this.conference.storedAuditoriums.length}
    • `; + html += `
    • № auditorium backstages located: ${this.conference.storedAuditoriumBackstages.length}
    • `; + html += `
    • № talk rooms located: ${this.conference.storedTalks.length}
    • `; html += "
    "; //////////////////////////////////////// html += "
    Scheduler
      "; - const scheduler: Scheduler = config.RUNTIME.scheduler; - html += `
    • Scheduled tasks yet to run: ${scheduler.inspect().length}
    • `; + html += `
    • Scheduled tasks yet to run: ${this.scheduler.inspect().length}
    • `; html += "
    "; - await client.sendHtmlNotice(roomId, html); + await this.client.sendHtmlNotice(roomId, html); } } diff --git a/src/commands/StopCommand.ts b/src/commands/StopCommand.ts index c1e6faba..b0b74b6c 100644 --- a/src/commands/StopCommand.ts +++ b/src/commands/StopCommand.ts @@ -16,15 +16,16 @@ limitations under the License. import { ICommand } from "./ICommand"; import { MatrixClient } from "matrix-bot-sdk"; -import { Conference } from "../Conference"; -import config from "../config"; -import { ScheduledTaskType } from "../Scheduler"; +import { Scheduler } from "../Scheduler"; export class StopCommand implements ICommand { public readonly prefixes = ["stop"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { - await config.RUNTIME.scheduler.stop(); - await client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); + constructor(private readonly client: MatrixClient, private readonly scheduler: Scheduler) {} + + + public async run(roomId: string, event: any, args: string[]) { + await this.scheduler.stop(); + await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } } diff --git a/src/commands/VerifyCommand.ts b/src/commands/VerifyCommand.ts index f483c54c..b4946be1 100644 --- a/src/commands/VerifyCommand.ts +++ b/src/commands/VerifyCommand.ts @@ -26,22 +26,24 @@ import { IPerson } from "../models/schedule"; export class VerifyCommand implements ICommand { public readonly prefixes = ["verify", "v"]; - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + constructor(private readonly client: MatrixClient, private readonly conference: Conference) {} + + public async run(roomId: string, event: any, args: string[]) { const audId = args[0]; - let aud: PhysicalRoom = conference.getAuditorium(audId); + let aud: PhysicalRoom = this.conference.getAuditorium(audId); if (args.includes("backstage")) { - aud = conference.getAuditoriumBackstage(audId); + aud = this.conference.getAuditoriumBackstage(audId); } if (!aud) { - aud = conference.getInterestRoom(audId); + aud = this.conference.getInterestRoom(audId); if (!aud) { - return await client.replyNotice(roomId, event, "Unknown auditorium/interest room"); + return await this.client.replyNotice(roomId, event, "Unknown auditorium/interest room"); } } - await client.replyNotice(roomId, event, "Calculating list of people..."); + await this.client.replyNotice(roomId, event, "Calculating list of people..."); let html = `

    ${await aud.getName()} (${await aud.getId()})

    `; @@ -57,18 +59,18 @@ export class VerifyCommand implements ICommand { let audToMod: IPerson[]; if (aud instanceof Auditorium) { - audToInvite = await conference.getInviteTargetsForAuditorium(aud); - audBackstageToInvite = await conference.getInviteTargetsForAuditorium(aud, true); - audToMod = await conference.getModeratorsForAuditorium(aud); + audToInvite = await this.conference.getInviteTargetsForAuditorium(aud); + audBackstageToInvite = await this.conference.getInviteTargetsForAuditorium(aud, true); + audToMod = await this.conference.getModeratorsForAuditorium(aud); } else if (aud instanceof InterestRoom) { - audToInvite = await conference.getInviteTargetsForInterest(aud); + audToInvite = await this.conference.getInviteTargetsForInterest(aud); audBackstageToInvite = []; - audToMod = await conference.getModeratorsForInterest(aud); + audToMod = await this.conference.getModeratorsForInterest(aud); } else { - return await client.replyNotice(roomId, event, "Unknown room kind"); + return await this.client.replyNotice(roomId, event, "Unknown room kind"); } - const publicAud = conference.getAuditorium(audId); + const publicAud = this.conference.getAuditorium(audId); if (publicAud || !(aud instanceof Auditorium)) { html += "Public-facing room:
      "; appendPeople(audToInvite, audToMod); @@ -79,10 +81,10 @@ export class VerifyCommand implements ICommand { appendPeople(audBackstageToInvite, audToMod); html += "
    "; - const talks = await asyncFilter(conference.storedTalks, async t => (await t.getAuditoriumId()) === (await aud.getId())); + const talks = await asyncFilter(this.conference.storedTalks, async t => (await t.getAuditoriumId()) === (await aud.getId())); for (const talk of talks) { - const talkToInvite = await conference.getInviteTargetsForTalk(talk); - const talkToMod = await conference.getModeratorsForTalk(talk); + const talkToInvite = await this.conference.getInviteTargetsForTalk(talk); + const talkToMod = await this.conference.getModeratorsForTalk(talk); if (talkToMod.length || talkToInvite.length) { html += `Talk: ${await talk.getName()} (${await talk.getId()})
      `; appendPeople(talkToInvite, talkToMod); @@ -91,6 +93,6 @@ export class VerifyCommand implements ICommand { } } - await client.sendHtmlNotice(roomId, html); + await this.client.sendHtmlNotice(roomId, html); } } diff --git a/src/commands/WidgetsCommand.ts b/src/commands/WidgetsCommand.ts index 22ba98e1..b6c23e3a 100644 --- a/src/commands/WidgetsCommand.ts +++ b/src/commands/WidgetsCommand.ts @@ -20,53 +20,58 @@ import { Conference } from "../Conference"; import { LiveWidget } from "../models/LiveWidget"; import { asyncFilter } from "../utils"; import { Auditorium } from "../models/Auditorium"; +import { IConfig } from "../config"; export class WidgetsCommand implements ICommand { + constructor(private readonly client: MatrixClient, private readonly conference: Conference, private readonly config: IConfig) {} + public readonly prefixes = ["widgets"]; - private async addToRoom(aud: Auditorium, client: MatrixClient, conference: Conference) { - const audWidget = await LiveWidget.forAuditorium(aud, client); + private async addToRoom(aud: Auditorium) { + const avatar = this.config.livestream.widgetAvatar; + const baseUrl = this.config.webserver.publicBaseUrl; + const audWidget = await LiveWidget.forAuditorium(aud, this.client, avatar, baseUrl); const audLayout = LiveWidget.layoutForAuditorium(audWidget); - const audSchedule = await LiveWidget.scheduleForAuditorium(aud, client); - await client.sendStateEvent(aud.roomId, audWidget.type, audWidget.state_key, audWidget.content); - await client.sendStateEvent(aud.roomId, audSchedule.type, audSchedule.state_key, audSchedule.content); - await client.sendStateEvent(aud.roomId, audLayout.type, audLayout.state_key, audLayout.content); + const audSchedule = await LiveWidget.scheduleForAuditorium(aud, this.client, avatar, baseUrl); + await this.client.sendStateEvent(aud.roomId, audWidget.type, audWidget.state_key, audWidget.content); + await this.client.sendStateEvent(aud.roomId, audSchedule.type, audSchedule.state_key, audSchedule.content); + await this.client.sendStateEvent(aud.roomId, audLayout.type, audLayout.state_key, audLayout.content); - const talks = await asyncFilter(conference.storedTalks, async t => (await t.getAuditoriumId()) === (await aud.getId())); + const talks = await asyncFilter(this.conference.storedTalks, async t => (await t.getAuditoriumId()) === (await aud.getId())); for (const talk of talks) { - const talkWidget = await LiveWidget.forTalk(talk, client); - const scoreboardWidget = await LiveWidget.scoreboardForTalk(talk, client); + const talkWidget = await LiveWidget.forTalk(talk, this.client, avatar, baseUrl); + const scoreboardWidget = await LiveWidget.scoreboardForTalk(talk, this.client, this.conference, avatar, baseUrl); const talkLayout = LiveWidget.layoutForTalk(talkWidget, scoreboardWidget); - await client.sendStateEvent(talk.roomId, talkWidget.type, talkWidget.state_key, talkWidget.content); - await client.sendStateEvent(talk.roomId, scoreboardWidget.type, scoreboardWidget.state_key, scoreboardWidget.content); - await client.sendStateEvent(talk.roomId, talkLayout.type, talkLayout.state_key, talkLayout.content); + await this.client.sendStateEvent(talk.roomId, talkWidget.type, talkWidget.state_key, talkWidget.content); + await this.client.sendStateEvent(talk.roomId, scoreboardWidget.type, scoreboardWidget.state_key, scoreboardWidget.content); + await this.client.sendStateEvent(talk.roomId, talkLayout.type, talkLayout.state_key, talkLayout.content); } if ((await aud.getDefinition()).isPhysical) { // For physical auditoriums, the talks don't have anywhere to display a Q&A scoreboard. // So what we do instead is add a Q&A scoreboard to the backstage room, so that an organiser can read off // any questions if necessary. - const backstage = conference.getAuditoriumBackstage(await aud.getId()); - const audScoreboardWidget = await LiveWidget.scoreboardForAuditorium(aud, client); + const backstage = this.conference.getAuditoriumBackstage(await aud.getId()); + const audScoreboardWidget = await LiveWidget.scoreboardForAuditorium(aud, this.client, avatar, baseUrl); const backstageLayout = LiveWidget.layoutForPhysicalAudBackstage(audScoreboardWidget); - await client.sendStateEvent(backstage.roomId, audScoreboardWidget.type, audScoreboardWidget.state_key, audScoreboardWidget.content); - await client.sendStateEvent(backstage.roomId, backstageLayout.type, backstageLayout.state_key, backstageLayout.content); + await this.client.sendStateEvent(backstage.roomId, audScoreboardWidget.type, audScoreboardWidget.state_key, audScoreboardWidget.content); + await this.client.sendStateEvent(backstage.roomId, backstageLayout.type, backstageLayout.state_key, backstageLayout.content); } } - public async run(conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[]) { + public async run(roomId: string, event: any, args: string[]) { if (args[0] === 'all') { - for (const aud of conference.storedAuditoriums) { - await this.addToRoom(aud, client, conference); + for (const aud of this.conference.storedAuditoriums) { + await this.addToRoom(aud); } } else { - const aud = await conference.getAuditorium(args[0]); + const aud = await this.conference.getAuditorium(args[0]); if (!aud) { - return client.replyNotice(roomId, event, "Auditorium not found"); + return this.client.replyNotice(roomId, event, "Auditorium not found"); } - await this.addToRoom(aud, client, conference); + await this.addToRoom(aud); } - await client.replyNotice(roomId, event, "Widgets created"); + await this.client.replyNotice(roomId, event, "Widgets created"); } } diff --git a/src/commands/actions/people.ts b/src/commands/actions/people.ts index cc85a8c7..b42cd173 100644 --- a/src/commands/actions/people.ts +++ b/src/commands/actions/people.ts @@ -20,6 +20,7 @@ import { Auditorium } from "../../models/Auditorium"; import { Conference } from "../../Conference"; import { asyncFilter } from "../../utils"; import { InterestRoom } from "../../models/InterestRoom"; +import { ConferenceMatrixClient } from "../../ConferenceMatrixClient"; export interface IAction { (client: MatrixClient, roomId: string, people: ResolvedPersonIdentifier[]): Promise; @@ -27,7 +28,7 @@ export interface IAction { export async function doAuditoriumResolveAction( action: IAction, - client: MatrixClient, + client: ConferenceMatrixClient, aud: Auditorium, conference: Conference, backstageOnly = false, @@ -41,14 +42,14 @@ export async function doAuditoriumResolveAction( ? await conference.getInviteTargetsForAuditorium(aud, true) : await conference.getModeratorsForAuditorium(aud); LogService.info("backstagePeople", `${backstagePeople}`); - const resolvedBackstagePeople = await resolveIdentifiers(backstagePeople); + const resolvedBackstagePeople = await resolveIdentifiers(client, backstagePeople); const backstage = conference.getAuditoriumBackstage(audId); LogService.info("resolvedBackstagePeople", `${resolvedBackstagePeople}`); const allPossiblePeople = isInvite ? resolvedBackstagePeople - : await resolveIdentifiers(await conference.getInviteTargetsForAuditorium(aud, true)); + : await resolveIdentifiers(client, await conference.getInviteTargetsForAuditorium(aud, true)); await action(client, backstage.roomId, resolvedBackstagePeople); @@ -87,11 +88,11 @@ export async function doAuditoriumResolveAction( } } -export async function doInterestResolveAction(action: IAction, client: MatrixClient, int: InterestRoom, conference: Conference, isInvite = true): Promise { +export async function doInterestResolveAction(action: IAction, client: ConferenceMatrixClient, int: InterestRoom, conference: Conference, isInvite = true): Promise { // We know that everyone should be in the backstage room, so resolve that list of people // to make the identity server lookup efficient. const people = isInvite ? await conference.getInviteTargetsForInterest(int) : await conference.getModeratorsForInterest(int); - await action(client, int.roomId, await resolveIdentifiers(people)); + await action(client, int.roomId, await resolveIdentifiers(client, people)); } diff --git a/src/commands/actions/roles.ts b/src/commands/actions/roles.ts index f06a2cc0..02010861 100644 --- a/src/commands/actions/roles.ts +++ b/src/commands/actions/roles.ts @@ -17,8 +17,9 @@ limitations under the License. import { doAuditoriumResolveAction, doInterestResolveAction, IAction } from "./people"; import { MatrixClient } from "matrix-bot-sdk"; import { Conference } from "../../Conference"; +import { ConferenceMatrixClient } from "../../ConferenceMatrixClient"; -export async function runRoleCommand(action: IAction, conference: Conference, client: MatrixClient, roomId: string, event: any, args: string[], isInvite = true) { +export async function runRoleCommand(action: IAction, conference: Conference, client: ConferenceMatrixClient, roomId: string, event: any, args: string[], isInvite = true) { const backstageOnly = args.includes("backstage"); const skipTalks = args.includes("notalks"); diff --git a/src/config.ts b/src/config.ts index cc3fd670..1ed910ff 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,13 +15,9 @@ limitations under the License. */ import * as config from "config"; -import { MatrixClient } from "matrix-bot-sdk"; -import { Conference } from "./Conference"; -import { IRCBridge, IRCBridgeOpts } from "./IRCBridge"; -import { Scheduler } from "./Scheduler"; -import { CheckInMap } from "./CheckInMap"; +import { IRCBridgeOpts } from "./IRCBridge"; -interface IConfig { +export interface IConfig { homeserverUrl: string; accessToken: string; userId: string; @@ -74,14 +70,6 @@ interface IConfig { }; }; ircBridge: IRCBridgeOpts | null; - - RUNTIME: { - client: MatrixClient; - conference: Conference; - scheduler: Scheduler; - ircBridge: IRCBridge | null; - checkins: CheckInMap; - }; } export interface IPrefixConfig { diff --git a/src/index.ts b/src/index.ts index b543d09d..008ff0e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,9 +18,9 @@ limitations under the License. // TODO: Timezones!! (Europe-Brussels) // TODO: Start webserver -import { LogLevel, LogService, MatrixClient, SimpleFsStorageProvider, UserID } from "matrix-bot-sdk"; +import { LogLevel, LogService, SimpleFsStorageProvider, UserID } from "matrix-bot-sdk"; import * as path from "path"; -import config, { IPentaScheduleBackendConfig } from "./config"; +import runtimeConfig, { IConfig, IPentaScheduleBackendConfig } from "./config"; import { ICommand } from "./commands/ICommand"; import { HelpCommand } from "./commands/HelpCommand"; import { BuildCommand } from "./commands/BuildCommand"; @@ -61,224 +61,237 @@ import { JsonScheduleBackend } from "./backends/json/JsonScheduleBackend"; import { JoinCommand } from "./commands/JoinRoomCommand"; import { StatusCommand } from "./commands/StatusCommand"; import { CachingBackend } from "./backends/CachingBackend"; - -config.RUNTIME = { - // TODO `null!` is ... nasty. - client: null!, - conference: null!, - scheduler: null!, - ircBridge: null, - checkins: null!, -}; - -process.on('SIGINT', () => { - // Die immediately - // TODO: Wait for pending tasks - process.exit(); -}); +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; +import { Server } from "http"; LogService.setLogger(new CustomLogger()); LogService.setLevel(LogLevel.DEBUG); LogService.info("index", "Bot starting..."); -const storage = new SimpleFsStorageProvider(path.join(config.dataPath, "bot.json")); -const client = new MatrixClient(config.homeserverUrl, config.accessToken, storage); -config.RUNTIME.client = client; -client.impersonateUserId(config.userId); - -let localpart; -let displayName; -let userId; - -(async function () { - const backend = await loadBackend(); - - const conference = new Conference(backend, config.conference.id, client); - config.RUNTIME.conference = conference; - - const scoreboard = new Scoreboard(conference, client); - - const scheduler = new Scheduler(client, conference, scoreboard); - config.RUNTIME.scheduler = scheduler; - - let ircBridge: IRCBridge | null = null; - if (config.ircBridge != null) { - ircBridge = new IRCBridge(config.ircBridge, client); +export class ConferenceBot { + private webServer?: Server; + private static async loadBackend(config: IConfig) { + switch (config.conference.schedule.backend) { + case "penta": + return await CachingBackend.new(() => PentaBackend.new(config), path.join(config.dataPath, "penta_cache.json")); + case "json": + return await JsonScheduleBackend.new(config.dataPath, config.conference.schedule); + default: + throw new Error(`Unknown scheduling backend: choose penta or json!`) + } } - config.RUNTIME.ircBridge = ircBridge; - - const checkins = new CheckInMap(client, conference); - config.RUNTIME.checkins = checkins; - - // Quickly check connectivity before going much further - userId = await client.getUserId(); - LogService.info("index", "Running as ", userId); - - localpart = new UserID(userId).localpart; - - try { - const profile = await client.getUserProfile(userId); - displayName = profile?.displayname ?? localpart; - } catch (ex) { - LogService.warn("index", "The bot has no profile. Consider setting one."); - // No profile set, assume localpart. - displayName = localpart; + public static async start(config: IConfig): Promise { + const storage = new SimpleFsStorageProvider(path.join(config.dataPath, "bot.json")); + const client = await ConferenceMatrixClient.create(config, storage); + client.impersonateUserId(config.userId); + const backend = await this.loadBackend(config); + const conference = new Conference(backend, config.conference.id, client, config); + const checkins = new CheckInMap(client, config); + const scoreboard = new Scoreboard(conference, client, config); + const scheduler = new Scheduler(client, conference, scoreboard, checkins, config); + + let ircBridge: IRCBridge | null = null; + if (config.ircBridge != null) { + ircBridge = new IRCBridge(config, client); + } + + + return new ConferenceBot(config, backend, client, conference, scoreboard, scheduler, ircBridge); } - registerCommands(conference, ircBridge); - - await client.joinRoom(config.managementRoom); - - await conference.construct(); + private constructor( + private readonly config: IConfig, + private readonly backend: IScheduleBackend, + public readonly client: ConferenceMatrixClient, + public readonly conference: Conference, + public readonly scoreboard: Scoreboard, + public readonly scheduler: Scheduler, + private readonly ircBridge: IRCBridge|null) { - setupWebserver(scoreboard); - - if (!conference.isCreated) { - await client.sendHtmlNotice(config.managementRoom, "" + - "

      Welcome!

      " + - "

      Your conference hasn't been built yet (or I don't know of it). If your config is correct, run !conference build to start building your conference.

      " - ); - } else { - await client.sendHtmlNotice(config.managementRoom, "" + - "

      Bot restarted

      " + - "

      I am ready to start performing conference actions.

      " - ); - } - - if (backend.wasLoadedFromCache()) { - await client.sendHtmlText(config.managementRoom, "" + - "

      ⚠ Cached schedule in use ⚠

      " + - "

      @room ⚠ The bot failed to load the schedule properly and a cached copy is being used.

      " - ); } - // Load the previous room scoreboards. This has to happen before we start syncing, otherwise - // new scoreboard changes will get lost. The `MatrixClient` resumes syncing from where it left - // off, so events will only be missed if the bot dies while processing them. - await scoreboard.load(); - await scheduler.prepare(); - await client.start(); - - // Needs to happen after the sync loop has started - if (ircBridge !== null) { + public async main() { + let localpart; + let displayName; + let userId; + // Quickly check connectivity before going much further + userId = await this.client.getUserId(); + LogService.info("index", "Running as ", userId); + + localpart = new UserID(userId).localpart; + + try { + const profile = await this.client.getUserProfile(userId); + displayName = profile?.displayname ?? localpart; + } catch (ex) { + LogService.warn("index", "The bot has no profile. Consider setting one."); + // No profile set, assume localpart. + displayName = localpart; + } + + this.registerCommands(userId, localpart, displayName); + + await this.client.joinRoom(this.config.managementRoom); + + await this.conference.construct(); + + this.setupWebserver(); + + if (!this.conference.isCreated) { + await this.client.sendHtmlNotice(this.config.managementRoom, "" + + "

      Welcome!

      " + + "

      Your conference hasn't been built yet (or I don't know of it). If your this.config is correct, run !conference build to start building your conference.

      " + ); + } else { + await this.client.sendHtmlNotice(this.config.managementRoom, "" + + "

      Bot restarted

      " + + "

      I am ready to start performing conference actions.

      " + ); + } + + if (this.backend.wasLoadedFromCache()) { + await this.client.sendHtmlText(this.config.managementRoom, "" + + "

      ⚠ Cached schedule in use ⚠

      " + + "

      @room ⚠ The bot failed to load the schedule properly and a cached copy is being used.

      " + ); + } + + // Load the previous room scoreboards. This has to happen before we start syncing, otherwise + // new scoreboard changes will get lost. The `MatrixClient` resumes syncing from where it left + // off, so events will only be missed if the bot dies while processing them. + await this.scoreboard.load(); + + await this.scheduler.prepare(); + await this.client.start(); + + // Needs to happen after the sync loop has started // Note that the IRC bridge will cause a crash if wrongly configured, so be cautious that it's not // wrongly enabled in conferences without one. - await ircBridge.setup(); + await this.ircBridge?.setup(); } -})().catch((ex) => { - LogService.error("index", "Fatal error", ex); - process.exit(1); -}); - -async function loadBackend(): Promise { - switch (config.conference.schedule.backend) { - case "penta": - const pentaCfg: IPentaScheduleBackendConfig = config.conference.schedule; - return await CachingBackend.new(() => PentaBackend.new(pentaCfg), path.join(config.dataPath, "penta_cache.json")); - case "json": - return await JsonScheduleBackend.new(config.conference.schedule); - default: - throw new Error(`Unknown scheduling backend: choose penta or json!`) - } -} -function registerCommands(conference: Conference, ircBridge: IRCBridge | null) { - const commands: ICommand[] = [ - new HelpCommand(), - new BuildCommand(), - new VerifyCommand(), - new InviteCommand(), - new DevCommand(), - new PermissionsCommand(), - new InviteMeCommand(), - new JoinCommand(), - new WidgetsCommand(), - new RunCommand(), - new StopCommand(), - new CopyModeratorsCommand(), - new AttendanceCommand(), - new ScheduleCommand(), - new FDMCommand(), - new StatusCommand(), - ]; - if (ircBridge !== null) { - commands.push(new IrcPlumbCommand(ircBridge)); + private async setupWebserver() { + const app = express(); + const tmplPath = process.env.CONF_TEMPLATES_PATH || './srv'; + const engine = new Liquid({ + root: tmplPath, + cache: process.env.NODE_ENV === 'production', + }); + app.use(express.urlencoded({extended: true})); + app.use('/assets', express.static(this.config.webserver.additionalAssetsPath)); + app.use('/bundles', express.static(path.join(tmplPath, 'bundles'))); + app.engine('liquid', engine.express()); + app.set('views', tmplPath); + app.set('view engine', 'liquid'); + app.get('/widgets/auditorium.html', (req, res) => renderAuditoriumWidget(req, res, this.conference, this.config.livestream.auditoriumUrl)); + app.get('/widgets/talk.html', (req, res) => renderTalkWidget(req, res, this.conference, this.config.livestream.talkUrl, this.config.livestream.jitsiDomain)); + app.get('/widgets/scoreboard.html', (req, res) => renderScoreboardWidget(req,res, this.conference)); + app.get('/widgets/hybrid.html', (req, res) => renderHybridWidget(req, res, this.config.livestream.hybridUrl, this.config.livestream.jitsiDomain)); + app.post('/onpublish', (req, res) => rtmpRedirect(req,res,this.conference, this.config.livestream.onpublish)); + app.get('/healthz', renderHealthz); + app.get('/scoreboard/:roomId', (rq, rs) => renderScoreboard(rq, rs, this.scoreboard, this.conference)); + app.get('/make_hybrid', (req, res) => makeHybridWidget(req, res, this.client, this.config.livestream.widgetAvatar, this.config.webserver.publicBaseUrl)); + this.webServer = app.listen(this.config.webserver.port, this.config.webserver.address, () => { + LogService.info("web", `Webserver running at http://${this.config.webserver.address}:${this.config.webserver.port}`); + }); } - client.on("room.message", async (roomId: string, event: any) => { - if (roomId !== config.managementRoom) return; - if (!event['content']) return; - if (event['content']['msgtype'] !== 'm.text') return; - if (!event['content']['body']) return; - - // Check age just in case we recently started - const now = Date.now(); - if (Math.abs(now - event['origin_server_ts']) >= 900000) { // 15min - LogService.warn("index", `Ignoring ${event['event_id']} in management room due to age`); - return; - } - - const content = event['content']; - - const prefixes = [ - "!conference", - localpart + ":", - displayName + ":", - userId + ":", - localpart + " ", - displayName + " ", - userId + " ", + private async registerCommands(userId: string, localpart: string, displayName: string) { + const commands: ICommand[] = [ + new AttendanceCommand(this.client, this.conference), + new BuildCommand(this.client, this.conference, this.config), + new CopyModeratorsCommand(this.client), + new DevCommand(this.client, this.conference), + new FDMCommand(this.client, this.conference), + new HelpCommand(this.client), + new InviteCommand(this.client, this.conference, this.config), + new InviteMeCommand(this.client, this.conference), + new JoinCommand(this.client), + new PermissionsCommand(this.client, this.conference), + new RunCommand(this.client, this.conference, this.scheduler), + new ScheduleCommand(this.client, this.conference, this.scheduler), + new StatusCommand(this.client, this.conference, this.scheduler), + new StopCommand(this.client, this.scheduler), + new VerifyCommand(this.client, this.conference), + new WidgetsCommand(this.client, this.conference, this.config), ]; + if (this.ircBridge !== null) { + commands.push(new IrcPlumbCommand(this.client, this.conference, this.ircBridge)); + } - const prefixUsed = prefixes.find(p => content['body'].startsWith(p)); - if (!prefixUsed) return; + this.client.on("room.message", async (roomId: string, event: any) => { + if (roomId !== this.config.managementRoom) return; + if (!event['content']) return; + if (event['content']['msgtype'] !== 'm.text') return; + if (!event['content']['body']) return; + + // Check age just in case we recently started + const now = Date.now(); + if (Math.abs(now - event['origin_server_ts']) >= 900000) { // 15min + LogService.warn("index", `Ignoring ${event['event_id']} in management room due to age`); + return; + } - const restOfBody = content['body'].substring(prefixUsed.length).trim(); - const args = restOfBody.split(' '); - if (args.length <= 0) { - return await client.replyNotice(roomId, event, `Invalid command. Try ${prefixUsed.trim()} help`); - } + const content = event['content']; + + const prefixes = [ + "!conference", + localpart + ":", + displayName + ":", + userId + ":", + localpart + " ", + displayName + " ", + userId + " ", + ]; + + const prefixUsed = prefixes.find(p => content['body'].startsWith(p)); + if (!prefixUsed) return; + + const restOfBody = content['body'].substring(prefixUsed.length).trim(); + const args = restOfBody.split(' '); + if (args.length <= 0) { + return await this.client.replyNotice(roomId, event, `Invalid command. Try ${prefixUsed.trim()} help`); + } - try { - for (const command of commands) { - if (command.prefixes.includes(args[0].toLowerCase())) { - LogService.info("index", `${event['sender']} is running command: ${content['body']}`); - return await command.run(conference, client, roomId, event, args.slice(1)); + try { + for (const command of commands) { + if (command.prefixes.includes(args[0].toLowerCase())) { + LogService.info("index", `${event['sender']} is running command: ${content['body']}`); + return await command.run(roomId, event, args.slice(1)); + } } + } catch (e) { + LogService.error("index", "Error processing command: ", e); + return await this.client.replyNotice(roomId, event, `There was an error processing your command: ${e?.message}`); } - } catch (e) { - LogService.error("index", "Error processing command: ", e); - return await client.replyNotice(roomId, event, `There was an error processing your command: ${e?.message}`); - } - return await client.replyNotice(roomId, event, `Unknown command. Try ${prefixUsed.trim()} help`); - }); + return await this.client.replyNotice(roomId, event, `Unknown command. Try ${prefixUsed.trim()} help`); + }); + } + + public async stop() { + await this.scheduler.stop(); + this.client.stop(); + this.webServer?.close(); + } } -function setupWebserver(scoreboard: Scoreboard) { - const app = express(); - const tmplPath = process.env.CONF_TEMPLATES_PATH || './srv'; - const engine = new Liquid({ - root: tmplPath, - cache: process.env.NODE_ENV === 'production', - }); - app.use(express.urlencoded({extended: true})); - app.use('/assets', express.static(config.webserver.additionalAssetsPath)); - app.use('/bundles', express.static(path.join(tmplPath, 'bundles'))); - app.engine('liquid', engine.express()); - app.set('views', tmplPath); - app.set('view engine', 'liquid'); - app.get('/widgets/auditorium.html', renderAuditoriumWidget); - app.get('/widgets/talk.html', renderTalkWidget); - app.get('/widgets/scoreboard.html', renderScoreboardWidget); - app.get('/widgets/hybrid.html', renderHybridWidget); - app.post('/onpublish', rtmpRedirect); - app.get('/healthz', renderHealthz); - app.get('/scoreboard/:roomId', (rq, rs) => renderScoreboard(rq, rs, scoreboard)); - app.get('/make_hybrid', makeHybridWidget); - app.listen(config.webserver.port, config.webserver.address, () => { - LogService.info("web", `Webserver running at http://${config.webserver.address}:${config.webserver.port}`); +if (require.main === module) { + (async function () { + const conf = await ConferenceBot.start(runtimeConfig); + process.on('SIGINT', () => { + conf.stop().then(() => { + process.exit(0); + }).catch(ex => { + LogService.warn("index", "Failed to exit gracefully", ex); + process.exit(1); + }) + }); + await conf.main(); + })().catch((ex) => { + LogService.error("index", "Fatal error", ex); + process.exit(1); }); } diff --git a/src/invites.ts b/src/invites.ts index 79099e5d..8035867b 100644 --- a/src/invites.ts +++ b/src/invites.ts @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IdentityClient, LogLevel } from "matrix-bot-sdk"; -import config from "./config"; +import { LogLevel, LogService } from "matrix-bot-sdk"; import { RS_3PID_PERSON_ID } from "./models/room_state"; import { logMessage } from "./LogProxy"; import { IPerson } from "./models/schedule"; - -let idClient: IdentityClient; +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; const MAX_EMAILS_PER_BATCH = 1000; @@ -30,17 +28,13 @@ export interface ResolvedPersonIdentifier { person: IPerson; } -async function ensureIdentityClient() { - if (!idClient) { - idClient = await config.RUNTIME.client.getIdentityServerClient(config.idServerDomain); - await idClient.acceptAllTerms(); - idClient.brand = config.idServerBrand; - } -} - -async function resolveBatch(batch: IPerson[]): Promise { +async function resolveBatch(client: ConferenceMatrixClient, batch: IPerson[]): Promise { if (batch.length <= 0) return []; - const results = await idClient.lookup(batch.map(p => ({address: p.email, kind: "email"}))); + if (!client.identityClient) { + LogService.warn("invites", "No identity client configured, returning empty"); + return []; + } + const results = await client.identityClient.lookup(batch.map(p => ({address: p.email, kind: "email"}))); const resolved: ResolvedPersonIdentifier[] = []; for (let i = 0; i < results.length; i++) { const result = results[i]; @@ -54,15 +48,13 @@ async function resolveBatch(batch: IPerson[]): Promise { - await ensureIdentityClient(); - +export async function resolveIdentifiers(client: ConferenceMatrixClient, people: IPerson[]): Promise { const resolved: ResolvedPersonIdentifier[] = []; const pendingLookups: IPerson[] = []; const doResolve = async () => { - const results = await resolveBatch(pendingLookups); + const results = await resolveBatch(client, pendingLookups); resolved.push(...results); }; @@ -72,7 +64,7 @@ export async function resolveIdentifiers(people: IPerson[]): Promise { +export async function invitePersonToRoom(client: ConferenceMatrixClient, resolvedPerson: ResolvedPersonIdentifier, roomId: string): Promise { if (resolvedPerson.mxid) { - return await config.RUNTIME.client.inviteUser(resolvedPerson.mxid.trim(), roomId); - } - - await ensureIdentityClient(); + return await client.inviteUser(resolvedPerson.mxid.trim(), roomId); + } if (!resolvedPerson.emails) { throw new Error(`No e-mail addresses for resolved person ${resolvedPerson.person.id}.`); } + if (!client.identityClient) { + throw new Error(`No identity client configured, cannot make email invite.`); + + } + for (const email of resolvedPerson.emails) { - const idInvite = await idClient.makeEmailInvite(email, roomId); + const idInvite = await client.identityClient.makeEmailInvite(email, roomId); const content = { display_name: idInvite.display_name, // XXX: https://github.com/matrix-org/matrix-doc/issues/2948 - key_validity_url: `${idClient.serverUrl}/_matrix/identity/v2/pubkey/ephemeral/isvalid`, + key_validity_url: `${client.identityClient.serverUrl}/_matrix/identity/v2/pubkey/ephemeral/isvalid`, public_key: idInvite.public_key, public_keys: idInvite.public_keys, [RS_3PID_PERSON_ID]: resolvedPerson.person.id, }; const stateKey = idInvite.token; // not included in the content - await config.RUNTIME.client.sendStateEvent(roomId, "m.room.third_party_invite", stateKey, content); + await client.sendStateEvent(roomId, "m.room.third_party_invite", stateKey, content); } } diff --git a/src/models/InterestRoom.ts b/src/models/InterestRoom.ts index 7d978bb8..aa288aa9 100644 --- a/src/models/InterestRoom.ts +++ b/src/models/InterestRoom.ts @@ -19,7 +19,7 @@ import { Conference } from "../Conference"; import { MatrixRoom } from "./MatrixRoom"; import { decodePrefix } from "../backends/penta/PentabarfParser"; import { PhysicalRoom } from "./PhysicalRoom"; -import config from "../config"; +import { IPrefixConfig } from "../config"; /** * Represents an interest room. @@ -31,11 +31,11 @@ export class InterestRoom extends MatrixRoom implements PhysicalRoom { private id: string; private name: string; - constructor(roomId: string, client: MatrixClient, conference: Conference, id: string) { + constructor(roomId: string, client: MatrixClient, conference: Conference, id: string, prefixes: IPrefixConfig) { super(roomId, client, conference); this.id = id; - this.name = decodePrefix(id, config.conference.prefixes)!.name; + this.name = decodePrefix(id, prefixes)!.name; } public async getName(): Promise { diff --git a/src/models/LiveWidget.ts b/src/models/LiveWidget.ts index b7b91b93..6db08f2e 100644 --- a/src/models/LiveWidget.ts +++ b/src/models/LiveWidget.ts @@ -18,11 +18,10 @@ import { IStateEvent } from "./room_state"; import { IWidget } from "matrix-widget-api"; import { sha256 } from "../utils"; import { Auditorium } from "./Auditorium"; -import config from "../config"; import { MatrixClient } from "matrix-bot-sdk"; import { Talk } from "./Talk"; import * as template from "string-template"; -import { base32 } from "rfc4648"; +import { Conference } from "../Conference"; export interface ILayout { widgets: { @@ -36,11 +35,9 @@ export interface ILayout { } export class LiveWidget { - private constructor() { - // nothing - } + private constructor() { } - public static async forAuditorium(aud: Auditorium, client: MatrixClient): Promise> { + public static async forAuditorium(aud: Auditorium, client: MatrixClient, avatar: string, baseUrl: string): Promise> { const widgetId = sha256(JSON.stringify(await aud.getDefinition())); return { type: "im.vector.modular.widgets", @@ -51,8 +48,8 @@ export class LiveWidget { type: "m.custom", waitForIframeLoad: true, name: "Livestream", - avatar_url: config.livestream.widgetAvatar, - url: config.webserver.publicBaseUrl + "/widgets/auditorium.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&theme=$theme", + avatar_url: avatar, + url: baseUrl + "/widgets/auditorium.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&theme=$theme", data: { title: await aud.getName(), auditoriumId: await aud.getId(), @@ -61,7 +58,7 @@ export class LiveWidget { }; } - public static async forTalk(talk: Talk, client: MatrixClient): Promise> { + public static async forTalk(talk: Talk, client: MatrixClient, avatar: string, baseUrl: string): Promise> { const widgetId = sha256(JSON.stringify(await talk.getDefinition())); return { type: "im.vector.modular.widgets", @@ -72,8 +69,8 @@ export class LiveWidget { type: "m.custom", waitForIframeLoad: true, name: "Livestream / Q&A", - avatar_url: config.livestream.widgetAvatar, - url: config.webserver.publicBaseUrl + "/widgets/talk.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&talkId=$talkId&theme=$theme#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt", + avatar_url: avatar, + url: baseUrl + "/widgets/talk.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&talkId=$talkId&theme=$theme#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt", data: { title: await talk.getName(), auditoriumId: await talk.getAuditoriumId(), @@ -83,7 +80,7 @@ export class LiveWidget { }; } - public static async hybridForRoom(roomId: string, client: MatrixClient): Promise> { + public static async hybridForRoom(roomId: string, client: MatrixClient, avatar: string, url: string): Promise> { const widgetId = sha256(JSON.stringify({roomId, kind: "hybrid"})); return { type: "im.vector.modular.widgets", @@ -94,8 +91,8 @@ export class LiveWidget { type: "m.custom", waitForIframeLoad: true, name: "Livestream / Q&A", - avatar_url: config.livestream.widgetAvatar, - url: config.webserver.publicBaseUrl + "/widgets/hybrid.html?widgetId=$matrix_widget_id&roomId=$matrix_room_id&theme=$theme#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt", + avatar_url: avatar, + url: url + "/widgets/hybrid.html?widgetId=$matrix_widget_id&roomId=$matrix_room_id&theme=$theme#displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&roomId=$matrix_room_id&auth=openidtoken-jwt", data: { title: "Join the conference to ask questions", }, @@ -103,17 +100,17 @@ export class LiveWidget { }; } - public static async scoreboardForTalk(talk: Talk, client: MatrixClient): Promise> { - const aud = config.RUNTIME.conference.getAuditorium(await talk.getAuditoriumId()); + public static async scoreboardForTalk(talk: Talk, client: MatrixClient, conference: Conference, avatar: string, url: string): Promise> { + const aud = conference.getAuditorium(await talk.getAuditoriumId()); if (aud === undefined) { throw new Error(`No auditorium ${await talk.getAuditoriumId()} for talk ${await talk.getId()}`); } - return this.scoreboardForAuditorium(aud, client, talk); + return this.scoreboardForAuditorium(aud, client, avatar, url, talk); } - public static async scoreboardForAuditorium(aud: Auditorium, client: MatrixClient, talk?: Talk): Promise> { + public static async scoreboardForAuditorium(aud: Auditorium, client: MatrixClient, avatar: string, url: string, talk?: Talk): Promise> { // note: this is a little bit awkward, but there's nothing special about the widget ID, it just needs to be unique - const widgetId = sha256(JSON.stringify([await aud.getId(), talk ? await talk.getId() : ""]) + "_SCOREBOARD"); + const widgetId = sha256(JSON.stringify([await aud.getId(), (await talk?.getId()) ?? '' ]) + "_SCOREBOARD"); const title = `Messages from ${await aud.getCanonicalAlias()}`; return { type: "im.vector.modular.widgets", @@ -124,8 +121,8 @@ export class LiveWidget { type: "m.custom", waitForIframeLoad: true, name: "Upvoted messages", - avatar_url: config.livestream.widgetAvatar, - url: config.webserver.publicBaseUrl + "/widgets/scoreboard.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&talkId=$talkId&theme=$theme", + avatar_url: avatar, + url: url + "/widgets/scoreboard.html?widgetId=$matrix_widget_id&auditoriumId=$auditoriumId&talkId=$talkId&theme=$theme", data: { title: title, auditoriumId: await aud.getId(), @@ -135,9 +132,9 @@ export class LiveWidget { }; } - public static async scheduleForAuditorium(aud: Auditorium, client: MatrixClient): Promise> { + public static async scheduleForAuditorium(aud: Auditorium, client: MatrixClient, avatar: string, scheduleUrl: string): Promise> { const widgetId = sha256(JSON.stringify(await aud.getDefinition()) + "_AUDSCHED"); - const widgetUrl = template(config.livestream.scheduleUrl, { + const widgetUrl = template(scheduleUrl, { audId: await aud.getId(), }); return { @@ -149,7 +146,7 @@ export class LiveWidget { type: "m.custom", waitForIframeLoad: true, name: "Schedule", - avatar_url: config.livestream.widgetAvatar, + avatar_url: avatar, url: widgetUrl, data: { title: "Conference Schedule", diff --git a/src/models/room_kinds.ts b/src/models/room_kinds.ts index f1a28993..4914dd04 100644 --- a/src/models/room_kinds.ts +++ b/src/models/room_kinds.ts @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; +import { RoomCreateOptions } from "matrix-bot-sdk"; + export const KickPowerLevel = 50; -export const PUBLIC_ROOM_POWER_LEVELS_TEMPLATE = { +export const PUBLIC_ROOM_POWER_LEVELS_TEMPLATE = (moderatorUserId: string) => ({ ban: 50, events_default: 0, invite: 50, @@ -40,10 +41,10 @@ export const PUBLIC_ROOM_POWER_LEVELS_TEMPLATE = { "m.space.child": 100, }, users: { - [config.moderatorUserId]: 100, + [moderatorUserId]: 100, // should be populated with the creator }, -}; +}); export const PRIVATE_ROOM_POWER_LEVELS_TEMPLATE = { ...PUBLIC_ROOM_POWER_LEVELS_TEMPLATE, @@ -78,7 +79,7 @@ export const RSC_AUDITORIUM_ID = "auditoriumId"; export const RSC_TALK_ID = "talkId"; export const RSC_SPECIAL_INTEREST_ID = "interestId"; -export const CONFERENCE_ROOM_CREATION_TEMPLATE = { +export const CONFERENCE_ROOM_CREATION_TEMPLATE: RoomCreateOptions = { preset: 'private_chat', visibility: 'private', initial_state: [ @@ -90,7 +91,7 @@ export const CONFERENCE_ROOM_CREATION_TEMPLATE = { }, }; -export const AUDITORIUM_CREATION_TEMPLATE = { +export const AUDITORIUM_CREATION_TEMPLATE = (moderatorUserId: string) => ({ preset: 'public_chat', visibility: 'public', initial_state: [ @@ -100,11 +101,11 @@ export const AUDITORIUM_CREATION_TEMPLATE = { creation_content: { [RSC_ROOM_KIND_FLAG]: RoomKind.Auditorium, }, - power_level_content_override: PUBLIC_ROOM_POWER_LEVELS_TEMPLATE, - invite: [config.moderatorUserId], -}; + power_level_content_override: PUBLIC_ROOM_POWER_LEVELS_TEMPLATE(moderatorUserId), + invite: [moderatorUserId], +} satisfies RoomCreateOptions); -export const AUDITORIUM_BACKSTAGE_CREATION_TEMPLATE = { +export const AUDITORIUM_BACKSTAGE_CREATION_TEMPLATE: RoomCreateOptions = { preset: 'private_chat', visibility: 'private', initial_state: [ @@ -117,7 +118,7 @@ export const AUDITORIUM_BACKSTAGE_CREATION_TEMPLATE = { power_level_content_override: PRIVATE_ROOM_POWER_LEVELS_TEMPLATE, }; -export const TALK_CREATION_TEMPLATE = { // before being opened up to the public +export const TALK_CREATION_TEMPLATE = (moderatorUserId: string) => ({ // before being opened up to the public preset: 'private_chat', visibility: 'private', initial_state: [ @@ -127,11 +128,11 @@ export const TALK_CREATION_TEMPLATE = { // before being opened up to the public creation_content: { [RSC_ROOM_KIND_FLAG]: RoomKind.Talk, }, - power_level_content_override: PUBLIC_ROOM_POWER_LEVELS_TEMPLATE, - invite: [config.moderatorUserId], -} + power_level_content_override: PUBLIC_ROOM_POWER_LEVELS_TEMPLATE(moderatorUserId), + invite: [moderatorUserId], +} satisfies RoomCreateOptions); -export const SPECIAL_INTEREST_CREATION_TEMPLATE = { +export const SPECIAL_INTEREST_CREATION_TEMPLATE = (moderatorUserId: string) => ({ preset: 'public_chat', visibility: 'public', initial_state: [ @@ -148,16 +149,16 @@ export const SPECIAL_INTEREST_CREATION_TEMPLATE = { "m.room.power_levels": 50, }, }, - invite: [config.moderatorUserId], -}; + invite: [moderatorUserId], +} satisfies RoomCreateOptions); -export function mergeWithCreationTemplate(template: any, addlProps: any): any { +export function mergeWithCreationTemplate(template: RoomCreateOptions, addlProps: any): any { const result = {...template}; - template.initial_state = template.initial_state.slice(); // clone to prevent mutation by accident + template.initial_state = template.initial_state?.slice(); // clone to prevent mutation by accident for (const prop of Object.keys(addlProps)) { switch (prop) { case 'initial_state': - result.initial_state.push(...addlProps.initial_state); + result.initial_state?.push(...addlProps.initial_state); break; case 'creation_content': result.creation_content = {...result.creation_content, ...addlProps.creation_content}; diff --git a/src/utils.ts b/src/utils.ts index 9fee11ce..55c9143f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,10 +28,10 @@ import { import { logMessage } from "./LogProxy"; import * as htmlEscape from "escape-html"; import * as crypto from "crypto"; -import config from "./config"; import { readFile, writeFile, rename } from "fs"; +import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; -export async function replaceRoomIdsWithPills(client: MatrixClient, text: string, roomIds: string[] | string, msgtype: MessageType = "m.text"): Promise { +export async function replaceRoomIdsWithPills(client: ConferenceMatrixClient, text: string, roomIds: string[] | string, msgtype: MessageType = "m.text"): Promise { if (!Array.isArray(roomIds)) roomIds = [roomIds]; const content: TextualMessageEventContent = { @@ -52,7 +52,7 @@ export async function replaceRoomIdsWithPills(client: MatrixClient, text: string alias = (await client.getPublishedAlias(roomId)) || roomId; } catch (e) { // This is a recursive call, so tell the function not to try and call us - await logMessage(LogLevel.WARN, "utils", `Failed to resolve room alias for ${roomId} - see console for details`, null, true); + await logMessage(LogLevel.WARN, "utils", `Failed to resolve room alias for ${roomId} - see console for details`, client, null, true); LogService.warn("utils", e); } const regexRoomId = new RegExp(escapeRegex(roomId), "g"); @@ -188,19 +188,6 @@ export function applySuffixRules( return str; } -/** - * Formats the display name for a room according to the config. - * @param name The base name of the room. - * @param identifier The identifier of the room. - * @returns The formatted display name for the room. - */ -export function makeDisplayName(name: string, identifier: string): string { - return applySuffixRules( - name, identifier, config.conference.prefixes.displayNameSuffixes - ); -} - - /** * Reads a JSON file from disk. */ diff --git a/src/utils/aliases.ts b/src/utils/aliases.ts index b9e0988c..c2c96b0d 100644 --- a/src/utils/aliases.ts +++ b/src/utils/aliases.ts @@ -16,9 +16,10 @@ limitations under the License. import { LogLevel, LogService, MatrixClient, UserID } from "matrix-bot-sdk"; import { logMessage } from "../LogProxy"; -import config from "../config"; import { applySuffixRules } from "../utils"; import { setDifference } from "./sets"; +import { IConfig } from "../config"; +import { ConferenceMatrixClient } from "../ConferenceMatrixClient"; export interface ICanonicalAliasContent { alias: string | null; @@ -35,7 +36,7 @@ export async function getCanonicalAliasInfo(client: MatrixClient, roomId: string } } -export async function safeAssignAlias(client: MatrixClient, roomId: string, localpart: string): Promise { +export async function safeAssignAlias(client: ConferenceMatrixClient, roomId: string, localpart: string): Promise { try { // yes, we reuse the variable despite the contents changing. This is to make sure that the log message // gives a sense of what request failed. @@ -53,22 +54,22 @@ export async function safeAssignAlias(client: MatrixClient, roomId: string, loca aliasInfo.alt_aliases.push(localpart); await client.sendStateEvent(roomId, "m.room.canonical_alias", "", aliasInfo); } catch (e) { - await logMessage(LogLevel.WARN, "utils/alias", `Non-fatal error trying to assign '${localpart}' as an alias to ${roomId} - ${e.message}`); + await logMessage(LogLevel.WARN, "utils/alias", `Non-fatal error trying to assign '${localpart}' as an alias to ${roomId} - ${e.message}`, client) } } -export function makeLocalpart(localpart: string, identifier?: string): string { +export function makeLocalpart(localpart: string, suffixes: IConfig["conference"]["prefixes"]["suffixes"], identifier?: string): string { if (!identifier) { return localpart; } - return applySuffixRules(localpart, identifier, config.conference.prefixes.suffixes); + return applySuffixRules(localpart, identifier, suffixes); } -export async function assignAliasVariations(client: MatrixClient, roomId: string, origLocalparts: string[], identifier?: string): Promise { +export async function assignAliasVariations(client: ConferenceMatrixClient, roomId: string, origLocalparts: string[], suffixes: IConfig["conference"]["prefixes"]["suffixes"], identifier?: string): Promise { const localparts = new Set(); for (const origLocalpart of origLocalparts) { - for (const localpart of calculateAliasVariations(origLocalpart, identifier)) { + for (const localpart of calculateAliasVariations(origLocalpart, suffixes, identifier)) { localparts.add(localpart); } } @@ -89,8 +90,8 @@ export async function assignAliasVariations(client: MatrixClient, roomId: string * @param identifier optionally, an identifier for evaluating suffix rules; see `applySuffixRules`. * @returns set of variations */ -export function calculateAliasVariations(localpart: string, identifier?: string): Set { - localpart = makeLocalpart(localpart, identifier); +export function calculateAliasVariations(localpart: string, suffixes: IConfig["conference"]["prefixes"]["suffixes"], identifier?: string): Set { + localpart = makeLocalpart(localpart, suffixes, identifier); return new Set([localpart, localpart.toLowerCase(), localpart.toUpperCase()]); } @@ -133,7 +134,7 @@ async function listManagedAliasLocalpartsInRoom(client: MatrixClient, roomId: st return presentLocalparts; } -export async function addAndDeleteManagedAliases(client: MatrixClient, roomId: string, desiredLocalparts: Set): Promise { +export async function addAndDeleteManagedAliases(client: ConferenceMatrixClient, roomId: string, desiredLocalparts: Set): Promise { const presentLocalparts: Set = await listManagedAliasLocalpartsInRoom(client, roomId); const localpartsToBeAdded = setDifference(desiredLocalparts, presentLocalparts); diff --git a/src/web.ts b/src/web.ts index a4f5a2db..3beed70d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -16,26 +16,27 @@ limitations under the License. import { Response, Request } from "express"; import * as template from "string-template"; -import config from "./config"; import { base32 } from "rfc4648"; -import { LogService } from "matrix-bot-sdk"; +import { LogService, MatrixClient } from "matrix-bot-sdk"; import { sha256 } from "./utils"; import * as dns from "dns"; import { Scoreboard } from "./Scoreboard"; import { LiveWidget } from "./models/LiveWidget"; import { IDbTalk } from "./backends/penta/db/DbTalk"; +import { Conference } from "./Conference"; +import { IConfig } from "./config"; -export function renderAuditoriumWidget(req: Request, res: Response) { +export function renderAuditoriumWidget(req: Request, res: Response, conference: Conference, auditoriumUrl: string) { const audId = req.query?.['auditoriumId'] as string; if (!audId || Array.isArray(audId)) { return res.sendStatus(404); } - if (!config.RUNTIME.conference.getAuditorium(audId)) { + if (!conference.getAuditorium(audId)) { return res.sendStatus(404); } - const streamUrl = template(config.livestream.auditoriumUrl, { + const streamUrl = template(auditoriumUrl, { id: audId.toLowerCase(), sId: audId.toLowerCase().replace(/[^a-z0-9]/g, ''), }); @@ -60,11 +61,11 @@ const dbTalksCache: { * @param talkId The talk ID. * @returns The database record for the talk, if it exists; `null` otherwise. */ -async function getDbTalk(talkId: string): Promise { +async function getDbTalk(talkId: string, conference: Conference): Promise { const now = Date.now(); if (!(talkId in dbTalksCache) || now - dbTalksCache[talkId].cachedAt > TALK_CACHE_DURATION) { - const db = await config.RUNTIME.conference.getPentaDb(); + const db = await conference.getPentaDb(); if (db === null) return null; dbTalksCache[talkId] = { @@ -76,7 +77,7 @@ async function getDbTalk(talkId: string): Promise { return dbTalksCache[talkId].talk; } -export async function renderTalkWidget(req: Request, res: Response) { +export async function renderTalkWidget(req: Request, res: Response, conference: Conference, talkUrl: string, jitsiDomain: string) { const audId = req.query?.['auditoriumId'] as string; if (!audId || Array.isArray(audId)) { return res.sendStatus(404); @@ -86,12 +87,12 @@ export async function renderTalkWidget(req: Request, res: Response) { return res.sendStatus(404); } - const aud = config.RUNTIME.conference.getAuditorium(audId); + const aud = conference.getAuditorium(audId); if (!aud) { return res.sendStatus(404); } - const talk = config.RUNTIME.conference.getTalk(talkId); + const talk = conference.getTalk(talkId); if (!talk) { return res.sendStatus(404); } @@ -102,9 +103,9 @@ export async function renderTalkWidget(req: Request, res: Response) { // Fetch the corresponding talk from Pentabarf. We cache the `IDbTalk` to avoid hitting the // Pentabarf database for every visiting attendee once talk rooms are opened to the public. - const dbTalk = await getDbTalk(talkId); + const dbTalk = await getDbTalk(talkId, conference); - const streamUrl = template(config.livestream.talkUrl, { + const streamUrl = template(talkUrl, { audId: audId.toLowerCase(), slug: (await talk.getDefinition()).slug.toLowerCase(), jitsi: base32.stringify(Buffer.from(talk.roomId), { pad: false }).toLowerCase(), @@ -114,20 +115,20 @@ export async function renderTalkWidget(req: Request, res: Response) { theme: req.query?.['theme'] === 'dark' ? 'dark' : 'light', videoUrl: streamUrl, roomName: await talk.getName(), - conferenceDomain: config.livestream.jitsiDomain, + conferenceDomain: jitsiDomain, conferenceId: base32.stringify(Buffer.from(talk.roomId), { pad: false }).toLowerCase(), livestreamStartTime: dbTalk?.livestream_start_datetime ?? "", livestreamEndTime: dbTalk?.livestream_end_datetime ?? "", }); } -export async function renderHybridWidget(req: Request, res: Response) { +export async function renderHybridWidget(req: Request, res: Response, hybridUrl: string, jitsiDomain: string) { const roomId = req.query?.['roomId'] as string; if (!roomId || Array.isArray(roomId)) { return res.sendStatus(404); } - const streamUrl = template(config.livestream.hybridUrl, { + const streamUrl = template(hybridUrl, { jitsi: base32.stringify(Buffer.from(roomId), { pad: false }).toLowerCase(), }); @@ -135,18 +136,18 @@ export async function renderHybridWidget(req: Request, res: Response) { theme: req.query?.['theme'] === 'dark' ? 'dark' : 'light', videoUrl: streamUrl, roomName: "Livestream / Q&A", - conferenceDomain: config.livestream.jitsiDomain, + conferenceDomain: jitsiDomain, conferenceId: base32.stringify(Buffer.from(roomId), { pad: false }).toLowerCase(), }); } -export async function makeHybridWidget(req: Request, res: Response) { +export async function makeHybridWidget(req: Request, res: Response, client: MatrixClient, avatar: string, url: string) { const roomId = req.query?.['roomId'] as string; if (!roomId || Array.isArray(roomId)) { return res.sendStatus(404); } - const widget = await LiveWidget.hybridForRoom(roomId, config.RUNTIME.client); + const widget = await LiveWidget.hybridForRoom(roomId, client, avatar, url); const layout = LiveWidget.layoutForHybrid(widget); res.send({ @@ -156,9 +157,9 @@ export async function makeHybridWidget(req: Request, res: Response) { }); } -export async function rtmpRedirect(req: Request, res: Response) { +export async function rtmpRedirect(req: Request, res: Response, conference: Conference, cfg: IConfig["livestream"]["onpublish"]) { // Check auth (salt must match known salt) - if (req.query?.['auth'] !== config.livestream.onpublish.salt) { + if (req.query?.['auth'] !== cfg.salt) { return res.sendStatus(200); // fake a 'no mapping' response for security } @@ -169,17 +170,17 @@ export async function rtmpRedirect(req: Request, res: Response) { const mxRoomId = Buffer.from(base32.parse(confName, {loose: true})).toString(); // Try to find a talk with that room ID - const talk = config.RUNTIME.conference.storedTalks.find(t => t.roomId === mxRoomId); + const talk = conference.storedTalks.find(t => t.roomId === mxRoomId); if (!talk) return res.sendStatus(200); // Annoying thing of nginx wanting "no mapping" to be 200 OK // Redirect to RTMP URL - const hostname = template(config.livestream.onpublish.rtmpHostnameTemplate, { + const hostname = template(cfg.rtmpHostnameTemplate, { squishedAudId: (await talk.getAuditoriumId()).replace(/[^a-zA-Z0-9]/g, '').toLowerCase(), }); const ip = await dns.promises.resolve(hostname); - const uri = template(config.livestream.onpublish.rtmpUrlTemplate, { + const uri = template(cfg.rtmpUrlTemplate, { hostname: ip, - saltedHash: sha256((await talk.getId()) + '.' + config.livestream.onpublish.salt), + saltedHash: sha256((await talk.getId()) + '.' + cfg.salt), }); return res.redirect(uri); } catch (e) { @@ -192,13 +193,13 @@ export function renderHealthz(req: Request, res: Response) { return res.sendStatus(200); } -export async function renderScoreboardWidget(req: Request, res: Response) { +export async function renderScoreboardWidget(req: Request, res: Response, conference: Conference) { const audId = req.query?.['auditoriumId'] as string; if (!audId || Array.isArray(audId)) { return res.sendStatus(404); } - const aud = config.RUNTIME.conference.getAuditorium(audId); + const aud = conference.getAuditorium(audId); if (!aud) { return res.sendStatus(404); } @@ -214,7 +215,7 @@ export async function renderScoreboardWidget(req: Request, res: Response) { return res.sendStatus(404); } - const talk = config.RUNTIME.conference.getTalk(talkId); + const talk = conference.getTalk(talkId); if (!talk) { return res.sendStatus(404); } @@ -231,11 +232,11 @@ export async function renderScoreboardWidget(req: Request, res: Response) { }); } -export function renderScoreboard(req: Request, res: Response, scoreboard: Scoreboard) { +export function renderScoreboard(req: Request, res: Response, scoreboard: Scoreboard, conference: Conference) { const roomId = req.params['roomId']; if (!roomId) return res.sendStatus(400); - const auditorium = config.RUNTIME.conference.storedAuditoriums.find(a => a.roomId === roomId); + const auditorium = conference.storedAuditoriums.find(a => a.roomId === roomId); if (!auditorium) return res.sendStatus(404); let sb = scoreboard.getScoreboard(auditorium.roomId); diff --git a/yarn.lock b/yarn.lock index 3b2b5dff..c49d4509 100644 --- a/yarn.lock +++ b/yarn.lock @@ -289,7 +289,7 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2": +"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.7": version "7.20.12" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5" integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ== @@ -355,61 +355,61 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.3.1.tgz#3e3f876e4e47616ea3b1464b9fbda981872e9583" - integrity sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg== +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" -"@jest/core@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.3.1.tgz#bff00f413ff0128f4debec1099ba7dcd649774a1" - integrity sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw== +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: - "@jest/console" "^29.3.1" - "@jest/reporters" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" ci-info "^3.2.0" exit "^0.1.2" graceful-fs "^4.2.9" - jest-changed-files "^29.2.0" - jest-config "^29.3.1" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-resolve-dependencies "^29.3.1" - jest-runner "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - jest-watcher "^29.3.1" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" micromatch "^4.0.4" - pretty-format "^29.3.1" + pretty-format "^29.7.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6" - integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag== +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== dependencies: - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.3.1" + jest-mock "^29.7.0" "@jest/expect-utils@^29.3.1": version "29.3.1" @@ -418,47 +418,54 @@ dependencies: jest-get-type "^29.2.0" -"@jest/expect@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.3.1.tgz#456385b62894349c1d196f2d183e3716d4c6a6cd" - integrity sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg== +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== dependencies: - expect "^29.3.1" - jest-snapshot "^29.3.1" + jest-get-type "^29.6.3" -"@jest/fake-timers@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67" - integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A== +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: - "@jest/types" "^29.3.1" - "@sinonjs/fake-timers" "^9.1.2" + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" "@types/node" "*" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" -"@jest/globals@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.3.1.tgz#92be078228e82d629df40c3656d45328f134a0c6" - integrity sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q== +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/types" "^29.3.1" - jest-mock "^29.3.1" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" -"@jest/reporters@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.3.1.tgz#9a6d78c109608e677c25ddb34f907b90e07b4310" - integrity sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA== +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" - "@jridgewell/trace-mapping" "^0.3.15" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" @@ -466,13 +473,13 @@ glob "^7.1.3" graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^5.1.0" + istanbul-lib-instrument "^6.0.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.1.3" - jest-message-util "^29.3.1" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" slash "^3.0.0" string-length "^4.0.1" strip-ansi "^6.0.0" @@ -485,55 +492,62 @@ dependencies: "@sinclair/typebox" "^0.24.1" -"@jest/source-map@^29.2.0": - version "29.2.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.2.0.tgz#ab3420c46d42508dcc3dc1c6deee0b613c235744" - integrity sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ== +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: - "@jridgewell/trace-mapping" "^0.3.15" + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" callsites "^3.0.0" graceful-fs "^4.2.9" -"@jest/test-result@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.3.1.tgz#92cd5099aa94be947560a24610aa76606de78f50" - integrity sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw== +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== dependencies: - "@jest/console" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz#fa24b3b050f7a59d48f7ef9e0b782ab65123090d" - integrity sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA== +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== dependencies: - "@jest/test-result" "^29.3.1" + "@jest/test-result" "^29.7.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.7.0" slash "^3.0.0" -"@jest/transform@^29.3.1": - version "29.3.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.3.1.tgz#1e6bd3da4af50b5c82a539b7b1f3770568d6e36d" - integrity sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug== +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== dependencies: "@babel/core" "^7.11.6" - "@jest/types" "^29.3.1" - "@jridgewell/trace-mapping" "^0.3.15" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" micromatch "^4.0.4" pirates "^4.0.4" slash "^3.0.0" - write-file-atomic "^4.0.1" + write-file-atomic "^4.0.2" "@jest/types@^29.3.1": version "29.3.1" @@ -547,6 +561,18 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -569,6 +595,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -579,7 +610,12 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -587,17 +623,26 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.18": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@matrix-org/matrix-sdk-crypto-nodejs@^0.1.0-beta.3": - version "0.1.0-beta.3" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.3.tgz#a07225dd180d9d227c24ba62bba439939446d113" - integrity sha512-jHFn6xBeNqfsY5gX60akbss7iFBHZwXycJWMw58Mjz08OwOi7AbTxeS9I2Pa4jX9/M2iinskmGZbzpqOT2fM3A== +"@matrix-org/matrix-sdk-crypto-nodejs@0.1.0-beta.11": + version "0.1.0-beta.11" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.11.tgz#537cd7a7bbce1d9745b812a5a7ffa9a5944e146c" + integrity sha512-z5adcQo4o0UAry4zs6JHGxbTDlYTUMKUfpOpigmso65ETBDumbeTSQCWRw8UeUV7aCAyVoHARqDTol9SrauEFA== dependencies: - node-downloader-helper "^2.1.1" + https-proxy-agent "^5.0.1" + node-downloader-helper "^2.1.5" "@selderee/plugin-htmlparser2@^0.6.0": version "0.6.0" @@ -612,19 +657,24 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" integrity sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA== -"@sinonjs/commons@^1.7.0": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" - integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" - integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== dependencies: - "@sinonjs/commons" "^1.7.0" + "@sinonjs/commons" "^3.0.0" "@tsconfig/node18@^1.0.1": version "1.0.1" @@ -837,11 +887,6 @@ "@types/node" "*" "@types/pg-types" "*" -"@types/prettier@^2.1.5": - version "2.7.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" - integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== - "@types/prettier@^2.6.1": version "2.7.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e" @@ -1108,6 +1153,13 @@ acorn@^8.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.4.tgz#7a3ae4191466a6984eee0fe3407a4f3aa9db8354" integrity sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1341,15 +1393,15 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -babel-jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.3.1.tgz#05c83e0d128cd48c453eea851482a38782249f44" - integrity sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA== +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== dependencies: - "@jest/transform" "^29.3.1" + "@jest/transform" "^29.7.0" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.2.0" + babel-preset-jest "^29.6.3" chalk "^4.0.0" graceful-fs "^4.2.9" slash "^3.0.0" @@ -1365,10 +1417,10 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz#23ee99c37390a98cfddf3ef4a78674180d823094" - integrity sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA== +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -1393,12 +1445,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz#3048bea3a1af222e3505e4a767a974c95a7620dc" - integrity sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA== +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== dependencies: - babel-plugin-jest-hoist "^29.2.0" + babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -1974,6 +2026,26 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -2086,6 +2158,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + debug@^3.1.1, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2110,10 +2189,10 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== deep-equal@^1.0.1: version "1.1.1" @@ -2222,6 +2301,11 @@ diff-sequences@^29.3.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" integrity sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + discontinuous-range@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" @@ -2685,7 +2769,7 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^29.0.0, expect@^29.3.1: +expect@^29.0.0: version "29.3.1" resolved "https://registry.yarnpkg.com/expect/-/expect-29.3.1.tgz#92877aad3f7deefc2e3f6430dd195b92295554a6" integrity sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA== @@ -2696,6 +2780,17 @@ expect@^29.0.0, expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3228,6 +3323,13 @@ hls.js@^0.14.17: eventemitter3 "^4.0.3" url-toolkit "^2.1.6" +homerunner-client@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/homerunner-client/-/homerunner-client-0.0.6.tgz#d448ecb437753155c14ea67ad9012505cc9af529" + integrity sha512-1QfA2/skYhHRjb1xTxki3I5buMIm9lkRUDzRU29gxGrDJp5eUgq4apYdc+UEJ27rI/bSbockKGIrwHK8okiy7A== + dependencies: + cross-fetch "^3.1.5" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -3406,6 +3508,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3801,7 +3911,7 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: +istanbul-lib-instrument@^5.0.4: version "5.2.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== @@ -3812,6 +3922,17 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" @@ -3838,82 +3959,83 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.2.0.tgz#b6598daa9803ea6a4dce7968e20ab380ddbee289" - integrity sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA== +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== dependencies: execa "^5.0.0" + jest-util "^29.7.0" p-limit "^3.1.0" -jest-circus@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.3.1.tgz#177d07c5c0beae8ef2937a67de68f1e17bbf1b4a" - integrity sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg== +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== dependencies: - "@jest/environment" "^29.3.1" - "@jest/expect" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - dedent "^0.7.0" + dedent "^1.0.0" is-generator-fn "^2.0.0" - jest-each "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-runtime "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" p-limit "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.7.0" + pure-rand "^6.0.0" slash "^3.0.0" stack-utils "^2.0.3" -jest-cli@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.3.1.tgz#e89dff427db3b1df50cea9a393ebd8640790416d" - integrity sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ== +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: - "@jest/core" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" chalk "^4.0.0" + create-jest "^29.7.0" exit "^0.1.2" - graceful-fs "^4.2.9" import-local "^3.0.2" - jest-config "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" - prompts "^2.0.1" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" yargs "^17.3.1" -jest-config@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.3.1.tgz#0bc3dcb0959ff8662957f1259947aedaefb7f3c6" - integrity sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg== +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== dependencies: "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.3.1" - "@jest/types" "^29.3.1" - babel-jest "^29.3.1" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" chalk "^4.0.0" ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.3" graceful-fs "^4.2.9" - jest-circus "^29.3.1" - jest-environment-node "^29.3.1" - jest-get-type "^29.2.0" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-runner "^29.3.1" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" micromatch "^4.0.4" parse-json "^5.2.0" - pretty-format "^29.3.1" + pretty-format "^29.7.0" slash "^3.0.0" strip-json-comments "^3.1.1" @@ -3927,67 +4049,82 @@ jest-diff@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" -jest-docblock@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.2.0.tgz#307203e20b637d97cee04809efc1d43afc641e82" - integrity sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A== +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: - detect-newline "^3.0.0" + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" -jest-each@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.3.1.tgz#bc375c8734f1bb96625d83d1ca03ef508379e132" - integrity sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA== +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: - "@jest/types" "^29.3.1" - chalk "^4.0.0" - jest-get-type "^29.2.0" - jest-util "^29.3.1" - pretty-format "^29.3.1" + detect-newline "^3.0.0" -jest-environment-node@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.3.1.tgz#5023b32472b3fba91db5c799a0d5624ad4803e74" - integrity sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag== +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" - jest-mock "^29.3.1" - jest-util "^29.3.1" + jest-mock "^29.7.0" + jest-util "^29.7.0" jest-get-type@^29.2.0: version "29.2.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.2.0.tgz#726646f927ef61d583a3b3adb1ab13f3a5036408" integrity sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA== -jest-haste-map@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.3.1.tgz#af83b4347f1dae5ee8c2fb57368dc0bb3e5af843" - integrity sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.6.3" "@types/graceful-fs" "^4.1.3" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.9" - jest-regex-util "^29.2.0" - jest-util "^29.3.1" - jest-worker "^29.3.1" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" micromatch "^4.0.4" walker "^1.0.8" optionalDependencies: fsevents "^2.3.2" -jest-leak-detector@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz#95336d020170671db0ee166b75cd8ef647265518" - integrity sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA== +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== dependencies: - jest-get-type "^29.2.0" - pretty-format "^29.3.1" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" jest-matcher-utils@^29.3.1: version "29.3.1" @@ -3999,6 +4136,16 @@ jest-matcher-utils@^29.3.1: jest-get-type "^29.2.0" pretty-format "^29.3.1" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb" @@ -4014,132 +4161,143 @@ jest-message-util@^29.3.1: slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e" - integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA== +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: - "@jest/types" "^29.3.1" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.7.0" jest-pnp-resolver@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== -jest-regex-util@^29.2.0: - version "29.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.2.0.tgz#82ef3b587e8c303357728d0322d48bbfd2971f7b" - integrity sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== -jest-resolve-dependencies@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz#a6a329708a128e68d67c49f38678a4a4a914c3bf" - integrity sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA== +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== dependencies: - jest-regex-util "^29.2.0" - jest-snapshot "^29.3.1" + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" -jest-resolve@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.3.1.tgz#9a4b6b65387a3141e4a40815535c7f196f1a68a7" - integrity sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw== +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: chalk "^4.0.0" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" + jest-haste-map "^29.7.0" jest-pnp-resolver "^1.2.2" - jest-util "^29.3.1" - jest-validate "^29.3.1" + jest-util "^29.7.0" + jest-validate "^29.7.0" resolve "^1.20.0" - resolve.exports "^1.1.0" + resolve.exports "^2.0.0" slash "^3.0.0" -jest-runner@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.3.1.tgz#a92a879a47dd096fea46bb1517b0a99418ee9e2d" - integrity sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA== +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: - "@jest/console" "^29.3.1" - "@jest/environment" "^29.3.1" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" emittery "^0.13.1" graceful-fs "^4.2.9" - jest-docblock "^29.2.0" - jest-environment-node "^29.3.1" - jest-haste-map "^29.3.1" - jest-leak-detector "^29.3.1" - jest-message-util "^29.3.1" - jest-resolve "^29.3.1" - jest-runtime "^29.3.1" - jest-util "^29.3.1" - jest-watcher "^29.3.1" - jest-worker "^29.3.1" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.3.1.tgz#21efccb1a66911d6d8591276a6182f520b86737a" - integrity sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A== - dependencies: - "@jest/environment" "^29.3.1" - "@jest/fake-timers" "^29.3.1" - "@jest/globals" "^29.3.1" - "@jest/source-map" "^29.2.0" - "@jest/test-result" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" chalk "^4.0.0" cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" glob "^7.1.3" graceful-fs "^4.2.9" - jest-haste-map "^29.3.1" - jest-message-util "^29.3.1" - jest-mock "^29.3.1" - jest-regex-util "^29.2.0" - jest-resolve "^29.3.1" - jest-snapshot "^29.3.1" - jest-util "^29.3.1" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.3.1.tgz#17bcef71a453adc059a18a32ccbd594b8cc4e45e" - integrity sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA== +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== dependencies: "@babel/core" "^7.11.6" "@babel/generator" "^7.7.2" "@babel/plugin-syntax-jsx" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.3.1" - "@jest/transform" "^29.3.1" - "@jest/types" "^29.3.1" - "@types/babel__traverse" "^7.0.6" - "@types/prettier" "^2.1.5" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^29.3.1" + expect "^29.7.0" graceful-fs "^4.2.9" - jest-diff "^29.3.1" - jest-get-type "^29.2.0" - jest-haste-map "^29.3.1" - jest-matcher-utils "^29.3.1" - jest-message-util "^29.3.1" - jest-util "^29.3.1" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" natural-compare "^1.4.0" - pretty-format "^29.3.1" - semver "^7.3.5" + pretty-format "^29.7.0" + semver "^7.5.3" jest-util@^29.0.0, jest-util@^29.3.1: version "29.3.1" @@ -4153,30 +4311,42 @@ jest-util@^29.0.0, jest-util@^29.3.1: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.3.1.tgz#d56fefaa2e7d1fde3ecdc973c7f7f8f25eea704a" - integrity sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g== +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: - "@jest/types" "^29.3.1" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^29.2.0" + jest-get-type "^29.6.3" leven "^3.1.0" - pretty-format "^29.3.1" + pretty-format "^29.7.0" -jest-watcher@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.3.1.tgz#3341547e14fe3c0f79f9c3a4c62dbc3fc977fd4a" - integrity sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg== +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== dependencies: - "@jest/test-result" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" emittery "^0.13.1" - jest-util "^29.3.1" + jest-util "^29.7.0" string-length "^4.0.1" jest-worker@^26.6.1: @@ -4188,25 +4358,25 @@ jest-worker@^26.6.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.3.1.tgz#e9462161017a9bb176380d721cab022661da3d6b" - integrity sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw== +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: "@types/node" "*" - jest-util "^29.3.1" + jest-util "^29.7.0" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.3.1: - version "29.3.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.3.1.tgz#c130c0d551ae6b5459b8963747fed392ddbde122" - integrity sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA== +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: - "@jest/core" "^29.3.1" - "@jest/types" "^29.3.1" + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" import-local "^3.0.2" - jest-cli "^29.3.1" + jest-cli "^29.7.0" js-tokens@^4.0.0: version "4.0.0" @@ -4302,7 +4472,7 @@ json5@^2.1.1, json5@^2.1.2: dependencies: minimist "^1.2.5" -json5@^2.2.1, json5@^2.2.2: +json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -4536,12 +4706,12 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -matrix-bot-sdk@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/matrix-bot-sdk/-/matrix-bot-sdk-0.6.3.tgz#7ed72a314cbf2cf7b952affdb3681ccf7d1dfa58" - integrity sha512-YXVML3VaRWa6KUtx2z2WQ6819e20ssruw7ytNZAB+c2X7GWvZgMV41PoZ+jNFJj5H2OPIuwVxJF5aYBy49Xm6A== +"matrix-bot-sdk@npm:@vector-im/matrix-bot-sdk@^0.6.7-element.1": + version "0.6.7-element.1" + resolved "https://registry.yarnpkg.com/@vector-im/matrix-bot-sdk/-/matrix-bot-sdk-0.6.7-element.1.tgz#f33721ca33f05b181b52287672b09dbbb2f3f852" + integrity sha512-SFaCorDIibhMcJq3AfdacecKALWBEL+sDZVCUDkdA/pgqoWyJiC5c5TcMJT+Gs/vCxCYZzD1qmYssXJ6w7wEYQ== dependencies: - "@matrix-org/matrix-sdk-crypto-nodejs" "^0.1.0-beta.3" + "@matrix-org/matrix-sdk-crypto-nodejs" "0.1.0-beta.11" "@types/express" "^4.17.13" another-json "^0.2.0" async-lock "^1.3.2" @@ -4887,10 +5057,10 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-downloader-helper@^2.1.1: - version "2.1.6" - resolved "https://registry.yarnpkg.com/node-downloader-helper/-/node-downloader-helper-2.1.6.tgz#f73ac458e3ac8c21afd0b952a994eab99c64b879" - integrity sha512-VkOvAXIopI3xMuM/MC5UL7NqqnizQ/9QXZt28jR8FPZ6fHLQm4xe4+YXJ9FqsWwLho5BLXrF51nfOQ0QcohRkQ== +node-downloader-helper@^2.1.5: + version "2.1.9" + resolved "https://registry.yarnpkg.com/node-downloader-helper/-/node-downloader-helper-2.1.9.tgz#a59ee7276b2bf708bbac2cc5872ad28fc7cd1b0e" + integrity sha512-FSvAol2Z8UP191sZtsUZwHIN0eGoGue3uEXGdWIH5228e9KH1YHXT7fN8Oa33UGf+FbqGTQg3sJfrRGzmVCaJA== node-fetch@^2.6.1: version "2.6.9" @@ -4899,6 +5069,13 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -5820,6 +5997,15 @@ pretty-format@^29.0.0, pretty-format@^29.3.1: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -5877,6 +6063,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pure-rand@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" + integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== + qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -6131,10 +6322,10 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== resolve@^1.20.0: version "1.22.1" @@ -6246,13 +6437,6 @@ selfsigned@^1.10.8: dependencies: node-forge "^0.10.0" -semver@7.x, semver@^7.3.5: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -6270,6 +6454,13 @@ semver@^7.3.2, semver@^7.3.4: dependencies: lru-cache "^6.0.0" +semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -6915,18 +7106,18 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-jest@^29.0.3: - version "29.0.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.3.tgz#63ea93c5401ab73595440733cefdba31fcf9cb77" - integrity sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ== +ts-jest@^29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" jest-util "^29.0.0" - json5 "^2.2.1" + json5 "^2.2.3" lodash.memoize "4.x" make-error "1.x" - semver "7.x" + semver "^7.5.3" yargs-parser "^21.0.1" ts-loader@^9.4.2: @@ -7342,7 +7533,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^4.0.1: +write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==