diff --git a/.gitignore b/.gitignore index 168cbee..9b1ee42 100644 --- a/.gitignore +++ b/.gitignore @@ -173,5 +173,3 @@ dist # Finder (MacOS) folder config .DS_Store - -replay-*.osr \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index a8f06bf..fdde9d8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.ts b/index.ts index 18363b4..e771046 100644 --- a/index.ts +++ b/index.ts @@ -6,14 +6,6 @@ import "dotenv/config"; import bcrypt from "bcrypt"; import type { StaticSelectAction } from "@slack/bolt"; import { inspect } from "node:util"; -import { scheduleJob } from "node-schedule"; - -// @ts-expect-error No typings :))))))))))) -import osr from "node-osr"; -import { Client, Events } from "ordr.js"; - -import io from "socket.io-client"; -import fs from "fs"; const sql = postgres({ host: '/var/run/postgresql', @@ -32,8 +24,6 @@ const app = new App({ } }); -const ordr = new Client(process.env.ORDR_TOKEN!); - const states = new Map(); app.command("/osu-link", async (ctx) => { @@ -172,27 +162,22 @@ async function getTemporaryToken(): Promise { return data.access_token; } -async function getAccessToken(slack_id: string): Promise { +async function getAccessToken(slack_id: string): Promise { const user = await sql`SELECT * FROM links WHERE slack_id = ${slack_id}`; if (!user.length) return null - try { - const data = await fetch("https://osu.ppy.sh/oauth/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: `client_id=33126&client_secret=${encodeURIComponent(process.env.CLIENT_SECRET!)}&grant_type=refresh_token&refresh_token=${user[0].refresh_token}&scope=public` - }).then(res => res.json()); + const data = await fetch("https://osu.ppy.sh/oauth/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: `client_id=33126&client_secret=${encodeURIComponent(process.env.CLIENT_SECRET!)}&grant_type=refresh_token&refresh_token=${user[0].refresh_token}&scope=public` + }).then(res => res.json()); - await sql`UPDATE links SET refresh_token = ${data.refresh_token} WHERE slack_id = ${slack_id}`; + await sql`UPDATE links SET refresh_token = ${data.refresh_token} WHERE slack_id = ${slack_id}`; - return data.access_token; - } catch (err) { - console.error(err) - return null - } + return data.access_token; } async function sendGET(path: string, token?: string): Promise { @@ -217,26 +202,20 @@ function splitArray(arr: T[], maxElements: number): T[][] { } /// GENERATED /// -type CacheUser = { - username: string; - id: number; - slackId: string; +const cache: { + username: string, + id: number, + slackId: string, score: { - osu: number; - taiko: number; - fruits: number; - mania: number; - }; -} - -const cache: CacheUser[] = [] + osu: number, + taiko: number + fruits: number, + mania: number + } +}[] = [] const multiplayerRoundCache: any[] = []; -const sentWarningDM = { - ref: false -} - async function cacheStuff(): Promise { const token = await getTemporaryToken(); @@ -267,10 +246,10 @@ async function cacheStuff(): Promise { id: user.id, slackId: osuUsers.find(v => v[0] == user.id)![1], score: { - osu: Math.floor(user.statistics_rulesets.osu?.pp) || 0, - taiko: Math.floor(user.statistics_rulesets.taiko?.pp) || 0, - fruits: Math.floor(user.statistics_rulesets.fruits?.pp) || 0, - mania: Math.floor(user.statistics_rulesets.mania?.pp) || 0, + osu: user.statistics_rulesets.osu?.total_score || 0, + taiko: user.statistics_rulesets.taiko?.total_score || 0, + fruits: user.statistics_rulesets.fruits?.total_score || 0, + mania: user.statistics_rulesets.mania?.total_score || 0, } }))) } @@ -285,49 +264,7 @@ async function cacheStuff(): Promise { const tohken = await getAccessToken("U06TBP41C3E") as string; - if (!tohken) { - const verifCode = `OSULEADERBOARD-U06TBP41C3E-${Date.now()}`; - - states.set('U06TBP41C3E', verifCode); - - const encodedCode = await bcrypt.hash(verifCode, 10); - - await app.client.chat.postMessage({ - channel: "U06TBP41C3E", - text: "uh oh, your token seems to have expired!! multiplayer round fetching + daily challenges are disabled.", - blocks: [ - { - type: 'section', - text: { - type: "mrkdwn", - text: `uh oh, your token seems to have expired!! multiplayer round fetching + daily challenges are disabled.` - } - }, - { - type: 'section', - text: { - type: "mrkdwn", - text: `Please re-authenticate to generate it by clicking the Reauthenticate button.` - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "Reauthenticate", - "emoji": true - }, - "value": "link", - "url": `https://osu.ppy.sh/oauth/authorize?client_id=33126&redirect_uri=https://osu.haroon.hackclub.app/osu/callback&response_type=code&state=${encodeURIComponent("U06TBP41C3E:" + encodedCode)}&scope=public`, - "action_id": "link" - } - }, - ] - }) - - sentWarningDM.ref = true; - - return - }; + if (!tohken) return; const rooms = await fetch(`https://osu.ppy.sh/api/v2/rooms?category=realtime`, { headers: { @@ -543,23 +480,17 @@ app.command('/osu-profile', async (ctx) => { app.command('/osu-leaderboard', async (ctx) => { await ctx.ack(); - const cached = splitArray(cache.sort((a, b) => { + const cached = splitArray(cache, 10)[0].sort((a, b) => { return b.score.osu - a.score.osu - }), 10); + }); const users = []; - for (let i in cached[0]) { - try { - const cachedU = cached[0][i]; - const slackInfo = await ctx.client.users.info({ user: cachedU.slackId }) - const slackProfile = slackInfo.user!; + for (let i in cached) { + const cachedU = cached[i]; + const slackProfile = (await ctx.client.users.info({ user: cachedU.slackId })).user!; - users.push(`${users.length + 1}. / - ${cachedU.score.osu.toLocaleString()}`) - } catch (e) { - console.error(e) - continue; - } + users.push(`${parseInt(i) + 1}. / - ${cachedU.score.osu.toLocaleString()}`) } ctx.respond({ @@ -657,24 +588,19 @@ app.action(/change-leaderboard\|.+/, async (ctx) => { return ctx.respond({ replace_original: false, response_type: "ephemeral", text: `This leaderboard was initialised by <@${userId}>. Only they can manage it.` }) } - const selected = action.selected_option.value as "osu" | "taiko" | "fruits" | "mania"; + const selected = action.selected_option.value; - const cached = splitArray(cache.sort((a, b) => { + const cached = splitArray(cache, 10)[0].sort((a, b) => { return b.score[selected] - a.score[selected] - }), 10)[0]; + }); const users = []; for (let i in cached) { - try { - const cachedU = cached[i]; - const slackProfile = (await ctx.client.users.info({ user: cachedU.slackId })).user!; + const cachedU = cached[i]; + const slackProfile = (await ctx.client.users.info({ user: cachedU.slackId })).user!; - users.push(`${users.length + 1}. / - ${cachedU.score[selected].toLocaleString()}`) - } catch (e) { - console.error(e) - continue; - } + users.push(`${parseInt(i) + 1}. / - ${cachedU.score[selected].toLocaleString()}`) } ctx.respond({ @@ -933,41 +859,6 @@ app.command('/osu-search', async (ctx) => { }); } - const accessToken = await getAccessToken(ctx.context.userId!); - - if (!accessToken) { - const verifCode = `OSULEADERBOARD-${ctx.context.userId}-${Date.now()}`; - - states.set(ctx.context.userId, verifCode); - - const encodedCode = await bcrypt.hash(verifCode, 10); - - return ctx.respond({ - response_type: 'ephemeral', - text: `Hey <@${ctx.context.userId}>, your token has expired. Please re-authenticate to generate it by clicking the Reauthenticate button.`, - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `Hey <@${ctx.context.userId}>, your token has expired. Please re-authenticate to generate it by clicking the \`Reauthenticate\` button.` - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "Reauthenticate", - "emoji": true - }, - "value": "link", - "url": `https://osu.ppy.sh/oauth/authorize?client_id=33126&redirect_uri=https://osu.haroon.hackclub.app/osu/callback&response_type=code&state=${encodeURIComponent(ctx.context.userId + ":" + encodedCode)}&scope=public`, - "action_id": "link" - } - }, - ] - }); - } - ctx.client.views.open({ trigger_id: ctx.payload.trigger_id, view: { @@ -1166,7 +1057,7 @@ app.view("search", async (ctx) => { const set = data.beatmapsets[0]; - ctx.client.chat.postMessage({ + return ctx.client.chat.postMessage({ channel: ctx.view.private_metadata, "blocks": [ { @@ -1199,255 +1090,12 @@ receiver.router.get('*', (req, res) => { res.redirect(`https://osu.ppy.sh${req.path}`) }) -enum Mods { - EZ = "Easy", - NF = "No Fail", - HT = "Half Time", - HR = "Hard Rock", - SD = "Sudden Death", - PF = "Perfect", - DT = "Double Time", - NC = "Nightcore", - HD = "Hidden", - FI = "Fade In", - FL = "Flashlight", - RL = "Relax", - AP = "Autopilot", - SO = "Spun Out", - "1K" = "One Key", - "2K" = "Two Keys", - "3K" = "Three Keys", - "4K" = "Four Keys", - "5K" = "Five Keys", - "6K" = "Six Keys", - "7K" = "Seven Keys", - "8K" = "Eight Keys", - "9K" = "Nine Keys", - "10K" = "Ten Keys" -} - -async function debugDailyChallenge() { - // Daily Challenge!! - - const tohken = await getAccessToken("U06TBP41C3E"); - - const rooms: any[] = await fetch(`https://osu.ppy.sh/api/v2/rooms`, { - headers: { - 'Authorization': `Bearer ${tohken}`, - 'X-Api-Version': '20240529' - } - }).then(res => res.json()); - - const dailyChallenge = rooms.find(room => room.host.id == 3 && room.active && room.category == "daily_challenge"); - - const currentSong = dailyChallenge.current_playlist_item - - const ruleset = [":osu-standard: osu!standard", ":osu-taiko: osu!taiko", ":osu-catch: osu!catch", ":osu-mania: osu!mania"][currentSong.ruleset_id] - - return app.client.chat.postMessage({ - channel: "C165V7XT9", - text: "A new daily challenge has started!", - "blocks": [ - { - "type": "header", - text: { - text: ruleset.split(' ').shift() + " A new daily challenge has started!", - emoji: true, - type: "plain_text" - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": ` - -*Ruleset:* ${ruleset} -*Required mods:* ${currentSong.required_mods.length === 0 ? "None" : currentSong.required_mods.map((mod: any) => - // @ts-ignore I HATE THIS - Mods[mod.acronym] || mod.acronym - ).join(', ')}` - }, - "accessory": { - "type": "image", - "image_url": dailyChallenge.host.avatar_url, - "alt_text": `${dailyChallenge.host.username}'s osu profile picture` - } - } - ], - unfurl_links: true - }) -} - -type QueueJob = { - md5: string, - playerName: string, - ts: string, - userId: string -} - -const queue: QueueJob[] = [] -// key is renderID -const waiting = new Map() - -const processQueue = async () => { - if (queue.length > 0) { - const job = queue.shift()!; - - setTimeout(processQueue, 5000) - - app.client.reactions.add({ - channel: "C165V7XT9", - name: "thinkspin", - timestamp: job.ts - }) - - const render = await ordr.sendRender({ - replay: `replay-${job.md5}.osr`, - skin: 'default', - username: job.playerName, - showDanserLogo: false, - resolution: '1280x720', - introBGDim: 100, - inGameBGDim: 100, - breakBGDim: 100 - }) - - console.log(render) - - // @ts-ignore Error code 0 DOES exist: https://ordr.issou.best/docs/#section/Error-codes - if (render.errorCode !== 0) { - app.client.chat.postEphemeral({ - channel: "C165V7XT9", - user: job.userId, - text: `Hey: it looks like you posted a replay! Unfortunately, I couldn't generate a video of it: "${render.message}"` - }) - return - } - - waiting.set(render.renderID!, job); - } -}; - -const socket = io('https://apis.issou.best', { - path: '/ordr/ws', - autoConnect: false -}) - -socket.on('connect', () => { - console.log('Connected to ordr websocket!') -}) - -socket.on('disconnect', reason => { - if (reason == "io server disconnect") { - socket.connect() - } -}) - -socket.on('render_done_json', async (render) => { - const job = waiting.get(render.renderID!); - - if (!job) return; - - app.client.chat.postMessage({ - channel: 'C165V7XT9', - thread_ts: job.ts, - reply_broadcast: true, - text: `<${render.videoUrl}|replay-${job.md5}.mp4>`, - unfurl_media: true - }) - - app.client.reactions.remove({ - channel: "C165V7XT9", - name: "thinkspin", - timestamp: job.ts - }) - - waiting.delete(render.renderID!); -}) - -const addToQueue = (job: QueueJob) => { - queue.push(job); - if (queue.length === 1) { - processQueue(); - } -}; - -app.event("message", async (ctx) => { - if (ctx.event.channel != "C165V7XT9") return; - if (ctx.event.subtype != "file_share") return; - const ts = ctx.event.ts; - - const history = await ctx.client.conversations.history({ - channel: "C165V7XT9", - latest: ts, - limit: 1, - inclusive: true - }) - - if (!(history.messages && history.messages.length > 0)) { - return; - } - - const message = history.messages[0]; - - if (!message.files) return; - if (message.files.length === 0) return; - - const replay = message.files.find(file => file.name?.endsWith(".osr")); - - if (!replay) return; - - const replayData = await fetch(replay.url_private_download!, { - headers: { - 'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}` - } - }).then(res => res.arrayBuffer()); - - const replayBuffer = Buffer.from(replayData); - - const _replay = await osr.read(replayBuffer); - - if (_replay.gameMode !== 0) { - ctx.client.chat.postEphemeral({ - channel: "C165V7XT9", - user: ctx.context.userId!, - text: "Hey: it looks like you posted a replay! Unfortunately, it's not an :osu-standard: osu!standard replay, and so I can't generate a video of it. Sorry!" - }) - return; - } - - const replayFile = fs.createWriteStream(`replay-${_replay.replayMD5}.osr`); - - replayFile.write(replayBuffer); - replayFile.end(); - - - replayFile.on('finish', () => { - addToQueue({ - md5: _replay.replayMD5, - playerName: _replay.playerName, - ts: ts, - userId: ctx.context.userId! - }) - }); -}) - ; (async () => { await app.start(41691); console.log('⚡️ Bolt app is running!'); - socket.connect(); - cacheStuff(); setInterval(cacheStuff, 60 * 1000) // Cache every minute. Ratelimit is 1200 req/m anyways. - - scheduleJob('30 5 0 * * *', debugDailyChallenge) })(); diff --git a/package.json b/package.json index 43328d4..f6dc48d 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,8 @@ "dependencies": { "@slack/bolt": "^4.0.0", "@types/bcrypt": "^5.0.2", - "@types/node-schedule": "^2.1.7", "bcrypt": "^5.1.1", "dotenv": "^16.4.5", - "node-osr": "^1.2.1", - "node-schedule": "^2.1.1", - "ordr.js": "^4.0.0", - "postgres": "^3.4.4", - "socket.io-client": "^4.8.1" + "postgres": "^3.4.4" } } \ No newline at end of file