diff --git a/config/default.yaml b/config/default.yaml index d46e24c5..c491a1ba 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -175,88 +175,6 @@ conference: # Should never be less than 5 minutes. lookaheadMinutes: 5 - # Connection information to get data about the conference during the conference. - # This can be readonly. Currently the bot only supports postgresql. - # Should be specified as null if not using Pentabarf. - database: null - #database: - #host: "localhost" - #port: 5432 - #username: "db_username" - #password: "db_password" - #database: "your_db" - #sslmode: "require" - - ## The table/view for the bot to access for information about people involved in the conference. - ## Must at least have the following columns: - ## event_id - TEXT(like) - The penta ID for the talk (event) associated with - ## the person, if relevant. Null if a coordinator. - ## person_id - TEXT(like) - The penta ID for the person. - ## event_role - TEXT(like) - One of [speaker, coordinator, host]. See roles later on. - ## name - TEXT(like) - The full name, or otherwise useful name, for the person. - ## email - TEXT(like) - The preferred email address for the person. - ## matrix_id - TEXT(like) - If known, the Matrix User ID for the person, otherwise null. - ## conference_room - TEXT(like) - The relevant room. This should match the event_id or be - ## the room where coordinators are assigned. - ## - ## People may be assigned multiple roles with multiple rows. - ## - ## Roles: - ## speaker - A speaker for event_id. This person will get moderator in their talk's - ## room, and invited to the auditorium backstage room. They will be required - ## to check in before their talk starts. - ## - ## host - Someone who is moderating the talk itself in collaboration with the speakers - ## for the event_id. One host must check in before the talk starts, otherwise - ## the issue will be raised to the management room and coordinators will be - ## asked to take over. Hosts will get moderator in their talk's room, and be - ## invited to the public auditorium room, backstage room, and talk room. - ## - ## coordinator - These are typically people responsible for scheduling the auditorium's - ## talks. They'll get moderator in the auditorium room, backstage room, and all - ## talk rooms for their auditorium, though will only be invited to the auditorium - ## and backstage room (and the talk rooms on-demand if needed to fill in for a - ## missing host). - ## - ## The bot will ignore unknown events, rooms, and roles. - ## - ## CAUTION: Although the bot uses parameterized queries, it is unable to use the table name as - ## a parameter. As such, this particular config value is vulnerable to SQL injection. Seeing as - ## how you (the bot's admin) are the one entering it: don't do that to yourself. - #tblPeople: "view_matrix_bot_export_people" - - ## The table/view for the bot to access for information about the conference schedule. - ## Must at least have the following columns: - ## event_id - TEXT(like) - The penta ID for the talk (event). - ## conference_room - TEXT(like) - The relevant room. This should match the event_id. - ## start_datetime - TIMESTAMP WITHOUT TIME ZONE - The start time of the talk. The timezone - ## is determined by the bot using the timezone elsewhere in this config. - ## duration - INTERVAL - How long the talk is meant to last (start to end). - ## presentation_length - INTERVAL - The portion of `duration` where the speaker is presenting. - ## The remainder is assumed to be Q&A. - ## prerecorded - BOOLEAN - True if the presentation is prerecorded. When false, the - ## bot will assume that presentation_length is zero and switch - ## directly into "hallway" mode at start_datetime. - ## - ## The bot will ignore unknown talks. Any if the start_datetime is NULL then the bot will ignore - ## that record. If the INTERVAL fields are NULL, the bot will assume zero. - ## - ## Note: the bot is capable of running both tblSchedule and tblPeople off the same table/view. - ## It will use DISTINCT where it needs to in order to get accurate information. - ## - ## CAUTION: Although the bot uses parameterized queries, it is unable to use the table name as - ## a parameter. As such, this particular config value is vulnerable to SQL injection. Seeing as - ## how you (the bot's admin) are the one entering it: don't do that to yourself. - #tblSchedule: "view_matrix_bot_export_schedule" - - ## The duration in seconds added to the `presentation_length` to account for preroll material - ## such as sponsor segments. If this is built into the table already then set this to zero. - #schedulePreBufferSeconds: 30 - - ## The duration in seconds added to the talk `duration` to account for postroll material - ## such as sponsor segments. If this is built into the table already then set this to zero. - #schedulePostBufferSeconds: 30 - # Various prefixes used by the bot when parsing information. prefixes: # The prefixes for the rooms listed in the pentabarf definition which diff --git a/package.json b/package.json index 8e5a0e2e..56d7176c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "matrix-widget-api": "^1.6.0", "moment": "^2.29.4", "node-fetch": "^2.6.1", - "pg": "^8.9.0", "postcss-preset-env": "^6.7.0", "prom-client": "^15.0.0", "qs": "^6.11.2", @@ -47,7 +46,6 @@ "devDependencies": { "@types/jest": "^29.2.5", "@types/node": "^18", - "@types/pg": "^7.14.7", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.8.1", "homerunner-client": "^0.0.6", diff --git a/spec/util/e2e-test.ts b/spec/util/e2e-test.ts index b5b37459..fe2ab877 100644 --- a/spec/util/e2e-test.ts +++ b/spec/util/e2e-test.ts @@ -192,7 +192,6 @@ export class E2ETestEnv { }, schedule: { backend: 'json', - database: undefined, scheduleDefinition, }, subspaces: { diff --git a/src/Conference.ts b/src/Conference.ts index 5d5db102..385602be 100644 --- a/src/Conference.ts +++ b/src/Conference.ts @@ -50,13 +50,11 @@ import { MatrixRoom } from "./models/MatrixRoom"; import { Auditorium, AuditoriumBackstage } from "./models/Auditorium"; import { Talk } from "./models/Talk"; import { ResolvedPersonIdentifier, resolveIdentifiers } from "./invites"; -import { PentaDb } from "./backends/penta/db/PentaDb"; import { PermissionsCommand } from "./commands/PermissionsCommand"; import { InterestRoom } from "./models/InterestRoom"; import { IStateEvent } from "./models/room_state"; import { logMessage } from "./LogProxy"; import { IScheduleBackend } from "./backends/IScheduleBackend"; -import { PentaBackend } from "./backends/penta/PentaBackend"; import { setUnion } from "./utils/sets"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; import { Gauge } from "prom-client"; @@ -66,8 +64,6 @@ const attendeeTotalGauge = new Gauge({ name: "confbot_attendee_total", help: "Th export class Conference { private rootSpace: Space | null; private dbRoom: MatrixRoom | null; - // TODO This shouldn't be here. - private pentaDb: PentaDb | null = null; private subspaces: { [subspaceId: string]: Space } = {}; @@ -206,11 +202,6 @@ export class Conference { public async construct() { this.reset(); - if (this.backend instanceof PentaBackend) { - // TODO this is not nice. - this.pentaDb = this.backend.db; - } - // Locate all the rooms for the conference const roomIds = await this.client.getJoinedRooms(); const batchSize = 20; @@ -357,12 +348,6 @@ export class Conference { } } - public async getPentaDb(): Promise { - if (this.pentaDb === null) return null; - await this.pentaDb.connect(); - return this.pentaDb; - } - public async getSpace(): Promise { return this.rootSpace; } @@ -728,22 +713,17 @@ export class Conference { return people; } + /** + * @deprecated Just use `.getSpeakers()` + */ public async getPeopleForTalk(talk: Talk): Promise { - const db = await this.getPentaDb(); - if (db !== null) { - return await this.resolvePeople(await db.findAllPeopleForTalk(await talk.getId())); - } - return talk.getSpeakers(); } + /** + * @deprecated This always returns `[]`. + */ public async getPeopleForInterest(int: InterestRoom): Promise { - const db = await this.getPentaDb(); - if (db !== null) { - // Yes, an interest room is an auditorium to Penta. - return await this.resolvePeople(await db.findAllPeopleForAuditorium(await int.getId())); - } - return []; } @@ -889,13 +869,10 @@ export class Conference { return this.getUpcomingTalksByLambda(talk => talk.endTime, inNextMinutes, minBefore); } + /** + * @deprecated This always returns `[]` and should be removed or fixed. + */ public async findPeopleWithId(personId: string): Promise { - // TODO - const db = await this.getPentaDb(); - if (db !== null) { - return await db.findPeopleWithId(personId); - } - return []; } diff --git a/src/__tests__/backends/penta/CachingBackend.test.ts b/src/__tests__/backends/penta/CachingBackend.test.ts deleted file mode 100644 index b97751d8..00000000 --- a/src/__tests__/backends/penta/CachingBackend.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { PentaDb } from "../../../backends/penta/db/PentaDb"; -import { test, expect, jest } from "@jest/globals"; -import { PentabarfParser } from "../../../backends/penta/PentabarfParser"; -import { IPrefixConfig } from "../../../config"; -import { PentaBackend } from "../../../backends/penta/PentaBackend"; -import { Role } from "../../../models/schedule"; -import { CachingBackend } from "../../../backends/CachingBackend"; - -import * as utils from "../../../utils"; - -const fs = require("fs"); -const path = require("path"); - -const prefixConfig: IPrefixConfig = { - // Unused here. - aliases: "", displayNameSuffixes: {}, suffixes: {}, physicalAuditoriumRooms: [], - - auditoriumRooms: [ - "A.", - "AQ.", - ], - qaAuditoriumRooms: [ - "AQ.", - ], - interestRooms: [ - "X." - ], - nameOverrides: { - "A.special": "special-room", - }, -}; - -const actualUtils = jest.requireActual("../../../utils") as any; -jest.mock("../../../utils"); - -test("the cache should restore the same talks that were saved", async () => { - const xml = fs.readFileSync(path.join(__dirname, "pentabarf02_withqa.xml"), 'utf8'); - const parser = new PentabarfParser(xml, prefixConfig); - - const fakeDb = { - connect: jest.fn(PentaDb.prototype.connect).mockResolvedValue(), - getTalk: jest.fn(PentaDb.prototype.getTalk).mockResolvedValue({ - event_id: "AAA", - duration_seconds: 3600, - end_datetime: 3600, - livestream_end_datetime: 3560, - livestream_start_datetime: 5, - prerecorded: true, - presentation_length_seconds: 300, - qa_start_datetime: 305, - start_datetime: 350, - conference_room: "abc", - }), - findPeopleWithId: jest.fn(PentaDb.prototype.findPeopleWithId).mockResolvedValue([ - { - id: "AAA", - matrix_id: "@someone:example.org", - email: "someone@example.org", - name: "Someone Someoneus", - role: Role.Speaker - } - ]), - findAllPeopleForTalk: jest.fn(PentaDb.prototype.findAllPeopleForTalk).mockResolvedValue([]), - } as any as PentaDb; - - async function newPentaBackend(): Promise { - const b = new PentaBackend(parser, fakeDb); - await b.init(); - return b; - } - // A pretend backend which is broken: trying to open it just leads to an error. - async function newBrokenBackend(): Promise { - throw "this backend is broken"; - } - - const readJsonFileAsync = utils.readJsonFileAsync as jest.MockedFunction; - const writeJsonFileAsync = utils.writeJsonFileAsync as jest.MockedFunction; - const fsRename = fs.rename as jest.MockedFunction; - // We don't want this function to be mocked, so return it to its original implementation: - (utils.jsonReplacerMapToObject as jest.MockedFunction).mockImplementation(actualUtils.jsonReplacerMapToObject); - - - // Open the backend, this time with a working PentaBackend - expect(writeJsonFileAsync).toHaveBeenCalledTimes(0); - const cachePath = "/tmp/cachebackend_should_not_exist.json"; - const cache1 = await CachingBackend.new(newPentaBackend, cachePath); - expect(writeJsonFileAsync).toHaveBeenCalledTimes(1); - expect(writeJsonFileAsync.mock.calls[0][0]).toEqual(cachePath); - const writtenContent = writeJsonFileAsync.mock.calls[0][1]; - const replacerFunc = writeJsonFileAsync.mock.calls[0][2]; - // Encode and then decode the content through JSON and then return it to the reader - readJsonFileAsync.mockResolvedValue(JSON.parse(JSON.stringify(writtenContent, replacerFunc))); - - // Open the backend again, but this time the underlying backend can't be created, - // so we should fall back to the cache - expect(readJsonFileAsync).toHaveBeenCalledTimes(0); - const cache2 = await CachingBackend.new(newBrokenBackend, cachePath); - expect(readJsonFileAsync).toHaveBeenCalledTimes(1); - expect(readJsonFileAsync.mock.calls[0][0]).toEqual(cachePath); - - // Expect the live and cache schedule to be exactly the same - expect(cache1.conference).toStrictEqual(cache2.conference); - expect(cache1.auditoriums).toStrictEqual(cache2.auditoriums); - expect(cache1.interestRooms).toStrictEqual(cache2.interestRooms); - expect(cache1.talks).toStrictEqual(cache2.talks); -}); \ No newline at end of file diff --git a/src/__tests__/backends/penta/PentaBackend.test.ts b/src/__tests__/backends/penta/PentaBackend.test.ts deleted file mode 100644 index 709c9205..00000000 --- a/src/__tests__/backends/penta/PentaBackend.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PentaDb } from "../../../backends/penta/db/PentaDb"; -import { test, expect, jest } from "@jest/globals"; -import { PentabarfParser } from "../../../backends/penta/PentabarfParser"; -import { IPrefixConfig } from "../../../config"; -import { PentaBackend } from "../../../backends/penta/PentaBackend"; -import { Role } from "../../../models/schedule"; - -const fs = require("fs"); -const path = require("path"); - -const prefixConfig: IPrefixConfig = { - // Unused here. - aliases: "", displayNameSuffixes: {}, suffixes: {}, physicalAuditoriumRooms: [], - - auditoriumRooms: [ - "A.", - "AQ.", - ], - qaAuditoriumRooms: [ - "AQ.", - ], - interestRooms: [ - "X." - ], - nameOverrides: { - "A.special": "special-room", - }, -}; - -jest.mock('../../../backends/penta/db/PentaDb'); - -test("talks should be rehydrated from the database", async () => { - const xml = fs.readFileSync(path.join(__dirname, "pentabarf02_withqa.xml"), 'utf8'); - const parser = new PentabarfParser(xml, prefixConfig); - - const fakeDb = { - connect: jest.fn(PentaDb.prototype.connect).mockResolvedValue(), - getTalk: jest.fn(PentaDb.prototype.getTalk).mockResolvedValue({ - event_id: "AAA", - duration_seconds: 3600, - end_datetime: 3600, - livestream_end_datetime: 3560, - livestream_start_datetime: 5, - prerecorded: true, - presentation_length_seconds: 300, - qa_start_datetime: 305, - start_datetime: 350, - conference_room: "abc", - }), - findPeopleWithId: jest.fn(PentaDb.prototype.findPeopleWithId).mockResolvedValue([ - { - id: "AAA", - matrix_id: "@someone:example.org", - email: "someone@example.org", - name: "Someone Someoneus", - role: Role.Speaker - } - ]), - findAllPeopleForTalk: jest.fn(PentaDb.prototype.findAllPeopleForTalk).mockResolvedValue([]), - } as any as PentaDb; - - const b = new PentaBackend(parser, fakeDb); - await b.init(); - - const talk = b.talks.get("E002")!; - expect(talk).toBeDefined(); - - expect(talk.qa_startTime).toEqual(305); - expect(talk.livestream_endTime).toEqual(3560); - expect(talk.speakers[0].email).toEqual("someone@example.org"); - expect(talk.speakers[0].matrix_id).toEqual("@someone:example.org"); -}); \ No newline at end of file diff --git a/src/__tests__/backends/penta/PentabarfParser.test.ts b/src/__tests__/backends/penta/PentabarfParser.test.ts deleted file mode 100644 index 3683cbfd..00000000 --- a/src/__tests__/backends/penta/PentabarfParser.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { PentabarfParser } from "../../../backends/penta/PentabarfParser"; -import { test, expect } from "@jest/globals"; -import { IPrefixConfig } from "../../../config"; - -const fs = require("fs"); -const path = require("path"); - -const prefixConfig: IPrefixConfig = { - // Unused here. - aliases: "", displayNameSuffixes: {}, suffixes: {}, physicalAuditoriumRooms: [], - - auditoriumRooms: [ - "A.", - "AQ.", - ], - qaAuditoriumRooms: [ - "AQ.", - ], - interestRooms: [ - "X." - ], - nameOverrides: { - "A.special": "special-room", - }, -}; - -function getFixture(fixtureFile: string) { - return fs.readFileSync(path.join(__dirname, fixtureFile), 'utf8') -} - -/* - * A somewhat vague test that just loads in a basic file and checks a few things, comparing against the snapshots in - * __snapshots__. - */ -test('parsing pentabarf XML: overview', () => { - const xml = getFixture("pentabarf01_overview.xml"); - const p = new PentabarfParser(xml, prefixConfig); - - // TODO the auditorium id and slug look dodgy and not id-like or slug-like... - expect(p.auditoriums).toMatchSnapshot("auditoriums"); - - expect(p.conference).toMatchSnapshot("conference"); - - expect(p.interestRooms).toMatchSnapshot("interestRooms"); - - // NOTE Speakers don't have contact information: that's filled in by the PentaDb in the PentaBackend afterwards. - expect(p.speakers).toMatchSnapshot("speakers"); - - // NOTE I don't like that qa_startTime and livestream_endTime are 0 — they are updated in the PentaBackend - // using the PentaDb afterwards. - expect(p.talks).toMatchSnapshot("talks"); -}); - - -test('duplicate events lead to errors', () => { - const xml = getFixture("pentabarf03_duplicate_talk.xml"); - - expect(() => new PentabarfParser(xml, prefixConfig)).toThrow( - "Auditorium A.01 (Someroom): Talk E001: this talk already exists and is defined a second time." - ); -}); - -test("unrecognised prefixes don't create rooms", () => { - const xml = getFixture("pentabarf04_unrecognised_prefix.xml"); - const p = new PentabarfParser(xml, prefixConfig); - - expect(p.auditoriums.length).toEqual(0); - expect(p.talks.length).toEqual(0); - expect(p.interestRooms.length).toEqual(0); -}); - -test("tracks that set a online qa value correctly apply to talks", () => { - const xml = getFixture("pentabarf05_online_qa.xml"); - const p = new PentabarfParser(xml, prefixConfig); - expect(p.talks).toMatchSnapshot("talks"); -}); \ No newline at end of file diff --git a/src/__tests__/backends/penta/__snapshots__/PentabarfParser.test.ts.snap b/src/__tests__/backends/penta/__snapshots__/PentabarfParser.test.ts.snap deleted file mode 100644 index 97faf454..00000000 --- a/src/__tests__/backends/penta/__snapshots__/PentabarfParser.test.ts.snap +++ /dev/null @@ -1,281 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`parsing pentabarf XML: overview: auditoriums 1`] = ` -[ - { - "id": "A.01 (Someroom)", - "isPhysical": false, - "kind": "auditorium", - "name": "A.01 (Someroom)", - "slug": "a.01_someroom_", - "talks": Map { - "E001" => { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675502100000, - "id": "E001", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": null, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675501200000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "Track01", - }, - }, - }, -] -`; - -exports[`parsing pentabarf XML: overview: conference 1`] = ` -{ - "auditoriums": [ - { - "id": "A.01 (Someroom)", - "isPhysical": false, - "kind": "auditorium", - "name": "A.01 (Someroom)", - "slug": "a.01_someroom_", - "talks": Map { - "E001" => { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675502100000, - "id": "E001", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": null, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675501200000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "Track01", - }, - }, - }, - ], - "interestRooms": [], - "title": "Testcon01", -} -`; - -exports[`parsing pentabarf XML: overview: interestRooms 1`] = `[]`; - -exports[`parsing pentabarf XML: overview: speakers 1`] = ` -[ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, -] -`; - -exports[`parsing pentabarf XML: overview: talks 1`] = ` -[ - { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675502100000, - "id": "E001", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": null, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675501200000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "Track01", - }, -] -`; - -exports[`tracks that set a online qa value correctly apply to talks: talks 1`] = ` -[ - { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675502100000, - "id": "E001", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": 0, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675501200000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "TrackWithQA", - }, - { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675503900000, - "id": "E002", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": 0, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675502100000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "TrackWithoutQA", - }, - { - "auditoriumId": "A.01 (Someroom)", - "dateTs": 1675468800000, - "endTime": 1675504800000, - "id": "E003", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": null, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675503000000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "UnspecifiedTrack", - }, - { - "auditoriumId": "AQ.5 (QA Room)", - "dateTs": 1675468800000, - "endTime": 1675505700000, - "id": "E004", - "livestream_endTime": 0, - "prerecorded": true, - "pretalxCode": undefined, - "qa_startTime": 0, - "slug": "testcon_opening_remarks", - "speakers": [ - { - "email": "", - "id": "9012", - "matrix_id": "", - "name": "John Teststone", - "role": "speaker", - }, - { - "email": "", - "id": "90367", - "matrix_id": "", - "name": "Evå 完牲 Exampleson", - "role": "speaker", - }, - ], - "startTime": 1675503900000, - "subtitle": "", - "title": "Testcon01: Opening Remarks", - "track": "UnspecifiedTrack", - }, -] -`; diff --git a/src/__tests__/backends/penta/pentabarf01_overview.xml b/src/__tests__/backends/penta/pentabarf01_overview.xml deleted file mode 100644 index 5ab1e5f3..00000000 --- a/src/__tests__/backends/penta/pentabarf01_overview.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - Testcon01 - - - - - - 09:00 - 00:15 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - - Track01 - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - - diff --git a/src/__tests__/backends/penta/pentabarf02_withqa.xml b/src/__tests__/backends/penta/pentabarf02_withqa.xml deleted file mode 100644 index ca248abd..00000000 --- a/src/__tests__/backends/penta/pentabarf02_withqa.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - Testcon01 - - - - - - 09:00 - 00:15 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - - Track01 - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - - - 09:00 - 00:15 - AQ.02 (Questionroom) - testcon_questions - Testcon01: live questions - - Track02 - devroom - - <p>Questions for all!</p> - - - Ilian Interlocutor - - - - - diff --git a/src/__tests__/backends/penta/pentabarf03_duplicate_talk.xml b/src/__tests__/backends/penta/pentabarf03_duplicate_talk.xml deleted file mode 100644 index 27c13ddc..00000000 --- a/src/__tests__/backends/penta/pentabarf03_duplicate_talk.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - Testcon01 - - - - - - - 09:00 - 00:15 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - Track01 - devroom - - <p>Opening remarks</p> - - - John Teststone - Evå 完牲 Exampleson - - - - - - - - 09:00 - 00:15 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks for the second day - Track01 - devroom - - <p>Opening remarks</p> - - - John Teststone - - - - - diff --git a/src/__tests__/backends/penta/pentabarf04_unrecognised_prefix.xml b/src/__tests__/backends/penta/pentabarf04_unrecognised_prefix.xml deleted file mode 100644 index 5c1f314b..00000000 --- a/src/__tests__/backends/penta/pentabarf04_unrecognised_prefix.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - Testcon01 - - - - - - 09:00 - 08:00 - S.01 (Somestand) - teststand - TestStand - stand - - <p>Visit our stand today!</p> - - - John Teststone - - - - - diff --git a/src/__tests__/backends/penta/pentabarf05_online_qa.xml b/src/__tests__/backends/penta/pentabarf05_online_qa.xml deleted file mode 100644 index 9f2d0b29..00000000 --- a/src/__tests__/backends/penta/pentabarf05_online_qa.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - Testcon01 - - - - TrackWithQA - TrackWithoutQA - - - - - 09:00 - 00:15 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - - TrackWithQA - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - 09:15 - 00:30 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - - TrackWithoutQA - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - 09:30 - 00:30 - A.01 (Someroom) - testcon_opening_remarks - Testcon01: Opening Remarks - - UnspecifiedTrack - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - - - 09:45 - 00:30 - AQ.5 (QA Room) - testcon_opening_remarks - Testcon01: Opening Remarks - - UnspecifiedTrack - devroom - - - <p>Opening remarks</p> - - - - John Teststone - Evå 完牲 Exampleson - - - - - Submit feedback - - - - - diff --git a/src/backends/CachingBackend.ts b/src/backends/CachingBackend.ts deleted file mode 100644 index 23faa1db..00000000 --- a/src/backends/CachingBackend.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { LogService } from "matrix-bot-sdk"; -import { RoomKind } from "../models/room_kinds"; -import { IConference, ITalk, IAuditorium, IInterestRoom } from "../models/schedule"; -import { jsonReplacerMapToObject, readJsonFileAsync, writeJsonFileAsync } from "../utils"; -import { IScheduleBackend, TalkId } from "./IScheduleBackend"; - - -type BackendFactory = () => Promise; - -/** - * Layout of the cache file content, as encoded on disk. - * - * Note that this can only contain primitive JSON types — no Map<>s etc, so we have to make minor customisations sometimes. - */ -interface IRawCacheFileContent { - conference: { - title: string; - auditoriums: IRawCacheAuditorium[]; - interestRooms: IInterestRoom[]; - }; -} -interface IRawCacheAuditorium { - id: string; - slug: string; - name: string; - kind: RoomKind; - // This is a Map<> in the real type. - talks: Record; - isPhysical: boolean; -} - -/** - * Wrapper for any schedule backend that adds caching (in case the source goes down) and refresh support. - */ -export class CachingBackend implements IScheduleBackend { - public conference: IConference; - public talks: Map = new Map(); - public auditoriums: Map = new Map(); - public interestRooms: Map = new Map(); - - private wasCached: boolean = true; - - /** - * We hang on to the last backend for refreshing short-notice alterations. - * (I don't particularly like it, but there isn't time to reason through a better approach at the moment.) - */ - private lastUsedBackend: IScheduleBackend | null = null; - - /** - * @param underlyingBackend A factory for the underlying backend, which will be reconstructed each time we try to refresh. - */ - public constructor(private underlyingBackend: BackendFactory, private cachePath: string) { - // All the real work is in init(). The arguments here are properties. - } - - /** - * To be called immediately after construction. - */ - public async init(): Promise { - try { - await this.refresh(); - } catch (e) { - LogService.error("CachingBackend", "Failed to create underlying backend: ", e.body ?? e); - - try { - await this.loadFromCache(); - } catch (e) { - LogService.error("CachingBackend", "Double fault: can't load from schedule source and can't load from cache: ", e.body ?? e); - throw new Error("Double fault when trying to load either schedule source or cache."); - } - } - - } - - public static async new(underlyingBackend: BackendFactory, cachePath: string): Promise { - const cachingBackend = new CachingBackend(underlyingBackend, cachePath); - await cachingBackend.init(); - return cachingBackend; - } - - async refresh(): Promise { - const backend = await this.underlyingBackend(); - this.lastUsedBackend = backend; - - this.conference = backend.conference; - this.talks = backend.talks; - this.auditoriums = backend.auditoriums; - this.interestRooms = backend.interestRooms; - this.wasCached = false; - - try { - await this.saveCacheToDisk(); - } catch (e) { - // I wish we could be noisier about this, but not sure it's worth jeopardising a successful refresh over... - LogService.error("CachingBackend", "Failed to save cache to disk: ", e.body ?? e); - } - } - - async refreshShortTerm(lookaheadSeconds: number): Promise { - if (this.lastUsedBackend !== null) { - // It's notable that we don't save any changes to disk. - // It wouldn't be a bad idea to persist the changes, but introducing a lot of disk I/O into this frequent operation - // made me uncomfortable. - await this.lastUsedBackend.refreshShortTerm?.(lookaheadSeconds); - } - } - - private async saveCacheToDisk(): Promise { - // Save a cached copy. - // Do it atomically so that there's very little chance of anything going wrong: write to a file first, then move into place. - await writeJsonFileAsync(this.cachePath, { conference: this.conference }, jsonReplacerMapToObject); - } - - private async loadFromCache(): Promise { - const payload: IRawCacheFileContent = await readJsonFileAsync(this.cachePath) as any; - - function loadAuditorium(raw: IRawCacheAuditorium): IAuditorium { - return { - id: raw.id, - kind: raw.kind, - name: raw.name, - slug: raw.slug, - talks: new Map(Object.entries(raw.talks)), - isPhysical: raw.isPhysical - }; - } - - this.conference = { - auditoriums: payload.conference.auditoriums.map(loadAuditorium), - interestRooms: payload.conference.interestRooms, - title: payload.conference.title - }; - this.auditoriums.clear(); - this.interestRooms.clear(); - this.talks.clear(); - - // Rebuild the lists. - for (const auditorium of this.conference.auditoriums) { - this.auditoriums.set(auditorium.id, auditorium); - for (const [talkId, talk] of auditorium.talks) { - this.talks.set(talkId, talk); - } - } - - for (const interest of this.conference.interestRooms) { - this.interestRooms.set(interest.id, interest); - } - } - - wasLoadedFromCache(): boolean { - return this.wasCached; - } -} \ No newline at end of file diff --git a/src/backends/json/jsontypes/JsonSchedule.schema.d.ts b/src/backends/json/jsontypes/JsonSchedule.schema.d.ts index e95dea99..e418a263 100644 --- a/src/backends/json/jsontypes/JsonSchedule.schema.d.ts +++ b/src/backends/json/jsontypes/JsonSchedule.schema.d.ts @@ -1,4 +1,4 @@ -/* tslint:disable */ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, diff --git a/src/backends/penta/PentaBackend.ts b/src/backends/penta/PentaBackend.ts deleted file mode 100644 index e6b987b9..00000000 --- a/src/backends/penta/PentaBackend.ts +++ /dev/null @@ -1,162 +0,0 @@ -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"; -import { PentabarfParser } from "./PentabarfParser"; -import * as fetch from "node-fetch"; -import { LogService } from "matrix-bot-sdk"; -import { IDbTalk } from "./db/DbTalk"; - - -export class PentaBackend implements IScheduleBackend { - constructor(parser: PentabarfParser, public db: PentaDb) { - this.updateFromParser(parser); - } - - /** - * We need an async version of the constructor. - * Must be called right after construction. - */ - async init() { - await this.hydrateFromDatabase(); - } - - private updateFromParser(parser: PentabarfParser): void { - const conference = parser.conference; - const talks = new Map(); - const auditoriums = new Map(); - const interestRooms = new Map(); - - for (let auditorium of parser.auditoriums) { - if (auditoriums.has(auditorium.id)) { - throw `Conflict in auditorium ID «${auditorium.id}»!`; - } - auditoriums.set(auditorium.id, auditorium); - - for (let talk of auditorium.talks.values()) { - if (talks.has(talk.id)) { - const conflictingTalk = talks.get(talk.id); - throw `Talk ID ${talk.id} is not unique — occupied by both «${talk.title}» and «${conflictingTalk.title}»!`; - } - talks.set(talk.id, talk); - } - } - - for (let interest of parser.interestRooms) { - if (interestRooms.has(interest.id)) { - throw `Conflict in interest ID «${interest.id}»!`; - } - } - - // Update all at the end, to prevent non-atomic updates in the case of a failure. - this.conference = conference; - this.talks = talks; - this.auditoriums = auditoriums; - this.interestRooms = interestRooms; - } - - private async hydrateFromDatabase(): Promise { - for (let talk of this.talks.values()) { - this.hydrateTalk(talk); - - for (let person of talk.speakers) { - this.hydratePerson(person); - } - } - - // TODO do we need to hydrate any other objects? - } - - private async hydrateTalk(talk: ITalk): Promise { - const dbTalk = await this.db.getTalk(talk.id); - if (dbTalk === null) return; - this.rehydrateTalkFrom(talk, dbTalk); - - // Not all people are listed in the Penta XML! - // Notably, devroom managers ('coordinators') are only available in the database. - // Make sure we get those whilst we hydrate our talk... - for (const person of await this.db.findAllPeopleForTalk(talk.id)) { - // Ensure we don't wind up duplicating people; this step is purely to get people who were missed out of the XML. - if (!talk.speakers.find(s => s.id == person.id)) { - // The person is already hydrated — they're straight from the DB — so no hydration step needed here. - talk.speakers.push(person); - } - } - } - - private rehydrateTalkFrom(talk: ITalk, dbTalk: IDbTalk): void { - if (talk.qa_startTime !== null) { - // hydrate Q&A time if enabled - // Rationale for hydrating Q&A time: it's not available in the Pentabarf XML. - talk.qa_startTime = dbTalk.qa_start_datetime; - } - - talk.livestream_endTime = dbTalk.livestream_end_datetime; - - // Rationale for hydrating talk start & end time: there can be short-notice alterations to the schedule - // (and rehydrating talks is how `refreshShortTerm` is implemented) - // and during testing, the PentaDB can have a time shift set which changes the time of talks compared to the XML. - talk.startTime = dbTalk.start_datetime; - talk.endTime = dbTalk.end_datetime; - } - - private async hydratePerson(person: IPerson): Promise { - const dbPeople = await this.db.findPeopleWithId(person.id); - if (dbPeople.length == 0) return; - - // Multiple people may be returned by this query. - // See https://github.com/matrix-org/conference-bot/issues/151 - // In the future, would be nice to throw an exception: - // `Person ID '${person.id}' has ${dbPeople.length} different people associated with it!` - const dbPerson = dbPeople[0]; - person.matrix_id = dbPerson.matrix_id; - person.email = dbPerson.email; - } - - wasLoadedFromCache(): boolean { - // Penta backend doesn't support using a cache. - return false; - } - - 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(config); - await db.connect(); - const backend = new PentaBackend(parsed, db); - await backend.init(); - return backend; - } - - refresh(): Promise { - throw new Error("refresh() not implemented for Penta backend."); - } - - /** - * See description on `IScheduleBackend`. - * - * For the penta backend, we consult the database for short-notice alterations and rehydrate any affected talks. - */ - async refreshShortTerm(lookaheadSeconds: number): Promise { - const talksOfInterest = await this.db.getTalksWithUpcomingEvents(lookaheadSeconds / 60); - for (const dbTalk of talksOfInterest) { - const talk = this.talks.get(dbTalk.event_id); - if (talk === undefined) { - LogService.warn("PentaBackend", `refreshShortTerm: DB talk '${dbTalk.event_id}' is upcoming but has no talk entry to hydrate.`); - continue; - } - this.rehydrateTalkFrom(talk, dbTalk); - } - } - - conference: IConference; - talks: Map; - auditoriums: Map; - interestRooms: Map; -} \ No newline at end of file diff --git a/src/backends/penta/db/DbPerson.ts b/src/backends/penta/db/DbPerson.ts deleted file mode 100644 index 8ae35f13..00000000 --- a/src/backends/penta/db/DbPerson.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { IPerson, Role } from "../../../models/schedule"; - -export interface IDbPerson { - event_id: string; // penta talk ID - person_id: string; - event_role: Role; - name: string; - email: string; - matrix_id: string; - conference_room: string; - remark: string; -} - -export function dbPersonToPerson(dbPerson: IDbPerson): IPerson { - return { - id: dbPerson.person_id, - matrix_id: dbPerson.matrix_id, - role: dbPerson.event_role, - email: dbPerson.email, - name: dbPerson.name, - // TODO There are other attributes which we don't carry over right now. - }; -} \ No newline at end of file diff --git a/src/backends/penta/db/DbTalk.ts b/src/backends/penta/db/DbTalk.ts deleted file mode 100644 index 8250c3d3..00000000 --- a/src/backends/penta/db/DbTalk.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export interface IRawDbTalk { - event_id: string; // penta ID - /** - * ID of the **auditorium** that will hold this talk. - */ - conference_room: string; - start_datetime: number; // ms timestamp, utc - duration_seconds: number; // seconds - presentation_length_seconds: number; // seconds - end_datetime: number; // ms timestamp, utc - qa_start_datetime: number; // ms timestamp, utc - prerecorded: boolean; -} - -export interface IDbTalk extends IRawDbTalk { - /** - * The start time of the talk's livestream, as a Unix timestamp in milliseconds. - * - * This is the start of the Q&A session for prerecorded talks, and the start of the talk for - * non-prerecorded talks. - */ - livestream_start_datetime: number; // ms timestamp, utc - - /** - * The end time of the talk's livestream, as a Unix timestamp in milliseconds. - * - * This may occur before the end of the talk. - */ - livestream_end_datetime: number; // ms timestamp, utc -} diff --git a/src/backends/penta/db/PentaDb.ts b/src/backends/penta/db/PentaDb.ts deleted file mode 100644 index 684a9920..00000000 --- a/src/backends/penta/db/PentaDb.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { Pool } from "pg"; -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 "; -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 `; - -export class PentaDb { - private client: Pool; - private isConnected = false; - pentaConfig: 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.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.pentaConfig.sslmode === 'disable' ? false : { - rejectUnauthorized: this.pentaConfig.sslmode === 'no-verify', - }, - }); - } - - public async connect() { - if (this.isConnected) return; - await this.client.connect(); - this.isConnected = true; - } - - public async disconnect() { - if (!this.isConnected) return; - await this.client.end(); - this.isConnected = false; - } - - /** - * Gets a person by ID. Can returns multiple results because a person with the same ID may be defined multiple times - * acting in different capacities, e.g. speaker of one talk, coordinator of another. - * TODO Normalise this and ensure that a person is only defined once, with the roles split out from the definition of a person. - * see: https://github.com/matrix-org/conference-bot/issues/151 - */ - public async findPeopleWithId(personId: string): Promise { - const numericPersonId = Number(personId); - if (Number.isSafeInteger(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} ${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} ${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} ${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} ${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} ${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} ${this.pentaConfig.tblPeople} WHERE remark = $1`, [remark]); - return this.sanitizeRecords(result.rows).map(dbPersonToPerson); - } - - public async getUpcomingTalkStarts(inNextMinutes: number, minBefore: number): Promise { - return this.getTalksWithin(START_QUERY, inNextMinutes, minBefore); - } - - public async getUpcomingQAStarts(inNextMinutes: number, minBefore: number): Promise { - return this.getTalksWithin(QA_START_QUERY, inNextMinutes, minBefore); - } - - public async getUpcomingTalkEnds(inNextMinutes: number, minBefore: number): Promise { - return this.getTalksWithin(END_QUERY, inNextMinutes, minBefore); - } - - /** - * Returns a list of database entries for all talks who have some kind of event in the next `lookaheadSeconds` seconds. - * - * A suggested implementation might be to return all talks overlapping this window, but for now the simple implementation - * is just to return talks with a start, end or Q&A start within the time window. - * - * @param lookaheadMinutes Number of minutes to look ahead into the future. - */ - public async getTalksWithUpcomingEvents(lookaheadMinutes: number): Promise { - const talksStarting = await this.getUpcomingTalkStarts(lookaheadMinutes, lookaheadMinutes); - const talksEnding = await this.getUpcomingTalkEnds(lookaheadMinutes, lookaheadMinutes); - const talksQaStarting = await this.getUpcomingQAStarts(lookaheadMinutes, lookaheadMinutes); - const result: IDbTalk[] = []; - const seenTalkIds = new Set(); - - for (const talk of talksStarting.concat(talksEnding, talksQaStarting)) { - if (seenTalkIds.has(talk.event_id)) { - continue; - } - seenTalkIds.add(talk.event_id); - result.push(talk); - } - - return result; - } - - /** - * Gets the record for a talk. - * @param talkId The talk ID. - * @returns The record for the talk, if it exists; `null` otherwise. - */ - public async getTalk(talkId: string): Promise { - const result = await this.client.query( - `${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} ${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.pentaConfig.schedulePreBufferSeconds * 1000; - let livestreamStartDatetime: number; - if (talk.prerecorded) { - // For prerecorded talks, a preroll is shown, followed by the talk recording, then an - // interroll, then live Q&A. - livestreamStartDatetime = qaStartDatetime; - } else { - // For live talks, both the preroll and interroll are shown, followed by the live talk. - livestreamStartDatetime = talk.start_datetime + this.pentaConfig.schedulePreBufferSeconds * 1000; - } - const livestreamEndDatetime = talk.end_datetime - this.pentaConfig.schedulePostBufferSeconds * 1000; - - return { - ...talk, - - qa_start_datetime: qaStartDatetime, - livestream_start_datetime: livestreamStartDatetime, - livestream_end_datetime: livestreamEndDatetime, - }; - } - - private postprocessDbTalks(rows: IRawDbTalk[]): IDbTalk[] { - return rows.map(this.postprocessDbTalk.bind(this)); - } - - private sanitizeRecords(rows: IDbPerson[]): IDbPerson[] { - return rows.map(r => { - r = objectFastClone(r); - const userId = r.matrix_id; - try { - if (userId) { - // we use the variable even though it's a no-op just to avoid - // the compiler optimizing us out. - const parsed = new UserID(userId); - r.matrix_id = parsed.toString().trim(); - } - } catch (e) { - LogService.warn("PentaDb", "Invalid user ID: " + userId, e); - r.matrix_id = ""; // force clear - } - return r; - }); - } -} diff --git a/src/backends/pretalx/PretalxBackend.ts b/src/backends/pretalx/PretalxBackend.ts index 8de3110e..aa44210a 100644 --- a/src/backends/pretalx/PretalxBackend.ts +++ b/src/backends/pretalx/PretalxBackend.ts @@ -98,6 +98,7 @@ export class PretalxScheduleBackend implements IScheduleBackend { // For FOSDEM we prefer to use the pentabarf format as it contains // extra information not found in the JSON format. This may change // in the future. + // TODO what is this extra information??? if (cfg.scheduleFormat === PretalxScheduleFormat.FOSDEM) { const pentaData = new PentabarfParser(jsonOrXMLDesc, prefixCfg); data = { @@ -198,4 +199,4 @@ export class PretalxScheduleBackend implements IScheduleBackend { get interestRooms(): Map { return this.data.interestRooms; } -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index f98e0174..e93c72e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -110,20 +110,6 @@ export interface IJsonScheduleBackendConfig { * Path or HTTP(S) URL to schedule. */ scheduleDefinition: string; - - /** - * Slightly awful, but this works around some type errors in places that don't get hit if you're using a JSON schedule. - */ - database: undefined; -} - -export interface IPentaScheduleBackendConfig { - backend: "penta"; - /** - * HTTP(S) URL to schedule. - */ - scheduleDefinition: string; - database: IPentaDbConfig; } export enum PretalxScheduleFormat { @@ -155,20 +141,7 @@ export interface IPretalxScheduleBackendConfig { } -export type ScheduleBackendConfig = IJsonScheduleBackendConfig | IPentaScheduleBackendConfig | IPretalxScheduleBackendConfig; - -export interface IPentaDbConfig { - host: string; - port: number; - username: string; - password: string; - database: string; - sslmode: string; - tblPeople: string; - tblSchedule: string; - schedulePreBufferSeconds: number; - schedulePostBufferSeconds: number; -} +export type ScheduleBackendConfig = IJsonScheduleBackendConfig | IPretalxScheduleBackendConfig; const liveConfig: IConfig = { ...config, diff --git a/src/index.ts b/src/index.ts index be9cfcdd..e486f64f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,11 +54,9 @@ import { AttendanceCommand } from "./commands/AttendanceCommand"; import { ScheduleCommand } from "./commands/ScheduleCommand"; import { CheckInMap } from "./CheckInMap"; import { IScheduleBackend } from "./backends/IScheduleBackend"; -import { PentaBackend } from "./backends/penta/PentaBackend"; import { JsonScheduleBackend } from "./backends/json/JsonScheduleBackend"; import { JoinCommand } from "./commands/JoinRoomCommand"; import { StatusCommand } from "./commands/StatusCommand"; -import { CachingBackend } from "./backends/CachingBackend"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; import { Server } from "http"; import { collectDefaultMetrics, register } from "prom-client"; @@ -71,8 +69,6 @@ LogService.info("index", "Bot starting..."); export class ConferenceBot { 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 "pretalx": return await PretalxScheduleBackend.new(config.dataPath, config.conference.schedule, config.conference.prefixes); case "json": diff --git a/src/web.ts b/src/web.ts index dc91c6e8..f9dd5f78 100644 --- a/src/web.ts +++ b/src/web.ts @@ -23,7 +23,6 @@ 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"; @@ -56,35 +55,6 @@ export function renderAuditoriumWidget(req: Request, res: Response, conference: }); } -const TALK_CACHE_DURATION = 60 * 1000; // ms -const dbTalksCache: { - [talkId: string]: { - talk: Promise, - cachedAt: number, // ms - }, -} = {}; - -/** - * Gets the Pentabarf database record for a talk, with a cache. - * @param talkId The talk ID. - * @returns The database record for the talk, if it exists; `null` otherwise. - */ -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 conference.getPentaDb(); - if (db === null) return null; - - dbTalksCache[talkId] = { - talk: db.getTalk(talkId), - cachedAt: now, - }; - } - - return dbTalksCache[talkId].talk; -} - 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)) { @@ -109,10 +79,6 @@ export async function renderTalkWidget(req: Request, res: Response, conference: return res.sendStatus(404); } - // 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, conference); - const streamUrl = template(talkUrl, { audId: audId.toLowerCase(), slug: (await talk.getDefinition()).slug.toLowerCase(), @@ -125,8 +91,9 @@ export async function renderTalkWidget(req: Request, res: Response, conference: roomName: await talk.getName(), conferenceDomain: jitsiDomain, conferenceId: base32.stringify(Buffer.from(talk.roomId), { pad: false }).toLowerCase(), - livestreamStartTime: dbTalk?.livestream_start_datetime ?? "", - livestreamEndTime: dbTalk?.livestream_end_datetime ?? "", + // TODO These are broken/redundant as they relied on PentaDb + livestreamStartTime: "", + livestreamEndTime: "", }); }