diff --git a/services/bot/package.json b/services/bot/package.json index 0159288..2ff2d4a 100644 --- a/services/bot/package.json +++ b/services/bot/package.json @@ -22,10 +22,12 @@ "license": "Unlicense", "dependencies": { "@discordeno/bot": "19.0.0-next.746f0a9", + "@discordeno/rest": "19.0.0-next.b3a8c86", "@types/node": "^22.2.0", "@types/qs": "^6.9.15", "date-fns": "^3.6.0", "dd-cache-proxy": "^2.1.1", + "discordeno": "19.0.0-next.b3a8c86", "dotenv": "^16.4.5", "graphile-config": "0.0.1-beta.9", "graphile-worker": "^0.16.6", diff --git a/services/bot/pnpm-lock.yaml b/services/bot/pnpm-lock.yaml index e1f512b..744537d 100644 --- a/services/bot/pnpm-lock.yaml +++ b/services/bot/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@discordeno/bot': specifier: 19.0.0-next.746f0a9 version: 19.0.0-next.746f0a9 + '@discordeno/rest': + specifier: 19.0.0-next.b3a8c86 + version: 19.0.0-next.b3a8c86 '@types/node': specifier: ^22.2.0 version: 22.2.0 @@ -23,6 +26,9 @@ importers: dd-cache-proxy: specifier: ^2.1.1 version: 2.1.1(@discordeno/bot@19.0.0-next.746f0a9) + discordeno: + specifier: 19.0.0-next.b3a8c86 + version: 19.0.0-next.b3a8c86 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -79,18 +85,33 @@ packages: '@discordeno/bot@19.0.0-next.746f0a9': resolution: {integrity: sha512-M0BqdbGcJSHr7Nmxw/okFtkKZ9mMM0yUHBbB0XApxFxBRt68I1JhVbdFMwDkVAutargEr8BVDSt5SqUVpMnbrQ==} + '@discordeno/bot@19.0.0-next.b3a8c86': + resolution: {integrity: sha512-Jm7sEmxR0MyK3kwe8CVHDQfAXpbkkPLER2nGuS2nn2e/0AXQKCCgJRMNmorJ+ZzcSw+slrw4hHkVZMBv7JML9Q==} + '@discordeno/gateway@19.0.0-next.746f0a9': resolution: {integrity: sha512-IvXISmDVC8bGUreR/wo4hYoH4p8w5YanDDMpdO+ex6DTlsA2AgvpzzIHeshfOZNAupdr4spp4TDxziXfq1skhQ==} + '@discordeno/gateway@19.0.0-next.b3a8c86': + resolution: {integrity: sha512-FGQvHcJFCyC4WCdgvRo3vAMoyvDpTkbHN4DZlEDNhN6uBMsWsd1xbjWPsJTdWGVUvHQd4+5HOv44wPhO02jytg==} + '@discordeno/rest@19.0.0-next.746f0a9': resolution: {integrity: sha512-qM0d/MFhzC2TWDclwiVL4Tt/37C26gjCUgb0x9mwnQsetJvsYmd+nzQI6SCkzKjsn/esWCtjSSHFQ7z6bdURpw==} + '@discordeno/rest@19.0.0-next.b3a8c86': + resolution: {integrity: sha512-nhhHSzibxOwxFAckgcuU8nGj+AGo3IRb1qiCcTE4gQStjqmV3ZewoVRzaRGgoY4t5ld5oeDODplXY5tloobkiw==} + '@discordeno/types@19.0.0-next.746f0a9': resolution: {integrity: sha512-v/nG0vIFukJzFqAzABat2eGV3a7jTDQzbPkj1yoWaFfcB6pxlF44XJ4nsLLsvWj7oRH8eR97yMa2BT697Rs5JA==} + '@discordeno/types@19.0.0-next.b3a8c86': + resolution: {integrity: sha512-1uUOpfBN3a8zDYOyL61qvWhG+NL4KMjcun7XFg7CB6wjGubUv4Uc2QXKG7SkSfNmtYzxetrrx1kyoSxkbEHLOw==} + '@discordeno/utils@19.0.0-next.746f0a9': resolution: {integrity: sha512-UY5GataakuY0yc4SN5qJLexUbTc5y293G3gNAWSaOjaZivEytcdxD4xgeqjNj9c4eN57B3Lfzus6tFZHXwXNOA==} + '@discordeno/utils@19.0.0-next.b3a8c86': + resolution: {integrity: sha512-W4SDymUvevihQZry9hB4lzCUNSz5QwqAzdT+3VKswjARgmQQnOjZdJ1w9rX/xoMB5FgS8Vhk6er4ABULIwPi6g==} + '@esbuild/aix-ppc64@0.23.0': resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} engines: {node: '>=18'} @@ -366,6 +387,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -410,6 +435,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + discordeno@19.0.0-next.b3a8c86: + resolution: {integrity: sha512-n0IF5nlP5ECPaNsBHpuvmqgmV4AFlNr7DmbTNRX5jnh01b5z6YvSMmzAT1Um3YAIb/JI3krvJjg6+C1Mxy2AEA==} + hasBin: true + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -449,6 +478,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -563,6 +596,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -596,6 +633,14 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -604,6 +649,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -799,6 +848,10 @@ packages: undici-types@6.13.0: resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -842,6 +895,10 @@ packages: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + snapshots: '@babel/code-frame@7.24.7': @@ -872,6 +929,16 @@ snapshots: - bufferutil - utf-8-validate + '@discordeno/bot@19.0.0-next.b3a8c86': + dependencies: + '@discordeno/gateway': 19.0.0-next.b3a8c86 + '@discordeno/rest': 19.0.0-next.b3a8c86 + '@discordeno/types': 19.0.0-next.b3a8c86 + '@discordeno/utils': 19.0.0-next.b3a8c86 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@discordeno/gateway@19.0.0-next.746f0a9': dependencies: '@discordeno/types': 19.0.0-next.746f0a9 @@ -881,17 +948,37 @@ snapshots: - bufferutil - utf-8-validate + '@discordeno/gateway@19.0.0-next.b3a8c86': + dependencies: + '@discordeno/types': 19.0.0-next.b3a8c86 + '@discordeno/utils': 19.0.0-next.b3a8c86 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@discordeno/rest@19.0.0-next.746f0a9': dependencies: '@discordeno/types': 19.0.0-next.746f0a9 '@discordeno/utils': 19.0.0-next.746f0a9 + '@discordeno/rest@19.0.0-next.b3a8c86': + dependencies: + '@discordeno/types': 19.0.0-next.b3a8c86 + '@discordeno/utils': 19.0.0-next.b3a8c86 + '@discordeno/types@19.0.0-next.746f0a9': {} + '@discordeno/types@19.0.0-next.b3a8c86': {} + '@discordeno/utils@19.0.0-next.746f0a9': dependencies: '@discordeno/types': 19.0.0-next.746f0a9 + '@discordeno/utils@19.0.0-next.b3a8c86': + dependencies: + '@discordeno/types': 19.0.0-next.b3a8c86 + '@esbuild/aix-ppc64@0.23.0': optional: true @@ -1100,6 +1187,8 @@ snapshots: color-name@1.1.4: {} + commander@12.1.0: {} + concat-map@0.0.1: {} cosmiconfig@8.3.6(typescript@5.5.4): @@ -1135,6 +1224,20 @@ snapshots: diff@4.0.2: {} + discordeno@19.0.0-next.b3a8c86: + dependencies: + '@discordeno/bot': 19.0.0-next.b3a8c86 + '@discordeno/gateway': 19.0.0-next.b3a8c86 + '@discordeno/rest': 19.0.0-next.b3a8c86 + '@discordeno/types': 19.0.0-next.b3a8c86 + '@discordeno/utils': 19.0.0-next.b3a8c86 + commander: 12.1.0 + find-up: 7.0.0 + typescript: 5.5.4 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dotenv@16.4.5: {} emoji-regex@8.0.0: {} @@ -1189,6 +1292,12 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -1303,6 +1412,10 @@ snapshots: lines-and-columns@1.2.4: {} + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + make-error@1.3.6: {} minimatch@3.1.2: @@ -1338,6 +1451,14 @@ snapshots: obuf@1.1.2: {} + p-limit@4.0.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -1349,6 +1470,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + path-exists@5.0.0: {} + path-type@4.0.0: {} pg-cloudflare@1.1.1: @@ -1525,6 +1648,8 @@ snapshots: undici-types@6.13.0: {} + unicorn-magic@0.1.0: {} + v8-compile-cache-lib@3.0.1: {} web-streams-polyfill@3.3.3: {} @@ -1554,3 +1679,5 @@ snapshots: yargs-parser: 21.1.1 yn@3.1.1: {} + + yocto-queue@1.1.1: {} diff --git a/services/bot/src/bot.ts b/services/bot/src/bot.ts index 983bdd6..445684c 100644 --- a/services/bot/src/bot.ts +++ b/services/bot/src/bot.ts @@ -2,12 +2,6 @@ import { createBot, createGatewayManager, createRestManager, Intents, type Bot } import { createProxyCache, } from 'dd-cache-proxy'; import { configs } from './config.ts' -// not sure I need this. -// @see https://github.com/discordeno/discordeno/blob/352887c215cc9d93d7f1fa9c8589e66f47ffb3ea/packages/bot/src/bot.ts#L74 -// const getSessionInfoHandler = async () => { -// return await bot.rest.getGatewayBot() -// } - export const bot = createProxyCache( createBot({ token: configs.token, @@ -37,6 +31,7 @@ bot.transformers.desiredProperties.interaction.guildId = true bot.transformers.desiredProperties.interaction.member = true bot.transformers.desiredProperties.interaction.message = true bot.transformers.desiredProperties.interaction.user = true +bot.transformers.desiredProperties.interaction.channelId = true bot.transformers.desiredProperties.message.activity = true bot.transformers.desiredProperties.message.id = true diff --git a/services/bot/src/commands/record.ts b/services/bot/src/commands/record.ts index 8ad1e53..74e901e 100644 --- a/services/bot/src/commands/record.ts +++ b/services/bot/src/commands/record.ts @@ -1,16 +1,19 @@ import { + type Interaction, + type InteractionCallbackData, + type CreateMessageOptions, + MessageFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, - type Interaction, EmbedsBuilder, - type InteractionCallbackData, logger, } from '@discordeno/bot' +import { bot } from '../bot.ts' +import { rbacAllow } from '../middlewares/rbac.ts' import { createCommand } from '../commands.ts' import { configs } from '../config.ts' import type { Stream } from '@futureporn/types' - async function createStreamInDatabase(url: string, discordMessageId: string) { const streamPayload = { url, @@ -30,7 +33,7 @@ async function createStreamInDatabase(url: string, discordMessageId: string) { const status = res.status const statusText = res.statusText const body = await res.text() - const msg = `failed to create stream in database. status=${status}, statusText=${statusText}, body=${body}` + const msg = `Failed to create stream in database. status=${status}, statusText=${statusText}, body=${body}` console.error(msg) throw new Error(msg) } @@ -92,10 +95,14 @@ createCommand({ }, ], async execute(interaction: Interaction) { - logger.info('logger.info hello? record command is running now`)') + // logger.info('record command is running now`)') + // logger.info(interaction) await interaction.defer() + try { + // logger.info('we are at the top of the record command. start of the try{} block. we are about to run rbacAllow.') + await rbacAllow(['admin', 'patron', 'moderator', 'testAdmin'], interaction) // The url can come from one of two places. // interaction.data.options, or interaction.message?.embeds let url @@ -129,11 +136,29 @@ createCommand({ throw new Error(msg) } - // @todo create stream in db const stream = await createStreamInDatabase(url, message.id.toString()) logger.info(stream) } catch (e) { - await interaction.edit(`Record failed due to the following error.\n${e}`) + const message = `Record failed due to the following error.\n${e}` + logger.error(message) + // acknowledged interactions cannot be responded to. Instead, the message gets sent as a followup, which is a public message. + // we want the message to be ephemeral/isPrivate, so we send our own message flagged as ephemeral. + // Nevermind, the following flagged message doesn't really work because I don't think regular messages can be ephemeral. (only interaction responses can be ephemeral) + // + // if (interaction.acknowledged) { + // const messageOptions: CreateMessageOptions = { + // content: message, + // flags: MessageFlags.Ephemeral + // } + logger.info(`User ${interaction.user.username} (${interaction.user.id})`) + // const dmChannel = await bot.rest.getDmChannel() + // logger.info(`dmChannel as follows`) + // logger.info(dmChannel) + // await bot.rest.sendMessage(dmChannel.id, messageOptions) + // } else { + await interaction.edit(message) + // } + } diff --git a/services/bot/src/components/README.md b/services/bot/src/components/README.md deleted file mode 100644 index 3dce41a..0000000 --- a/services/bot/src/components/README.md +++ /dev/null @@ -1 +0,0 @@ -Handlers for Message Component interactions such as button presses \ No newline at end of file diff --git a/services/bot/src/events/interactionCreate.ts b/services/bot/src/events/interactionCreate.ts index a9227c4..b5a7bf0 100644 --- a/services/bot/src/events/interactionCreate.ts +++ b/services/bot/src/events/interactionCreate.ts @@ -11,7 +11,8 @@ const execCommand = async function execCommand(command: Command, interaction: In try { await command.execute(interaction, options) } catch (error) { - bot.logger.error(`There was an error running the ${command.name} command.`, error) + bot.logger.error(`There was an error running the ${command.name} command.`) + bot.logger.error(error) } } diff --git a/services/bot/src/index.ts b/services/bot/src/index.ts index 0022718..688c79b 100644 --- a/services/bot/src/index.ts +++ b/services/bot/src/index.ts @@ -1,5 +1,4 @@ -import update_discord_message from './tasks/update_discord_message.js' import { type WorkerUtils, type RunnerOptions, run } from 'graphile-worker' import { bot } from './bot.ts' import type { Interaction } from '@discordeno/bot' @@ -26,16 +25,8 @@ async function setupGraphileWorker() { taskDirectory: join(__dirname, 'tasks') }, }; - // console.log('worker preset as follows') - // console.log(preset) const runnerOptions: RunnerOptions = { preset - // concurrency: 3, - // connectionString: configs.connectionString, - // taskDirectory: join(__dirname, 'tasks'), - // taskList: { - // 'update_discord_message': update_discord_message - // } } const runner = await run(runnerOptions) diff --git a/services/bot/src/middlewares/rbac.ts b/services/bot/src/middlewares/rbac.ts new file mode 100644 index 0000000..1802bf7 --- /dev/null +++ b/services/bot/src/middlewares/rbac.ts @@ -0,0 +1,28 @@ +import type { Interaction } from "@discordeno/bot"; +import { logger } from "discordeno"; + + +const roleMap = new Map([ + ['patron', BigInt('1084677850180882555')], + ['admin', BigInt('1084928253699039282')], + ['moderator', BigInt('1084935803324612729')], + ['testAdmin', BigInt('1275625364382552169')] +]); + +export async function rbacAllow (roleNames: string[], interaction: Interaction) { + const roleIds = roleNames.map((role) => roleMap.get(role)).filter((roleId): roleId is bigint => roleId !== undefined); + + if (!interaction.member?.roles || interaction.member.roles.length < 1 ) { + throw new Error(`User has no roles.`) + } + logger.info(`rbacAllow ${roleNames.join(',')} begin! the user responsible for the interaction has the following roles-- ${interaction.member.roles.join(',')}`) + + const hasRequiredRole = roleIds.some(roleId => interaction.member!.roles.includes(roleId)); + + if (!hasRequiredRole) { + throw new Error(`User lacks the role to run this command. One of the following roles is required: ${roleNames.join(', ')}`) + } + + logger.info(`rbacAllow ${roleNames.join(',')} success!`) + return +} \ No newline at end of file diff --git a/services/bot/src/tasks/README.md b/services/bot/src/tasks/README.md index 64cc26b..75e5ee9 100644 --- a/services/bot/src/tasks/README.md +++ b/services/bot/src/tasks/README.md @@ -1 +1,14 @@ -task names uses underscores because graphile_worker expects them to be that way because graphile_worker interfaces with Postgresql which uses lowercase and numberscores. \ No newline at end of file +Task names uses underscores because graphile_worker expects them to be that way because graphile_worker interfaces with Postgresql which uses lowercase and numberscores. + + +## Add job via SQL + +```sql +SELECT graphile_worker.add_job('process_stream_recording', max_attempts := 3); +``` + +## complete/cancel a job via SQL + +```sql +SELECT * FROM graphile_worker.complete_jobs(ARRAY[7, 99, 38674, ...]); +``` \ No newline at end of file diff --git a/services/bot/src/tasks/update_discord_message.ts b/services/bot/src/tasks/update_discord_message.ts index 77407dc..ca2c9ff 100644 --- a/services/bot/src/tasks/update_discord_message.ts +++ b/services/bot/src/tasks/update_discord_message.ts @@ -179,10 +179,10 @@ function getButtonRow(streamStatus: Status): ActionRow[] { if (streamStatus === 'pending_recording' || streamStatus === 'recording') { components.push(cancelButton) - components.push(processButton) // @todo this is only for testing. normally the process button is hidden until recording completes. - components.push(yeahButton) // @todo this is only for testing. normally the process button is hidden until recording completes. + components.push(yeahButton) } else if (streamStatus === 'aborted') { components.push(retryButton) + components.push(processButton) // @todo this is only for testing. normally the process button is hidden if the stream was aborted. } else if (streamStatus === 'finished') { components.push(processButton) } else { diff --git a/services/factory/src/tasks/combine_video_segments.ts b/services/factory/src/tasks/combine_video_segments.ts index 2cbbb32..4361af9 100644 --- a/services/factory/src/tasks/combine_video_segments.ts +++ b/services/factory/src/tasks/combine_video_segments.ts @@ -168,16 +168,6 @@ const getS3ParallelUpload = async function ({ -const createStrapiB2File = async function (): Promise { - -} - - -const createStrapiStream = async function (): Promise { - -} - - export const combine_video_segments: Task = async function (payload: unknown, helpers: Helpers) { // helpers.logger.info('the following is the raw Task payload') @@ -196,7 +186,7 @@ export const combine_video_segments: Task = async function (payload: unknown, he * * VOD * * Stream(?) */ - + try { diff --git a/services/factory/src/tasks/process_recording.ts b/services/factory/src/tasks/process_recording.ts new file mode 100644 index 0000000..6ff9782 --- /dev/null +++ b/services/factory/src/tasks/process_recording.ts @@ -0,0 +1,83 @@ +import type { Helpers, Task } from "graphile-worker" +import { configs } from "../config" +import type { Stream } from '@futureporn/types' + +interface Payload { + stream_id: string; +} + +function assertPayload(payload: any): asserts payload is Payload { + if (typeof payload !== "object" || !payload) throw new Error("invalid payload (it must be an object)"); + if (typeof payload.stream_id !== "string") throw new Error("payload.stream_id was not a string"); +} + + +async function getStreamFromDatabase(streamId: string, helpers: Helpers) { + const url = `${configs.postgrestUrl}/streams?select=*,segments(*)&id=eq.${streamId}` + try { + const res = await fetch(url) + if (!res.ok) { + throw new Error(`failed fetching stream ${streamId}. status=${res.status}, statusText=${res.statusText}`) + } + const body = await res.json() as Stream[] + if (!body[0]) throw new Error('body[0] was expected to be Stream data, but it was either null or undefined.'); + return body[0]; + } catch (e) { + helpers.logger.error(`encountered an error during getStreamFromDatabase()`) + if (e instanceof Error) { + helpers.logger.error(e.message) + } else { + helpers.logger.error(JSON.stringify(e)) + } + return null + } +} + + +/** + * + * # process_recording + * + * We just recorded a livestream. Now what? + * process_recording takes a /streams record and runs a bunch of processes to get it ready for publishing. + * + * The following are graphile-worker tasks which process_recording is responsible for adding to the job queue. + * Some of these tasks are run conditionally based on the structure of the /streams record. + * For example, combine_video_segments is only useful on a stream recording which ended up with multiple segments. + * + * - combine_video_segments + * - generate_thumbnail + * - queue_moderator_review + * - create_mux_asset + * - create_torrent + * + * Some of the above Tasks are dependent on others. generate_thumbnail and everything following it depends on combine_video_segments. + * graphile-worker doesn't have support for dependent tasks, + * thus our solution is to run process_recording as many times as needed, each time adding as many parallel tasks as possible. + * + * For the first run, we add only combine_video_segments, which itself will add process_recording when it's done. + * The second run can tell that combine_video_segments has successfully completed it's task, so it doesn't run it a second time. + * generate_thumbnail runs next, after which it itself adds process_recording to the work queue. + * + * On the third run, combine_video_segments and generate_thumbnail are skipped due to idempotency. + * The three next tasks are added simultaneously for parallel execution-- queue_moderator_review, create_mux_asset, and create_torrent. + * + * + */ +export const process_recording: Task = async function (payload: unknown, helpers: Helpers) { + assertPayload(payload) + const { stream_id } = payload + helpers.logger.info(`process_recording task has begun for stream_id=${stream_id}`) + + const stream = await getStreamFromDatabase(stream_id, helpers) + if (!stream) throw new Error(`failed to get stream from database.`); + if (!stream.segments) throw new Error(`stream ${stream_id} fetched from database lacked any segments.`); + if (stream.segments.length > 1) { + const s3_manifest = stream.segments.map((segment) => ({ key: segment.s3_key })) + helpers.addJob('combine_video_segments', { s3_manifest }) + } + + + +} +