import { name, version, repository } from "./package.json"; const USER_AGENT = `${name}/${version} (+${repository.url})`; const { App, ExpressReceiver } = (await import("@slack/bolt")); import postgres from 'postgres'; import "dotenv/config"; import { spawnSync } from "child_process"; const sql = postgres({ host: '/var/run/postgresql', database: 'haroon_nxslack', username: 'haroon' }) const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, clientSecret: process.env.SLACK_CLIENT_SECRET }); app.event("app_home_opened", async (ctx) => { const slackUser = (await ctx.client.users.info({ user: ctx.context.userId! })).user!; const [ link = null ] = await sql`SELECT * FROM links WHERE slackid = ${ctx.context.userId!};`; if (link) { const frens = getFriends(); const me = frens.find(x => x.nsaId == link.nsaid)!; await ctx.client.views.publish({ user_id: ctx.event.user, view: { type: 'home', blocks: [ { type: 'header', text: { type: 'plain_text', text: "You're linked!", emoji: true } }, { "type": "section", "text": { "type": "mrkdwn", "text": `Hey *${slackUser.profile?.display_name_normalized}* - you're already linked to a Nintendo Switch account (${me.name}). To unlink yourself, DM <@U06TBP41C3E>.` }, "accessory": { "type": "image", "image_url": me.imageUri, "alt_text": "switch profile picture" } } ] } }); return } await ctx.client.views.publish({ // Use the user ID associated with the event user_id: ctx.event.user, view: { "type": "home", "blocks": [ { "type": "header", "text": { "type": "plain_text", "text": "You aren't linked", "emoji": true } }, { "type": "section", "text": { "type": "mrkdwn", "text": `Hey *${slackUser.profile?.display_name_normalized}*! To sync your Nintendo Switch and Slack status, give me your friend code!` }, "accessory": { "type": "button", "text": { "type": "plain_text", "text": "Sync", "emoji": true }, "value": "click_me_123", "action_id": "get-friend-code" } }, ...(slackUser.is_admin || slackUser.is_owner ? [ { type: 'section', // @ts-expect-error this feels dumb, but it said text wasn't a value on Block... what text: { type: 'mrkdwn', text: `By the way, *${slackUser.profile?.display_name_normalized}*... I can't actually EDIT your status because of how Slack works. I know that sounds annoying, but hopefully that should be fixed whenever I get an admin API token!` } } ] : []) ] } }); }) app.action("get-friend-code", async (ctx) => { ctx.ack(); ctx.client.views.open({ trigger_id: ctx.body.trigger_id!, view: { "type": "modal", callback_id: "friend-code-validation", "title": { "type": "plain_text", "text": "Add me as a friend!", "emoji": true }, "submit": { "type": "plain_text", "text": "Submit", "emoji": true }, "close": { "type": "plain_text", "text": "Cancel", "emoji": true }, "blocks": [ { "block_id": "FRIEND_CODE", "type": "input", "element": { "type": "plain_text_input", "action_id": "code", "placeholder": { "type": "plain_text", "text": "SW-xxxx-xxxx-xxxx" } }, "label": { "type": "plain_text", "text": "Enter your friend code here...", "emoji": true } } ] } }) }) const FRIEND_CODE_REGEX = /(?:SW-)?(\d{4}-\d{4}-\d{4})/g; app.view("friend-code-validation", async (ctx) => { // ts is lovely const friendCode = Object.values(ctx.view.state.values)[0]['code'].value!; if (!FRIEND_CODE_REGEX.test(friendCode)) return await ctx.ack({ response_action: 'errors', errors: { "FRIEND_CODE": "This isn't a valid friend code. (SW-xxxx-xxxx-xxxx or xxxx-xxxx-xxxx)" } }); const realCode = friendCode.replace("SW-", ""); spawnSync("/home/haroon/nxslack/node_modules/.bin/nxapi", ["nso", "add-friend", realCode], { cwd: process.cwd(), env: { 'NXAPI_USER_AGENT': USER_AGENT } }); await ctx.ack(); ctx.client.chat.postMessage({ channel: ctx.body.user.id, text: "I've added you on Nintendo Switch! Find my friend request and accept it or add me yourself (SW-2973-7062-1423) and then click this button to finish the process!", blocks: [ { type: 'section', text: { type: 'mrkdwn', text: "I've added you on Nintendo Switch! Find my friend request and accept it or add me back (`SW-2973-7062-1423`) and then click this button to finish the process!" } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "I've accepted!", "emoji": true }, "value": realCode, "action_id": "check-synced" } ] } ] }) }) type NSOUser = { id: number, nsaId: string, imageUri: string, name: string, isFriend: boolean, isFavouriteFriend: boolean, isServiceUser: boolean, friendCreatedAt: number, presence: any } const getFriends = function getFriends(): NSOUser[] { const { stdout } = spawnSync("/home/haroon/nxslack/node_modules/.bin/nxapi", ["nso", "friends", "--json"], { cwd: process.cwd(), env: { 'NXAPI_USER_AGENT': USER_AGENT } }); console.log("getFriends", stdout.toString('utf-8').slice()) return JSON.parse(stdout.toString('utf-8').slice()); }; app.action('check-synced', async (ctx) => { if (ctx.action.type != "button") return; await ctx.ack(); const friends = getFriends(); const { stdout } = spawnSync("/home/haroon/nxslack/node_modules/.bin/nxapi", ["nso", "lookup", ctx.action.value!, "--json"], { cwd: process.cwd(), env: { 'NXAPI_USER_AGENT': USER_AGENT } }); const user: NSOUser = JSON.parse(stdout.toString('utf-8').slice()); if (!!friends.find(userA => userA.nsaId == user.nsaId)) { await sql`INSERT INTO links VALUES (${ctx.context.userId!}, ${user.nsaId})` ctx.respond({ text: `Congrats! You've successfully linked your Slack account to your Nintendo Switch user (${user.name})!` }) } }) ; (async () => { await app.start(53371); console.log('⚡️ Bolt app is running!'); setInterval(async () => { const frens = getFriends(); const links = await sql`SELECT * FROM links;`; for (let fren of frens) { const dbFren = links.find(frend => frend.nsaid == fren.nsaId)!; if (!dbFren) continue; const slackUser = (await app.client.users.info({ user: dbFren.slackid })).user!; if (slackUser.is_admin || slackUser.is_owner) return; switch (fren.presence.state) { // Console offline case "OFFLINE": // Console online; but not playing a game case "INACTIVE": if (dbFren.last_status != null) { await app.client.users.profile.set({ token: process.env.SLACK_USER_OAUTH_TOKEN, user: dbFren.slackid, // @ts-expect-error This is correct, Bolt has incorrect typing profile: { status_text: "", status_emoji: "" } }) await sql`UPDATE links SET last_status = NULL WHERE nsaId = ${fren.nsaId};`; } break; // Playing a game case "ONLINE": if (dbFren.last_status != `Playing ${fren.presence.game.name}`) { await app.client.users.profile.set({ token: process.env.SLACK_USER_OAUTH_TOKEN, user: dbFren.slackid, // @ts-expect-error This is correct, Bolt has incorrect typing profile: { status_text: `Playing ${fren.presence.game.name}`, status_emoji: ":nintendo:" } }) await sql`UPDATE links SET last_status = ${`Playing ${fren.presence.game.name}`} WHERE nsaId = ${fren.nsaId};`; } break; } } }, 60_000) })();