slackmaster/index.js
DaInfLoop 3143423edf feat: add defend + item to player's turn
also fixed a bug where anyone could run eval but we don't talk about that
2024-06-20 22:17:26 +01:00

684 lines
25 KiB
JavaScript

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": userId,
"action_id": "continue"
}
]
}
]
});
await sql`UPDATE users SET battlemessage = ${`https://hackclub.slack.com/archives/${channelId}/p${msg.ts.replace('.', '')}`} WHERE slack_id = ${userId};`
})
async function checkButton(ctx) {
await ctx.ack();
if (ctx.payload.value != ctx.context.userId) {
return ctx.respond({
replace_original: false,
response_type: 'ephemeral',
text: "Battle Support: Please do not click other battlers buttons!",
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Battle Support:* Erm, <@${ctx.context.userId}>?... Please don't press other battlers buttons!
If you want to choose an opponent yourself, simply use \`/chooseopponent\` to get started!`
}
}
]
})
}
await ctx.next();
}
async function playerLoss(ctx) { }
async function playerWin(ctx) { }
app.action("continue", checkButton, async (ctx) => {
const user = await initializeUser(ctx.context.userId);
const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile;
if (user.playerhealth <= 0) {
return playerLoss(ctx)
} else if (user.opponenthealth <= 0) {
return playerWin(ctx)
}
ctx.respond({
replace_original: true,
text: "",
"blocks": [
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": `${slackUser.display_name_normalized}:\n\n*Health:* ${user.playerhealth}\n*Min Damage:* ${user.playermin}\n*Max Damage:* ${user.playermax}`
},
{
"type": "mrkdwn",
"text": `${AllOpponents.find(x => x.rawId == user.currentopponent).name}:\n\n*Health:* ${user.opponenthealth}\n*Min Damage:* ${user.opponentmin}\n*Max Damage:* ${user.opponentmax}`
}
],
"accessory": {
"type": "image",
"image_url": AllOpponents.find(x => x.rawId == user.currentopponent).image,
"alt_text": AllOpponents.find(x => x.rawId == user.currentopponent).name
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": `*${slackUser.display_name_normalized}* vs *${AllOpponents.find(x => x.rawId == user.currentopponent).name}* | Your Turn`
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "⚔️ Attack",
"emoji": true
},
"value": ctx.context.userId,
"action_id": "attack"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "🛡️ Defend",
"emoji": true
},
"value": ctx.context.userId,
"action_id": "defend"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": " 🎁 Item",
"emoji": true
},
"value": ctx.context.userId,
"action_id": "item"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "☠️ Forfeit",
"emoji": true
},
"style": "danger",
"value": ctx.context.userId,
"action_id": "forfeit"
}
]
}
]
})
})
app.action('forfeit', checkButton, async (ctx) => {
ctx.respond({
replace_original: false,
response_type: 'ephemeral',
text: 'Not yet...'
})
})
app.action(/attack|defend|item/, checkButton, async (ctx) => {
let response = "This error message isn't meant to show up. If it does, contact Haroon.";
const user = await initializeUser(ctx.context.userId);
const slackUser = (await ctx.client.users.info({ user: ctx.context.userId })).user.profile;
switch (ctx.payload.action_id) {
case 'attack':
if (user.opponentdefense == 'Strong') {
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\``
} else if (user.opponentdefense == 'Moderate') {
if (Math.random() < 0.5) {
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\``
} else {
const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin;
await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\``
}
} else if (user.opponentdefense == 'Weak') {
if (Math.random() < 0.25) {
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`NO DAMAGE\`\`\``
} else {
const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin;
await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\``
}
} else {
const damage = Math.floor(Math.random() * (user.playermax - user.playermin + 1)) + user.playermin;
await sql`UPDATE users SET opponenthealth = ${user.opponenthealth - damage} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} attacks ${AllOpponents.find(x => x.rawId == user.currentopponent).name}_*\n\n\`\`\`${damage.toLocaleString()} DAMAGE\`\`\``
}
break;
case 'defend':
if (user.playerdefendcount < 3) {
await sql`UPDATE users SET playerdefense = 'Strong' WHERE slack_id = ${ctx.context.userId};`
response = `*_A blue forcefield magically appears around ${slackUser.display_name_normalized}_*\n\n\`\`\`STRONG DEFENCE\`\`\``
} else {
const type = ["Strong", "Moderate", "Weak"][Math.floor(Math.random() * 3)];
await sql`UPDATE users SET playerdefense = ${type} WHERE slack_id = ${ctx.context.userId};`
response = `*_A blue forcefield magically appears around ${slackUser.display_name_normalized}_*\n\n\`\`\`${type.toUpperCase()} DEFENCE\`\`\``
}
break;
case 'item':
const increase = Math.floor(Math.random() * 5) + 1;
switch (['health', 'min', 'max', 'nothing'][Math.floor(Math.random() * 4)]) {
case 'health':
await sql`UPDATE users SET playerhealth = ${user.playerhealth + increase} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} HEALTH\`\`\``
break;
case 'min':
if ((user.playermin + increase) < user.playermax) {
response = `*_${slackUser.display_name_normalized} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\``
break;
}
await sql`UPDATE users SET playermin = ${user.playermin + increase} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MIN DAMAGE\`\`\``
break;
case 'max':
await sql`UPDATE users SET playermax = ${user.playermax + increase} WHERE slack_id = ${ctx.context.userId};`
response = `*_${slackUser.display_name_normalized} drinks some squash. U N D I L U T E D._*\n\n\`\`\`+ ${increase} MAX DAMAGE\`\`\``
break;
case 'nothing':
response = `*_${slackUser.display_name_normalized} touches some grass_*\n\n\`\`\`NOTHING HAPPENED\`\`\``
break;
}
}
await sql`UPDATE users SET opponentdefense = 'None' WHERE slack_id = ${ctx.context.userId};`
await ctx.respond({
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": response
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Continue",
"emoji": true
},
"value": ctx.context.userId,
"action_id": "continue-opponent"
}
]
}
]
})
})
app.command('/bm-eval', async (ctx) => {
await ctx.ack();
if (ctx.context.userId != 'U06TBP41C3E') return;
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!');
})();