303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
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)
|
|
})();
|