diff --git a/replay-handler.ts b/replay-handler.ts new file mode 100644 index 0000000..b6a9a4b --- /dev/null +++ b/replay-handler.ts @@ -0,0 +1,131 @@ +import cron from 'node-cron'; +import { WebClient } from "@slack/web-api"; + +export type QueueItem = { + md5: string, + playerName: string, + ts: string, + userId: string, + fileName: string +}; + +export const queue: QueueItem[] = []; +const rendering = new Map(); + +const client = new WebClient(process.env.BOT_TOKEN); + +export const replayRenderTask = cron.createTask('*/5 * * * * *', async (ctx) => { + if (queue.length === 0) { + replayRenderTask.stop(); + return; + } + + const item = queue.shift(); + + // This should really never happen, but typescript-language-server is screaming at me because of it. + if (!item) { + replayRenderTask.stop(); + return; + } + + const render = await ordr.sendRender({ + replay: `.replay/${item.md5}.osr`, + skin: 'default', + username: item.playerName, + showDanserLogo: false, + resolution: '1280x720', + introBGDim: 100, + inGameBGDim: 100, + breakBGDim: 100 + }); + + if (render.errorCode !== 0) { + client.reactions.add({ + channel: 'C165V7XT9', + name: 'x', + timestamp: item.ts + }); + + client.chat.postMessage({ + channel: 'C165V7XT9', + thread_ts: item.ts, + text: `:warning: *Hey <@${item.userId}>!* o!rdr refused your replay: \`${render.message}\`` + }); + + return; + } + + client.reactions.add({ + channel: 'C165V7XT9', + name: "thinkspin", + timestamp: item.ts + }); + + rendering.set(render.renderID!, item) +}); + +const socket = io('https://apis.issou.best', { + path: '/ordr/ws', + autoConnect: true +}); + +socket.on('connect', () => { + console.log('[ORDR] Connected to issou.best'); +}); + +socket.on('disconnect', (reason) => { + if (reason === "io server disconnect") { + console.log('[ORDR] issou.best disconnected client, attempting to reconnect'); + setTimeout(() => socket.connect(), 5_000); + } + + console.log('[ORDR] Disconnected from issou.best:', reason); +}); + +socket.on('render_done_json', async (render) => { + const item = rendering.get(render.renderID!); + + if (!item) return; + + client.reactions.remove({ + channel: 'C165V7XT9', + name: 'thinkspin', + timestamp: item.ts + }); + + client.chat.postMessage({ + channel: 'C165V7XT9', + thread_ts: item.ts, + reply_broadcast: true, + text: `<${render.videoUrl}|${item.fileName}>`, + unfurl_media: true + }) + + rendering.delete(render.renderID!) +}); + +socket.on('render_failed_json', async (render) => { + const item = rendering.get(render.renderID!); + + if (!item) return; + + client.reactions.remove({ + channel: 'C165V7XT9', + name: 'thinkspin', + timestamp: item.ts + }); + + client.reactions.add({ + channel: 'C165V7XT9', + name: 'x', + timestamp: item.ts + }); + + client.chat.postMessage({ + channel: 'C165V7XT9', + thread_ts: item.ts, + text: `:warning: *Hey <@${item.userId}>!* o!rdr couldn't render your replay for some reason: \`${render.errorMessage}\`` + }); + + rendering.delete(render.renderID!) +}) \ No newline at end of file diff --git a/src/events/message.ts b/src/events/message.ts new file mode 100644 index 0000000..1ad31b0 --- /dev/null +++ b/src/events/message.ts @@ -0,0 +1,68 @@ +import type { AllMiddlewareArgs, SlackEventMiddlewareArgs, StringIndexed } from "@slack/bolt"; +import fs from "node:fs/promises"; +import osr from "node-osr"; +import { queue } from "../../replay-handler"; + +// For proper type-checking + intellisense, replace "event_template" with the raw event name +export default async function Message(ctx: SlackEventMiddlewareArgs<"message"> & AllMiddlewareArgs) { + if (ctx.event.channel !== "C165V7XT9") return; + if (ctx.event.subtype !== "file_share") return; + + const msg = ctx.body.message!; + + if (!msg.files) return; + if (msg.files.length === 0) return; + + const replay = msg.files.find(file => file.name?.endsWith('.osr')); + + if (!replay) return; + + const replayData = await fetch(replay.url_private_download!, { + headers: { + 'Authorization': `Bearer ${process.env.BOT_TOKEN}` + } + }).then(res => res.arrayBuffer()); + + const replayBuffer = Buffer.from(replayData); + + const _replay = await osr.read(replayBuffer); + + if (_replay.gameMode !== 0) { + return ctx.client.chat.postEphemeral({ + channel: "C165V7XT9", + user: ctx.body.user_id!, + text: `:warning: *Hey <${ctx.body.user_id}>!* You uploaded a replay file. Unfortunately, o!rdr doesn't support replays other than :osu-standard: osu!standard replays, so I can't render your replay. Sorry!` + }); + } + + // ensure .replays folder exists + try { + const statRes = await fs.stat('.replay'); + if (!statRes.isDirectory()) throw { code: 'IS_A_FILE' } + } catch (err) { + if (err.code == 'ENOENT') { + await fs.mkdir('.replay') + } else { + return ctx.client.chat.postEphemeral({ + channel: "C165V7XT9", + user: ctx.body.user_id!, + text: `:warning: *Hey <@${ctx.body.user_id}>!* An unexpected error occured while trying to handle your replay. Contact the bot maintainer. (${err.code})` + }); + } + } + + const replayFile = fs.createWriteStream(`.replay/${_replay.replayMD5}.osr`); + + replayFile.write(replayBuffer); + replayFile.end(); + + replayFile.on('finish', () => { + queue.push({ + md5: '', + playerName: '', + ts: msg.ts, + userId: ctx.body.user_id, + fileName: replay.name.slice(0, -4) + }) + }) +} \ No newline at end of file