diff --git a/services/bot/src/bot.ts b/services/bot/src/bot.ts index 27d038f..58d0f1f 100644 --- a/services/bot/src/bot.ts +++ b/services/bot/src/bot.ts @@ -6,7 +6,7 @@ import { configs } from './config.ts' export const bot = createProxyCache( createBot({ token: configs.token, - intents: Intents.Guilds + intents: Intents.Guilds | Intents.GuildMessages }), { desiredProps: { diff --git a/services/bot/src/commands/record.ts b/services/bot/src/commands/record.ts index e6d9264..ff45eea 100644 --- a/services/bot/src/commands/record.ts +++ b/services/bot/src/commands/record.ts @@ -69,7 +69,7 @@ createCommand({ { name: 'Filesize', value: '0 bytes', inline: true}, { name: 'URL', value: url, inline: false } ]) - .setColor('#33eb23') + .setColor('#808080') const response: InteractionCallbackData = { embeds } const message = await interaction.edit(response) diff --git a/services/bot/src/config.ts b/services/bot/src/config.ts index 56a1619..6985970 100644 --- a/services/bot/src/config.ts +++ b/services/bot/src/config.ts @@ -1,18 +1,35 @@ + +if (!process.env.WORKER_CONNECTION_STRING) throw new Error("WORKER_CONNECTION_STRING was missing from env"); if (!process.env.POSTGREST_URL) throw new Error('Missing POSTGREST_URL env var'); if (!process.env.DISCORD_TOKEN) throw new Error('Missing DISCORD_TOKEN env var'); +if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env"); +if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env"); if (!process.env.AUTOMATION_USER_JWT) throw new Error('Missing AUTOMATION_USER_JWT env var'); const token = process.env.DISCORD_TOKEN! const postgrestUrl = process.env.POSTGREST_URL! +const discordChannelId = process.env.DISCORD_CHANNEL_ID! +const discordGuildId = process.env.DISCORD_GUILD_ID! const automationUserJwt = process.env.AUTOMATION_USER_JWT! +const connectionString = process.env.WORKER_CONNECTION_STRING! + +console.log(`hello i am configs and configs.connectionString=${connectionString}`) -export const configs: Config = { - token, - postgrestUrl, - automationUserJwt, -} export interface Config { token: string; postgrestUrl: string; automationUserJwt: string; + discordGuildId: string; + discordChannelId: string; + connectionString: string; } + + +export const configs: Config = { + token, + postgrestUrl, + automationUserJwt, + discordGuildId, + discordChannelId, + connectionString, +} \ No newline at end of file diff --git a/services/bot/src/index.ts b/services/bot/src/index.ts index f189357..a5832d7 100644 --- a/services/bot/src/index.ts +++ b/services/bot/src/index.ts @@ -1,14 +1,12 @@ -import 'dotenv/config' -// import loadCommands from './loadCommands.js' -// import deployCommands from './deployCommands.js' -// import loadEvents from './loadEvents.js' -// import updateDiscordMessage from './tasks/update_discord_message.js' -import { type WorkerUtils } from 'graphile-worker' + +import updateDiscordMessage 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' import { importDirectory } from './utils/loader.ts' import { join, dirname } from 'node:path' import { fileURLToPath } from 'url'; +import { configs } from './config.ts' const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -18,43 +16,36 @@ export interface ExecuteArguments { workerUtils: WorkerUtils; } -if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`); -if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from env"); -if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env"); -if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env"); -if (!process.env.WORKER_CONNECTION_STRING) throw new Error("WORKER_CONNECTION_STRING was missing from env"); -const preset: GraphileConfig.Preset = { - worker: { - connectionString: process.env.WORKER_CONNECTION_STRING, - concurrentJobs: 3, - fileExtensions: [".js", ".ts"] - }, -}; - -// async function setupGraphileWorker() { -// const runnerOptions: RunnerOptions = { -// preset, -// taskList: { -// 'updateDiscordMessage': updateDiscordMessage -// } -// } +async function setupGraphileWorker() { + const preset: GraphileConfig.Preset = { + worker: { + connectionString: configs.connectionString, + concurrentJobs: 3, + fileExtensions: [".js", ".ts"], + 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': updateDiscordMessage + // } + } -// const runner = await run(runnerOptions) -// if (!runner) throw new Error('failed to initialize graphile worker'); -// await runner.promise -// } + const runner = await run(runnerOptions) + if (!runner) throw new Error('failed to initialize graphile worker'); + await runner.promise +} -// async function setupWorkerUtils() { -// const workerUtils = await makeWorkerUtils({ -// preset -// }); -// await workerUtils.migrate() -// return workerUtils -// } -async function main() { +async function setupBot() { bot.logger.info('Starting @futureporn/bot.') @@ -64,16 +55,15 @@ async function main() { bot.logger.info('Loading events...') await importDirectory(join(__dirname, './events')) - - // const commands = await loadCommands() - // if (!commands) throw new Error('there were no commands available to be loaded.'); - // await deployCommands(commands.map((c) => c.data.toJSON())) - // console.log(`${commands.length} commands deployed: ${commands.map((c) => c.data.name).join(', ')}`) - // const workerUtils = await setupWorkerUtils() - // setupGraphileWorker() await bot.start() } + +async function main() { + await setupBot() + await setupGraphileWorker() +} + main().catch((e) => { console.error("error during main() function") console.error(e) diff --git a/services/bot/src/tasks/update_discord_message.ts b/services/bot/src/tasks/update_discord_message.ts index 0e711d7..4d404c8 100644 --- a/services/bot/src/tasks/update_discord_message.ts +++ b/services/bot/src/tasks/update_discord_message.ts @@ -3,9 +3,19 @@ import type { RecordingState } from '@futureporn/types' import { type Task, type Helpers } from 'graphile-worker' import { add } from 'date-fns' import prettyBytes from 'pretty-bytes' -import { EmbedsBuilder, type Component } from '@discordeno/bot' - - +import { + EmbedsBuilder, + ButtonStyles, + type ActionRow, + MessageComponentTypes, + type ButtonComponent, + type InputTextComponent, + type EditMessage, + type Message, + type Embed +} from '@discordeno/bot' +import { bot } from '../bot.ts' +import { configs } from '../config.ts' interface Payload { record_id: number; @@ -19,11 +29,6 @@ function assertPayload(payload: any): asserts payload is Payload { } -if (!process.env.AUTOMATION_USER_JWT) throw new Error(`AUTOMATION_USER_JWT was missing from env`); -if (!process.env.DISCORD_TOKEN) throw new Error("DISCORD_TOKEN was missing from env"); -if (!process.env.DISCORD_CHANNEL_ID) throw new Error("DISCORD_CHANNEL_ID was missing from env"); -if (!process.env.DISCORD_GUILD_ID) throw new Error("DISCORD_GUILD_ID was missing from env"); - async function editDiscordMessage({ helpers, recordingState, discordMessageId, url, fileSize, recordId }: { recordId: number, fileSize: number, url: string, helpers: Helpers, recordingState: RecordingState, discordMessageId: string }) { @@ -34,46 +39,52 @@ async function editDiscordMessage({ helpers, recordingState, discordMessageId, u // const { captureJobId } = job.data helpers.logger.info(`editDiscordMessage has begun with discordMessageId=${discordMessageId}, state=${recordingState}`) - - // create a discord.js client - const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], - }); - - // Log in to Discord with your client's token - client.login(process.env.DISCORD_TOKEN); - - const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID!) as Guild - if (!guild) throw new Error('guild was undefined'); + // const guild = await bot.cache.guilds.get(BigInt(configs.discordGuildId)) + // const channel = guild?.channels.get(BigInt(configs.discordChannelId)) - helpers.logger.info('here is the guild as follows') - helpers.logger.info(guild.toString()) - helpers.logger.info(`fetching discord channel id=${process.env.DISCORD_CHANNEL_ID} from discord guild`) - const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID!) as TextChannel - if (!channel) throw new Error(`discord channel was undefined`); + // // const channel = await bot.cache.channels.get() + // console.log('channel as follows') + // console.log(channel) + + const channelId = BigInt(configs.discordChannelId) + const updatedMessage: EditMessage = { + embeds: getStatusEmbed({ recordingState, fileSize, recordId, url }), + } + bot.helpers.editMessage(channelId, discordMessageId, updatedMessage) - const message = await channel.messages.fetch(discordMessageId) - helpers.logger.info(`discordMessageId=${discordMessageId}`) - helpers.logger.info(message as any) + // channel. + + // const guild = await client.guilds.fetch(process.env.DISCORD_GUILD_ID!) as Guild + // if (!guild) throw new Error('guild was undefined'); + + // helpers.logger.info('here is the guild as follows') + // helpers.logger.info(guild.toString()) + // helpers.logger.info(`fetching discord channel id=${process.env.DISCORD_CHANNEL_ID} from discord guild`) + // const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID!) as TextChannel + // if (!channel) throw new Error(`discord channel was undefined`); - const statusEmbed = getStatusEmbed({ recordId, recordingState, fileSize, url }) - const buttonRow = getButtonRow(recordingState) + // const message = await channel.messages.fetch(discordMessageId) + // helpers.logger.info(`discordMessageId=${discordMessageId}`) + // helpers.logger.info(message as any) + + // const statusEmbed = getStatusEmbed({ recordId, recordingState, fileSize, url }) + // const buttonRow = getButtonRow(recordingState) - // const embed = new EmbedBuilder().setTitle('Attachments'); + // // const embed = new EmbedBuilder().setTitle('Attachments'); - const updatedMessage = { - embeds: [ - statusEmbed - ], - components: [ - buttonRow - ] - }; + // const updatedMessage = { + // embeds: [ + // statusEmbed + // ], + // components: [ + // buttonRow + // ] + // }; - message.edit(updatedMessage) + // message.edit(updatedMessage) } @@ -121,14 +132,15 @@ export const updateDiscordMessage: Task = async function (payload, helpers: Help } } + function getStatusEmbed({ recordingState, recordId, fileSize, url }: { fileSize: number, recordingState: RecordingState, recordId: number, url: string }) { const embeds = new EmbedsBuilder() .setTitle(`Record ${recordId}`) .setFields([ - { name: 'Status', value: 'Pending', inline: true }, - { name: 'Filesize', value: `${fileSize} bytes (${prettyBytes(fileSize)})`, inline: true }, + { name: 'Status', value: recordingState.charAt(0).toUpperCase()+recordingState.slice(1), inline: true }, + { name: 'Filesize', value: prettyBytes(fileSize), inline: true }, { name: 'URL', value: url, inline: false }, ]) if (recordingState === 'pending') { @@ -157,72 +169,59 @@ function getStatusEmbed({ -function getButtonRow(state: RecordingState) { - - const button = new Component() - .setType("BUTTON") - - // // Button with raw types - // const button2 = new Component() - // .setType(2) - // .setStyle(4) - // .setLabel("DO NOT CLICK") - // .setCustomId("12345") - // .toJSON(); - - // const actionRow = new Component() - // .setType("ACTION_ROW") - // .setComponents(button, button2) - // .toJSON(); - - // return actionRow - - // Message to send - // const messageOptions = { content: "hello", components: [actionRow] }; - - // await client.helpers.sendMessage(channelId, messageOptions); // You can also use the Message Structure - - if (state === 'pending') { - button - .setCustomId('stop') - .setLabel('Cancel') - .setEmoji('❌') - .setStyle('DANGER') - } else if (state === 'recording') { - button - .setCustomId('stop') - .setLabel('Stop Recording') - .setEmoji('🛑') - .setStyle('DANGER') +function getButtonRow(state: RecordingState): ActionRow { + const components: ButtonComponent[] = [] + + if (state === 'pending' || state === 'recording') { + const stopButton: ButtonComponent = { + type: MessageComponentTypes.Button, + customId: 'stop', + label: 'Cancel', + style: ButtonStyles.Danger + } + components.push(stopButton) } else if (state === 'aborted') { - button - .setCustomId('retry') - .setLabel('Retry Recording') - .setEmoji('🔄') - .setStyle('SUCCESS') + const retryButton: ButtonComponent = { + type: MessageComponentTypes.Button, + customId: 'retry', + label: 'Retry Recording', + emoji: { + name: 'retry' + }, + style: ButtonStyles.Secondary + } + components.push(retryButton) } else if (state === 'ended') { - button - .setCustomId('download') - .setLabel('Download Recording') - .setEmoji('📥') - .setStyle('PRIMARY') + const downloadButton: ButtonComponent = { + type: MessageComponentTypes.Button, + customId: 'download', + label: 'Download Recording', + emoji: { + id: BigInt('1253191939461873756') + }, + style: ButtonStyles.Success + } + components.push(downloadButton) } else { - button - .setCustomId('unknown') - .setLabel('Unknown State') - .setEmoji('🤔') - .setStyle('SECONDARY') + const unknownButton: ButtonComponent = { + type: MessageComponentTypes.Button, + customId: 'unknown', + label: 'Unknown State', + emoji: { + name: 'thinking' + }, + style: ButtonStyles.Primary + } + components.push(unknownButton) + } + + + const actionRow: ActionRow = { + type: MessageComponentTypes.ActionRow, + components: components as [ButtonComponent] } - const actionRow = new Component - return new ActionRowBuilder() - .addComponents([ - new ButtonBuilder() - .setCustomId(id) - .setLabel(label) - .setEmoji(emoji) - .setStyle(style), - ]); + return actionRow } diff --git a/services/capture/src/Record.ts b/services/capture/src/Record.ts index f9ba29b..8cf5d53 100644 --- a/services/capture/src/Record.ts +++ b/services/capture/src/Record.ts @@ -131,6 +131,7 @@ export default class Record { parallelUploads3.on("httpUploadProgress", (progress) => { if (progress?.loaded) { + if (this.onProgress) this.onProgress(this.counter); console.log(`uploaded ${progress.loaded} bytes (${prettyBytes(progress.loaded)})`); } else { console.log(`httpUploadProgress ${JSON.stringify(progress, null, 2)}`) @@ -158,12 +159,9 @@ export default class Record { // streams setup + this.uploadStream.on('data', (data) => { this.counter += data.length - if (this.counter % (1 * 1024 * 1024) <= 1024) { - console.log(`Received ${this.counter} bytes (${prettyBytes(this.counter)})`); - if (this.onProgress) this.onProgress(this.counter) - } }) this.uploadStream.on('close', () => { console.log('[!!!] upload stream has closed') diff --git a/services/capture/src/tasks/record.ts b/services/capture/src/tasks/record.ts index 1f08088..5f35bab 100644 --- a/services/capture/src/tasks/record.ts +++ b/services/capture/src/tasks/record.ts @@ -67,7 +67,16 @@ function checkIfAborted(record: RawRecordingRecord): boolean { return (record.is_aborted) } -async function updateDatabaseRecord({recordId, recordingState, fileSize}: { recordId: number, recordingState: RecordingState, fileSize: number }): Promise { +async function updateDatabaseRecord({ + recordId, + recordingState, + fileSize +}: { + recordId: number, + recordingState: RecordingState, + fileSize: number +}): Promise { + console.log(`updating database record with recordId=${recordId}, recordingState=${recordingState}, fileSize=${fileSize}`) const payload: any = { file_size: fileSize } @@ -83,7 +92,8 @@ async function updateDatabaseRecord({recordId, recordingState, fileSize}: { reco body: JSON.stringify(payload) }) if (!res.ok) { - throw new Error(`failed to updateDatabaseRecord. status=${res.status}, statusText=${res.statusText}`); + const body = await res.text() + throw new Error(`failed to updateDatabaseRecord. status=${res.status}, statusText=${res.statusText}, body=${body}`); } const body = await res.json() as RawRecordingRecord[]; if (!body[0]) throw new Error(`failed to get a record that matched recordId=${recordId}`) diff --git a/services/migrations/migrations/00005_add-trigger-for-record-update.sql b/services/migrations/migrations/00005_add-trigger-for-record-update.sql index bce7252..5880cc4 100644 --- a/services/migrations/migrations/00005_add-trigger-for-record-update.sql +++ b/services/migrations/migrations/00005_add-trigger-for-record-update.sql @@ -5,7 +5,7 @@ CREATE FUNCTION public.tg__update_discord_message() RETURNS trigger AS $$ begin PERFORM graphile_worker.add_job('update_discord_message', json_build_object( - 'record_id', NEW.record_id + 'record_id', NEW.id ), max_attempts := 3); return NEW; end;