/record rbac
ci / build (push) Failing after 1s Details

This commit is contained in:
CJ_Clippy 2024-08-20 20:23:06 -08:00
parent f342bf9671
commit f714504cac
12 changed files with 292 additions and 38 deletions

View File

@ -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",

View File

@ -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: {}

View File

@ -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

View File

@ -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)
// }
}

View File

@ -1 +0,0 @@
Handlers for Message Component interactions such as button presses

View File

@ -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)
}
}

View File

@ -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)

View File

@ -0,0 +1,28 @@
import type { Interaction } from "@discordeno/bot";
import { logger } from "discordeno";
const roleMap = new Map<string, bigint>([
['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
}

View File

@ -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.
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, ...]);
```

View File

@ -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 {

View File

@ -168,16 +168,6 @@ const getS3ParallelUpload = async function ({
const createStrapiB2File = async function (): Promise<number> {
}
const createStrapiStream = async function (): Promise<number> {
}
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 {

View File

@ -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 })
}
}