const { App } = require('@slack/bolt'); const postgres = require('postgres'); require('dotenv').config() const sql = postgres({ host: '/var/run/postgresql', database: 'haroon_slackmaster', username: 'haroon' }) const BeginnerOpponents = require('./opponents/beginner'); const AllOpponents = [ ...BeginnerOpponents ] const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET }); async function initializeUser(slackUserId) { let a = await sql`SELECT * FROM users WHERE slack_id = ${slackUserId};` if (a.length === 0) { a = await sql`INSERT INTO users (slack_id) VALUES (${slackUserId}) RETURNING *;` } return a[0]; } app.use(async (ctx) => { await initializeUser(ctx.context.userId) await ctx.next() }) app.command('/chooseopponent', async (ctx) => { await ctx.ack(); const user = await initializeUser(ctx.body.user_id); if (user.currentopponent != "None") { const opponent = AllOpponents.find(x => x.rawId == user.currentopponent) return await ctx.respond({ response_type: 'ephemeral', text: `You are already in a battle with ${opponent.name}.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* You are already in a battle with *${opponent.name}*. Please finish your battle with them before proceeding. ${user.battlemessage}` } } ] }) } await ctx.client.views.open({ trigger_id: ctx.body.trigger_id, view: { "private_metadata": ctx.payload.channel_id, "type": "modal", "callback_id": "chooseopponent", "title": { "type": "plain_text", "text": "Choose an opponent", "emoji": true }, "submit": { "type": "plain_text", "text": "Choose", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `*Battle Support*: Hiya <@${ctx.body.user_id}>! What rank opponent would you like to battle against?\n\nNot sure yet? Don't worry! Just cancel out and view what opponents you can fight with /viewopponents!` } }, { "type": "divider" }, { "type": "input", "element": { "type": "radio_buttons", "options": [ { "text": { "type": "plain_text", "text": "Special", "emoji": true }, "value": "SPECIAL" }, { "text": { "type": "plain_text", "text": "Beginner", "emoji": true }, "value": "BEGINNER" }, { "text": { "type": "plain_text", "text": "Casual", "emoji": true }, "value": "CASUAL" } ], "action_id": "rank-selection" }, "label": { "type": "plain_text", "text": "Choose a rank:", "emoji": true } } ] } }) }); function generateProfile(dbUser, slackUser) { return [ { "type": "section", "fields": [ { "type": "mrkdwn", "text": "*User:* " + slackUser.display_name_normalized }, { "type": "mrkdwn", "text": "*Rank:* " + dbUser.rank }, { "type": "mrkdwn", "text": "*Battle Power*: " + (dbUser.health + dbUser.mindmg + dbUser.maxdmg) }, { "type": "mrkdwn", "text": `*Victories/Losses:* ${dbUser.victories}/${dbUser.losses} (${dbUser.victories / (dbUser.victories + dbUser.losses) || 0}%)` }, { "type": "mrkdwn", "text": "*Current Win Streak:* " + dbUser.curstreak }, { "type": "mrkdwn", "text": "*Highest Win Streak*: " + dbUser.highstreak } ], "accessory": { "type": "image", "image_url": slackUser.image_1024, "alt_text": "user profile" } }, { "type": "section", "text": { "type": "mrkdwn", "text": `*Base Health:* ${dbUser.health}\n*Base Min Damage:* ${dbUser.mindmg}\n*Base Max Damage:* ${dbUser.maxdmg}` } } ] } app.command('/profile', async (ctx) => { await ctx.ack(); const args = ctx.body.text.slice().split(/ +/g).filter(x => x); let match; // If there is an argument and the first one is a Slack ping if (args.length && (match = args[0].match(/\<\@(.+)\|(.+)>/))) { const mentionedUser = match[1]; const dbUser = await initializeUser(mentionedUser); const slackUser = (await ctx.client.users.info({ user: mentionedUser })).user.profile; ctx.say({ "text": `@${slackUser.display_name_normalized}'s profile`, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `<@${ctx.body.user_id}> ran \`/profile @${slackUser.display_name_normalized}\`` } }, ...generateProfile(dbUser, slackUser) ] }) } // If there is an argument but it isn't a Slack ping else if (args.length) { ctx.respond({ blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Greetings <@${ctx.body.user_id}>. You have tried to view the profile of an invalid user. Please ensure you either send a user ping as an argument or provide no argument at all.` } } ] }) } // There is no argument else { const dbUser = await initializeUser(ctx.body.user_id); const slackUser = (await ctx.client.users.info({ user: ctx.body.user_id })).user.profile; ctx.say({ "text": `@${slackUser.display_name_normalized}'s profile`, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `<@${ctx.body.user_id}> ran \`/profile\`` } }, ...generateProfile(dbUser, slackUser) ] }) } }) app.view("chooseopponent", async (ctx) => { const { selected_option } = Object.values(ctx.view.state.values)[0]['rank-selection']; const rank = selected_option.value; const messages = { "SPECIAL": `*Battle Special*: Wasn't expecting to see ya here <@${ctx.body.user.id}>... I guess you're up for a challenge huh? Alright then, who do ya wanna annoy today?`, "BEGINNER": `*Battle Beginner*: Hey there <@${ctx.body.user.id}>! Let's keep things simple, who do you want to battle against?`, "CASUAL": `*Battle Casual*: Alright <@${ctx.body.user.id}>, things are about to get a little bit tougher from here... Who do you feel like taking on today?` } await ctx.ack({ response_action: 'update', view: { "private_metadata": ctx.payload.private_metadata, "type": "modal", "callback_id": "chooseopponent-" + rank, "title": { "type": "plain_text", "text": "Choose an opponent", "emoji": true }, "submit": { "type": "plain_text", "text": "Battle", "emoji": true }, "close": { "type": "plain_text", "text": "Never mind", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": messages[rank] || `*Battle Master*: Greetings <@${ctx.body.user.id}>. Please choose an opponent from below to battle.` } }, { "type": "divider" }, { "type": "input", "element": { "type": "static_select", "options": BeginnerOpponents.map(opponent => ({ "text": { "type": "plain_text", "text": `${opponent.name} // ${opponent.stats.health + opponent.stats.min + opponent.stats.max} Battle Power`, "emoji": true }, "value": opponent.rawId }) ), "action_id": "opponents" }, "label": { "type": "plain_text", "text": "Choose an opponent:", "emoji": true } } ] } }) }) app.view("chooseopponent-BEGINNER", async (ctx) => { await ctx.ack(); const channelId = ctx.view.private_metadata; const userId = ctx.context.userId; const slackUser = (await ctx.client.users.info({ user: userId })).user.profile; const opponent = BeginnerOpponents.find(o => o.rawId == Object.values(ctx.payload.state.values)[0].opponents.selected_option.value); const player = await initializeUser(userId); await sql`UPDATE users SET playerhealth = ${player.health}, playermin = ${player.mindmg}, playermax = ${player.maxdmg}, currentOpponent = ${opponent.rawId}, opponenthealth = ${opponent.stats.health}, opponentmin = ${opponent.stats.min}, opponentmax = ${opponent.stats.max} WHERE slack_id = ${userId};` const msg = await ctx.client.chat.postMessage({ channel: channelId, text: `${slackUser.display_name_normalized} started a battle against ${opponent.name}.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: opponent.intro.replaceAll("{player}", slackUser.display_name_normalized) }, "accessory": { "type": "image", "image_url": opponent.image, "alt_text": opponent.name } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Continue", "emoji": true }, "value": "continue", "action_id": "continue" } ] } ] }); await sql`UPDATE users SET battlemessage = ${`https://hackclub.slack.com/archives/${channelId}/p${msg.ts.replace('.', '')}`} WHERE slack_id = ${userId};` }) app.command('/bm-eval', async (ctx) => { await ctx.ack(); const resp = require('util').inspect(await eval(ctx.body.text), undefined, 1) ctx.respond({ text: resp, response_type: 'ephemeral', blocks: [ { type: 'section', text: { type: 'mrkdwn', text: "```" + resp + "```" } } ] }) }) app.command('/viewopponents', async (ctx) => { await ctx.ack(); const args = ctx.body.text.slice().split(/ +/g); switch (args[0].toUpperCase()) { case "SPECIAL": ctx.respond({ text: "You're trying to view Special opponents.", response_type: 'ephemeral' }) break; case "BEGINNER": const mappedBeginner = BeginnerOpponents.map(opponent => ({ name: opponent.name, battlePower: opponent.stats.health + opponent.stats.min + opponent.stats.max }) ) ctx.respond({ response_type: 'ephemeral', text: `*Battle Master:* Greetings battler. Here are the avaliable *Beginner* opponents for you to battle.`, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `*Battle Master:* Greetings <@${ctx.body.user_id}>. Here are the avaliable *Beginner* opponents for you to battle.` } }, { type: 'section', text: { type: 'mrkdwn', text: mappedBeginner.map(opponent => `*${opponent.name}:*\n\n${opponent.battlePower} Battle Power`).join('\n\n\n') } } ] }) break; case "CASUAL": ctx.say("You're trying to view Casual opponents.") break; default: ctx.say("You either wrote nothing or just chose a rank that doesn't exist.") } }); ; (async () => { // Start your app await app.start(process.env.PORT); console.log('⚡️ Bolt app is running!'); })();