From 259704d001be22418a9bfe550f51c0483eef7d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl?= Date: Tue, 22 Mar 2022 23:24:37 +0100 Subject: [PATCH] New listener updateOengusSchedule Listener that will receive data with an Oengus schedule with lines that will be updated on Oengus. Only the runs included will be modified (matching on id) and you cannot add nor delete runs nor nullify fields --- configschema.json | 4 + schemas/reused/ScheduleImportStatus.json | 4 + src/extension/oengus-import.ts | 134 ++++++++++++++++++++-- src/types/Events.d.ts | 9 ++ src/types/schemas/configschema.d.ts | 1 + src/types/schemas/horaroImportStatus.d.ts | 1 + src/types/schemas/oengusImportStatus.d.ts | 1 + 7 files changed, 142 insertions(+), 12 deletions(-) diff --git a/configschema.json b/configschema.json index 43ac58d5..fa06bb39 100644 --- a/configschema.json +++ b/configschema.json @@ -188,6 +188,10 @@ "type": "string", "default": "SHORTNAME" }, + "token": { + "type": "string", + "default": "TOKEN" + }, "useJapanese": { "type": "boolean", "default": false diff --git a/schemas/reused/ScheduleImportStatus.json b/schemas/reused/ScheduleImportStatus.json index 383397a6..a104c72c 100644 --- a/schemas/reused/ScheduleImportStatus.json +++ b/schemas/reused/ScheduleImportStatus.json @@ -7,6 +7,10 @@ "type": "boolean", "default": false }, + "exporting": { + "type": "boolean", + "default": false + }, "item": { "type": "number", "default": 0 diff --git a/src/extension/oengus-import.ts b/src/extension/oengus-import.ts index de2898bd..99d1b1cb 100644 --- a/src/extension/oengus-import.ts +++ b/src/extension/oengus-import.ts @@ -1,4 +1,4 @@ -import { OengusMarathon, OengusSchedule, RunData, RunDataPlayer, RunDataTeam } from '@nodecg-speedcontrol/types'; // eslint-disable-line object-curly-newline, max-len +import { OengusLine, OengusMarathon, OengusSchedule, RunData, RunDataPlayer, RunDataTeam, SendMessageAck } from '@nodecg-speedcontrol/types'; // eslint-disable-line object-curly-newline, max-len import { Duration, parse as isoParse, toSeconds } from 'iso8601-duration'; import { isObject } from 'lodash'; import needle, { NeedleResponse } from 'needle'; @@ -9,6 +9,7 @@ import { verifyTwitchDir } from './twitch-api'; import { bundleConfig, checkGameAgainstIgnoreList, getTwitchUserFromURL, padTimeNumber, processAck, to } from './util/helpers'; // eslint-disable-line object-curly-newline, max-len import { get as ncgGet } from './util/nodecg'; import { defaultSetupTime, oengusImportStatus, runDataArray } from './util/replicants'; +import * as events from './util/events'; const nodecg = ncgGet(); const config = bundleConfig(); @@ -47,6 +48,44 @@ async function get(endpoint: string): Promise { } } +/** + * Make a PUT request to Oengus API. + * @param endpoint Oengus API endpoint you want to access. + * @param oengusSchedule Oengus schedule. + */ +async function put(endpoint: string, oengusSchedule: OengusSchedule): Promise { + if (config.oengus.token) { + try { + nodecg.log.debug(`[Oengus Update] API request processing on ${endpoint}`); + const resp = await needle( + 'put', + `https://${config.oengus.useSandbox ? 'sandbox.' : ''}oengus.io/api${endpoint}`, + oengusSchedule, + { + headers: { + 'Authorization': `Bearer ${config.oengus.token}`, + 'User-Agent': 'nodecg-speedcontrol', + 'Content-Type': 'application/json', + 'oengus-version': '1', + }, + }, + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: parser exists but isn't in the typings + if (resp.statusCode !== 204) { + throw new Error('Response status was not 204' + JSON.stringify(resp.body)); + } + nodecg.log.debug(`[Oengus Update] API request successful on ${endpoint}`); + } catch (err) { + nodecg.log.debug(`[Oengus Update] API request error on ${endpoint}:`, err); + throw err; + } + } else { + nodecg.log.debug(`[Oengus Update] No Authorization provided`); + throw new Error('No Authorization provided'); + } +} + /** * Format to time string from Duration object. * @param duration Duration object you want to format. @@ -74,6 +113,7 @@ function isOengusSchedule(source: any): source is OengusSchedule { */ function resetImportStatus(): void { oengusImportStatus.value.importing = false; + oengusImportStatus.value.exporting = false; oengusImportStatus.value.item = 0; oengusImportStatus.value.total = 0; nodecg.log.debug('[Oengus Import] Import status restored to default'); @@ -235,16 +275,86 @@ async function importSchedule(marathonShort: string, useJapanese: boolean): Prom } nodecg.listenFor('importOengusSchedule', async (data, ack) => { - try { - if (oengusImportStatus.value.importing) { - throw new Error('Already importing schedule'); - } - nodecg.log.info('[Oengus Import] Started importing schedule'); - await importSchedule(data.marathonShort, data.useJapanese); - nodecg.log.info('[Oengus Import] Successfully imported schedule from Oengus'); - processAck(ack, null); - } catch (err) { - nodecg.log.warn('[Oengus Import] Error importing schedule:', err); - processAck(ack, err); + importOengusSchedule(data) + .then(() => processAck(ack, null)) + .catch((err) => { + nodecg.log.warn('[Oengus Import] Error importing schedule:', err); + processAck(ack, err) + }); +}); + +events.listenFor('importOengusSchedule', async (data, ack) => { + importOengusSchedule(data) + .then(() => processAck(ack, null)) + .catch((err) => { + nodecg.log.warn('[Oengus Import] Error importing schedule:', err); + processAck(ack, err) + }); +}); + +async function importOengusSchedule(data: { marathonShort: string; useJapanese: boolean; }): Promise { + if (oengusImportStatus.value.importing) { + throw new Error('Already importing schedule'); } + if (oengusImportStatus.value.exporting) { + throw new Error('Already exporting schedule'); + } + nodecg.log.info('[Oengus Import] Started importing schedule'); + await importSchedule(data.marathonShort, data.useJapanese); + nodecg.log.info('[Oengus Import] Successfully imported schedule from Oengus'); +} + +nodecg.listenFor('updateOengusSchedule', async (data, ack) => { + updateOengusSchedule(data) + .then(() => processAck(ack, null)) + .catch((err) => { + nodecg.log.warn('[Oengus Import] Error importing schedule:', err); + processAck(ack, err) + }); }); + +events.listenFor('updateOengusSchedule', async (data, ack) => { + updateOengusSchedule(data) + .then(() => processAck(ack, null)) + .catch((err) => { + nodecg.log.warn('[Oengus Update] Error updating schedule:', err); + processAck(ack, err) + }); +}); + +async function updateOengusSchedule(data: { marathonShort: string, lines: Partial[]}): Promise { + if (oengusImportStatus.value.importing) { + throw new Error('Already importing schedule'); + } + if (oengusImportStatus.value.exporting) { + throw new Error('Already exporting schedule'); + } + nodecg.log.info('[Oengus Update] Started exporting schedule'); + if (!data) { + throw new Error('Did not receive schedule lines from parameters correctly'); + } + if (!data.marathonShort) { + throw new Error('Did not receive marathon name from parameters correctly'); + } + const getResponse = await get(`/marathons/${data.marathonShort}/schedule?withCustomData=true`); + if (!isOengusSchedule(getResponse.body)) { + throw new Error('Did not receive schedule data from Oengus'); + } + var oengusSchedule = getResponse.body; + + data.lines.forEach(lineWithUpdatedValues => { + var currentRun = oengusSchedule.lines.find(line => line.id === lineWithUpdatedValues.id); + if (!currentRun) { + throw new Error(`Did not find run with oengus id:${lineWithUpdatedValues.id} `); + } + Object.assign(currentRun, lineWithUpdatedValues); + }); + + oengusSchedule.lines.forEach(line => { + var lineAsAny = line as any; + lineAsAny.customData = line.customDataDTO; + }); + + await put(`/marathons/${data.marathonShort}/schedule`, oengusSchedule); + nodecg.log.info('[Oengus Update] Successfully updated schedule to Oengus'); +} diff --git a/src/types/Events.d.ts b/src/types/Events.d.ts index 6ddb3798..63f953f9 100644 --- a/src/types/Events.d.ts +++ b/src/types/Events.d.ts @@ -1,6 +1,7 @@ /* eslint-disable max-len */ import { BodyData, NeedleHttpVerbs, NeedleResponse } from 'needle'; +import { OengusLine } from './Oengus'; import { RunData } from './RunData'; import { Speedruncom } from './Speedruncom'; import { CommercialDuration } from './Twitch'; @@ -42,6 +43,10 @@ export interface SendMessageArgsMap { // Speedrun.com srcomSearchForUserDataMultiple: { type: 'name' | 'srcom' | 'twitch' | 'twitter', val: (string | undefined | null) }[]; + + // Oengus + importOengusSchedule: { marathonShort: string, useJapanese: boolean }; + updateOengusSchedule: { marathonShort: string, lines: Partial[]}; } export interface SendMessageReturnMap { @@ -76,6 +81,10 @@ export interface SendMessageReturnMap { // Speedrun.com srcomSearchForUserDataMultiple: Speedruncom.UserData | undefined; + + // Oengus + importOengusSchedule: void; + updateOengusSchedule: void; } export type SendMessageAck = HandledSendMessageAck | UnhandledSendMessageAck; diff --git a/src/types/schemas/configschema.d.ts b/src/types/schemas/configschema.d.ts index c9431d29..5a3dc481 100644 --- a/src/types/schemas/configschema.d.ts +++ b/src/types/schemas/configschema.d.ts @@ -36,6 +36,7 @@ export interface Configschema { horaro: Horaro; oengus: { defaultMarathon: string; + token?: string; useJapanese: boolean; ignoreGamesWhileImporting?: IgnoreGamesWhileImporting; disableSpeedrunComLookup: boolean; diff --git a/src/types/schemas/horaroImportStatus.d.ts b/src/types/schemas/horaroImportStatus.d.ts index 700b1283..18d0ac48 100644 --- a/src/types/schemas/horaroImportStatus.d.ts +++ b/src/types/schemas/horaroImportStatus.d.ts @@ -7,6 +7,7 @@ export interface HoraroImportStatus { importing?: boolean; + exporting?: boolean; item?: number; total?: number; } diff --git a/src/types/schemas/oengusImportStatus.d.ts b/src/types/schemas/oengusImportStatus.d.ts index ea83b15d..2f359a40 100644 --- a/src/types/schemas/oengusImportStatus.d.ts +++ b/src/types/schemas/oengusImportStatus.d.ts @@ -7,6 +7,7 @@ export interface OengusImportStatus { importing?: boolean; + exporting?: boolean; item?: number; total?: number; }