From 7d068addb7c15557ad749e862ad17ad47d011b44 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 13 Oct 2023 16:52:18 +0100 Subject: [PATCH] Start adding pretalx work --- src/backends/pretalx/PretalxBackend.ts | 144 +++++++++++++++++++++++++ src/backends/pretalx/PretalxSchema.ts | 83 ++++++++++++++ src/config.ts | 11 +- src/index.ts | 3 + 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/backends/pretalx/PretalxBackend.ts create mode 100644 src/backends/pretalx/PretalxSchema.ts diff --git a/src/backends/pretalx/PretalxBackend.ts b/src/backends/pretalx/PretalxBackend.ts new file mode 100644 index 0000000..f87c0d7 --- /dev/null +++ b/src/backends/pretalx/PretalxBackend.ts @@ -0,0 +1,144 @@ +import { IPretalxScheduleBackendConfig } from "../../config"; +import { IConference, ITalk, IAuditorium, IInterestRoom } from "../../models/schedule"; +import { AuditoriumId, InterestId, IScheduleBackend, TalkId } from "../IScheduleBackend"; +import * as fetch from "node-fetch"; +import * as path from "path"; +import { LogService } from "matrix-bot-sdk"; +import { PretalxData, PretalxSchema } from "./PretalxSchema"; +import { readFile, writeFile } from "fs/promises"; +import { XMLParser } from "fast-xml-parser"; + + +export class PretalxScheduleBackend implements IScheduleBackend { + data: PretalxSchema; + private constructor(private cfg: IPretalxScheduleBackendConfig, private wasFromCache: boolean, public readonly dataPath: string) { + + } + + wasLoadedFromCache(): boolean { + return this.wasFromCache; + } + + static async parseFromXML(rawXml: string): Promise { + const parser = new XMLParser({ + attributesGroupName: "attr", + attributeNamePrefix : "@_", + textNodeName: "#text", + ignoreAttributes: false, + }); + const { schedule } = parser.parse(rawXml) as PretalxData; + const interestRooms = new Map(); + const auditoriums = new Map(); + const talks = new Map(); + + const normaliseToArray = (v: T|undefined|T[]) => v !== undefined ? (Array.isArray(v) ? v : [v]) : []; + + for (const day of normaliseToArray(schedule.day)) { + for (const room of normaliseToArray(day.room)) { + for (const event of normaliseToArray(room.event)) { + + console.log(event.title); + } + } + } + + return { + conference: { + title: schedule.conference.title["#text"], + interestRooms: [], + auditoriums: [], + }, + interestRooms, + auditoriums, + talks, + } + } + + private static async loadConferenceFromCfg(dataPath: string, cfg: IPretalxScheduleBackendConfig, allowUseCache: boolean): Promise<{data: PretalxSchema, cached: boolean}> { + let xmlDesc; + let cached = false; + + const cachedSchedulePath = path.join(dataPath, 'cached_schedule.xml'); + + try { + if (cfg.scheduleDefinition.startsWith("http")) { + // Fetch the JSON track over the network + xmlDesc = await fetch(cfg.scheduleDefinition).then(r => r.json()); + } else { + // Load the JSON from disk + xmlDesc = await readFile(cfg.scheduleDefinition, 'utf-8'); + } + + // Save a cached copy. + await writeFile(cachedSchedulePath, xmlDesc); + } catch (e) { + // Fallback to cache — only if allowed + if (! allowUseCache) throw e; + + cached = true; + + LogService.error("JsonScheduleBackend", "Unable to load JSON schedule, will use cached copy if available.", e.body ?? e); + try { + xmlDesc = await readFile(cachedSchedulePath, 'utf-8'); + } catch (e) { + if (e.code === 'ENOENT') { + // No file + LogService.error("JsonScheduleBackend", "Double fault: Unable to load schedule and unable to load cached schedule (cached file doesn't exist)"); + } else if (e instanceof SyntaxError) { + LogService.error("JsonScheduleBackend", "Double fault: Unable to load schedule and unable to load cached schedule (cached file has invalid JSON)"); + } else { + LogService.error("JsonScheduleBackend", "Double fault: Unable to load schedule and unable to load cached schedule: ", e); + } + + throw "Double fault whilst trying to load JSON schedule"; + } + } + + + let data: PretalxSchema = { + interestRooms: new Map(), + auditoriums: new Map(), + talks: new Map(), + conference: { + title: 'Hi', + auditoriums: [], + interestRooms: [], + } + }; + + return {data, cached}; + } + + static async new(dataPath: string, cfg: IPretalxScheduleBackendConfig): Promise { + const loader = await PretalxScheduleBackend.loadConferenceFromCfg(dataPath, cfg, true); + return new PretalxScheduleBackend(cfg, loader.cached, dataPath); + } + + async refresh(): Promise { + this.data = (await PretalxScheduleBackend.loadConferenceFromCfg(this.dataPath, this.cfg, false)).data; + // If we managed to load anything, this isn't from the cache anymore. + this.wasFromCache = false; + } + + async refreshShortTerm(_lookaheadSeconds: number): Promise { + // NOP: There's no way to partially refresh a JSON schedule. + // Short-term changes to a JSON schedule are therefore currently unimplemented. + // This hack was intended for Penta anyway. + } + + get conference(): IConference { + return this.data.conference; + }; + + get talks(): Map { + return this.data.talks; + } + + get auditoriums(): Map { + return this.data.auditoriums; + } + + get interestRooms(): Map { + return this.data.interestRooms; + } +} \ No newline at end of file diff --git a/src/backends/pretalx/PretalxSchema.ts b/src/backends/pretalx/PretalxSchema.ts new file mode 100644 index 0000000..76cbe6c --- /dev/null +++ b/src/backends/pretalx/PretalxSchema.ts @@ -0,0 +1,83 @@ +import { IInterestRoom, IAuditorium, ITalk, IConference } from "../../models/schedule"; + +type TextNode = { + '#text': string; +} + +type OptionalTextNode = { + '#text': string; +} + +type OneOrMore = T|T[]; + +export interface PretalxData { + schedule: { + generator: { + attrs: { + '@_name': 'pretalx', + '@_version': string, + } + }, + version: TextNode, + conference: { + acronym: TextNode, + title: TextNode, + start: TextNode, + end: TextNode, + days: TextNode, + timeslot_duration: TextNode, + base_url: TextNode, + }, + day: OneOrMore<{ + attrs: { + '@_index': string, + '@_date': string, + '@_start': string, + '@_end': string, + }, + room?: OneOrMore<{ + attrs: { + '@_name': string, + }, + event?: OneOrMore<{ + attrs: { + '@_guid': string, + '@_id': string, + } + date: TextNode, + start: TextNode, + duration: TextNode, + room: TextNode, + slug: TextNode, + url: TextNode, + recording: { + licence?: TextNode, + optout?: TextNode, + }, + title: TextNode, + subtitle: OptionalTextNode, + track: OptionalTextNode, + type: OptionalTextNode, + language: OptionalTextNode, + abstract: OptionalTextNode, + description: OptionalTextNode, + logo: OptionalTextNode, + persons: OneOrMore<{ + attrs: { + '@_id': string, + } + }&TextNode>, + links: OneOrMore, + attachments: OneOrMore, + }>, + }>, + }>, + }, +} + +export interface PretalxSchema { + interestRooms: Map; + auditoriums: Map; + talks: Map; + conference: IConference; +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 1ed910f..990c801 100644 --- a/src/config.ts +++ b/src/config.ts @@ -111,7 +111,16 @@ export interface IPentaScheduleBackendConfig { database: IPentaDbConfig; } -export type ScheduleBackendConfig = IJsonScheduleBackendConfig | IPentaScheduleBackendConfig; +export interface IPretalxScheduleBackendConfig { + backend: "pretalx"; + /** + * HTTP(S) URL to schedule XML. + */ + scheduleDefinition: string; +} + + +export type ScheduleBackendConfig = IJsonScheduleBackendConfig | IPentaScheduleBackendConfig | IPretalxScheduleBackendConfig; export interface IPentaDbConfig { host: string; diff --git a/src/index.ts b/src/index.ts index 008ff0e..65fdbdd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ import { StatusCommand } from "./commands/StatusCommand"; import { CachingBackend } from "./backends/CachingBackend"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; import { Server } from "http"; +import { PretalxScheduleBackend } from "./backends/pretalx/PretalxBackend"; LogService.setLogger(new CustomLogger()); LogService.setLevel(LogLevel.DEBUG); @@ -76,6 +77,8 @@ export class ConferenceBot { 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); + case "pretalx": + return await PretalxScheduleBackend.new(config.dataPath, config.conference.schedule); default: throw new Error(`Unknown scheduling backend: choose penta or json!`) }