nxslack/index.ts

303 lines
10 KiB
TypeScript
Raw Permalink Normal View History

2024-07-23 09:13:37 +00:00
import { name, version, repository } from "./package.json";
const USER_AGENT = `${name}/${version} (+${repository.url})`;
2024-07-23 10:14:51 +00:00
const { App, ExpressReceiver } = (await import("@slack/bolt"));
import postgres from 'postgres';
2024-07-23 09:13:37 +00:00
import "dotenv/config";
2024-07-23 10:14:51 +00:00
import { spawnSync } from "child_process";
2024-07-23 09:13:37 +00:00
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,
2024-07-23 10:14:51 +00:00
clientSecret: process.env.SLACK_CLIENT_SECRET
2024-07-23 09:13:37 +00:00
});
app.event("app_home_opened", async (ctx) => {
const slackUser = (await ctx.client.users.info({ user: ctx.context.userId! })).user!;
2024-07-23 09:13:37 +00:00
2024-07-23 10:14:51 +00:00
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>.`
2024-07-23 10:14:51 +00:00
},
"accessory": {
"type": "image",
"image_url": me.imageUri,
"alt_text": "switch profile picture"
}
}
]
}
});
return
}
2024-07-23 09:13:37 +00:00
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",
2024-07-23 10:14:51 +00:00
"text": "You aren't linked",
2024-07-23 09:13:37 +00:00
"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!`
2024-07-23 09:13:37 +00:00
},
"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!`
}
}
] : [])
2024-07-23 09:13:37 +00:00
]
}
});
})
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!;
2024-07-23 10:14:51 +00:00
if (!FRIEND_CODE_REGEX.test(friendCode)) return await ctx.ack({
2024-07-23 09:13:37 +00:00
response_action: 'errors',
errors: {
"FRIEND_CODE": "This isn't a valid friend code. (SW-xxxx-xxxx-xxxx or xxxx-xxxx-xxxx)"
}
});
2024-07-23 10:14:51 +00:00
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
} });
2024-08-06 23:25:02 +00:00
console.log("getFriends", stdout.toString('utf-8').slice())
2024-07-23 10:14:51 +00:00
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})!`
})
}
2024-07-23 09:13:37 +00:00
})
; (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)!;
2024-07-23 15:23:53 +00:00
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)
2024-08-06 23:25:02 +00:00
})();