diff --git a/commands/cooldowns.js b/commands/cooldowns.js new file mode 100644 index 0000000..748ea22 --- /dev/null +++ b/commands/cooldowns.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require("discord.js"); +const { Card, User, Character } = require("../models"); +const { UserUtils } = require("../util"); + +//fetch all cards owned by the user and list them +module.exports = { + data: new SlashCommandBuilder() + .setName("cooldowns") + .setDescription("List cooldowns"), + async execute(interaction) { + //fetch the user given the userID and include his cards + const user = await UserUtils.getUserByDiscordId(interaction.member.id); + + //get user cooldowns using user utils + const cooldowns = await UserUtils.getCooldowns(user); + + let reply = "Cooldowns:\n"; + for (cooldown in cooldowns) { + //if cooldown contains the string formatted + if (cooldown.includes("Formatted")) { + reply += `${cooldowns[cooldown]}\n`; + } + } + + interaction.reply({ + content: reply, + ephemeral: false + }); + + } +} \ No newline at end of file diff --git a/commands/debug.js b/commands/debug.js index bb2d7cb..4a650b8 100644 --- a/commands/debug.js +++ b/commands/debug.js @@ -1,7 +1,7 @@ const { SlashCommandBuilder, ComponentType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { customAlphabet } = require("nanoid"); -const { Card, User, Character } = require("../models"); -const Util = require("../util/cards"); +const { Card, User } = require("../models"); +const { UserUtils, CardUtils, GeneralUtils } = require("../util"); module.exports = { data: new SlashCommandBuilder() @@ -15,8 +15,8 @@ module.exports = { ), async execute(interaction) { - const identifier = Util.generateIdentifier(); - + const identifier = CardUtils.generateIdentifier(); + let user = await UserUtils.getUserByDiscordId(interaction.member.id); switch (interaction.options.getString("feature")) { case "ping": interaction.reply({ @@ -45,6 +45,31 @@ module.exports = { content: `Cleared ${cards.length} cards`, ephemeral: false }); + break; + case "cooldowns": + const timeouts = await UserUtils.getCooldowns(user); + console.log(`UserTimeouts: ${JSON.stringify(timeouts)}`); + let timeoutInMinutes = 0; + interaction.reply({ + content: `\`\`\`${JSON.stringify(timeouts, null, 2)}\`\`\` `, + ephemeral: false + }); + break; + case "bot": + let botProperties = await GeneralUtils.getBotProperty(null); + interaction.reply({ + content: `\`\`\`${JSON.stringify(botProperties, null, 2)}\`\`\` `, + ephemeral: false + }); + break; + case "reset_cd": + await UserUtils.setCooldown(user, "pull", 1); + await UserUtils.setCooldown(user, "drop", 1); + await UserUtils.setCooldown(user, "daily", 1); + interaction.reply({ + content: `Reset cooldowns`, + ephemeral: false + }); } } } \ No newline at end of file diff --git a/commands/drop.js b/commands/drop.js index de380d3..459d75a 100644 --- a/commands/drop.js +++ b/commands/drop.js @@ -1,7 +1,7 @@ const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require("discord.js"); const { Card, User, Character } = require("../models"); const { customAlphabet } = require("nanoid"); -const { CardUtils, UserUtils, ReplyUtils } = require("../util"); +const { CardUtils, UserUtils, ReplyUtils, GeneralUtils } = require("../util"); const card = require("../models/card"); module.exports = { @@ -10,11 +10,16 @@ module.exports = { .setDescription("Drop a card"), async execute(interaction) { - const user = await User.findOne({ - where: { - discordId: interaction.member.id - } - }); + const user = await UserUtils.getUserByDiscordId(interaction.member.id); + + const cooldowns = await UserUtils.getCooldowns(user); + if (cooldowns.dropCooldown > 0) { + interaction.reply({ + content: `You can't drop cards for another ${cooldowns.dropCooldown} milliseconds`, + ephemeral: false + }); + return; + } //Generate 3 cards, each is persisted with an initial userId of NULL const cards = []; @@ -55,6 +60,8 @@ module.exports = { } const message = await interaction.reply({ content: reply, components: [row], fetchReply: true }); + //set users drop cooldown + await UserUtils.setCooldown(user, "drop", await GeneralUtils.getBotProperty("dropTimeout")); const filter = m => m.author.id === interaction.user.id; const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 15000 }); @@ -64,10 +71,21 @@ module.exports = { if (await cards[cardId].userId) { i.reply({ content: "This card has already been claimed!", ephemeral: true }); return; } let claimUser = await UserUtils.getUserByDiscordId(i.user.id); + const cooldowns = await UserUtils.getCooldowns(user); + if (cooldowns.pullCooldown > 0) { + i.reply({ + content: `You can't claim cards for another ${cooldowns.dropCooldown} milliseconds`, + ephemeral: false + }); + return; + } + if (claimUser) { //Update card with the user id cards[cardId].userId = claimUser.id; + await UserUtils.setCooldown(user, "pull", await GeneralUtils.getBotProperty("pullTimeout")); await cards[cardId].save(); + //fetch character name from database given the character id let character = await Character.findOne({ attributes: ["name"], @@ -87,7 +105,7 @@ module.exports = { collector.on('end', collected => { console.log(`Collected ${collected.size} interactions.`); - message.interaction.editReply({ components: [], deferred: true }); + message.edit({ components: [] }); }); } diff --git a/docker-compose.yml b/docker-compose.yml index d6301ec..24847a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: image: node:16.9.0-alpine command: sh -c "npm install && node ." restart: unless-stopped + environment: + - TZ=Europe/Berlin depends_on: - "mysql" working_dir: /app @@ -18,11 +20,11 @@ services: - bandbot-db:/var/lib/mysql - ./db:/tmp/db environment: - MYSQL_ROOT_PASSWORD: ${DB_ROOTPW} - MYSQL_DATABASE: ${DB_DATABASE} - MYSQL_USER: ${DB_USERNAME} - MYSQL_PASSWORD: ${DB_PASSWORD} - + - TZ=Europe/Berlin + - MYSQL_ROOT_PASSWORD=${DB_PASSWORD} + - MYSQL_DATABASE=${DB_DATABASE} + - MYSQL_USER=${DB_USERNAME} + - MYSQL_PASSWORD=${DB_PASSWORD} volumes: bandbot-db: \ No newline at end of file diff --git a/migrations/20220818130258-make-card-userid-nullable.js b/migrations/20220818130258-make-card-userid-nullable.js index f106567..46997f6 100644 --- a/migrations/20220818130258-make-card-userid-nullable.js +++ b/migrations/20220818130258-make-card-userid-nullable.js @@ -12,7 +12,7 @@ module.exports = { async down (queryInterface, Sequelize) { await queryInterface.changeColumn('Cards', 'userId', { type: Sequelize.INTEGER, - allowNull: false + defaultValue: 0 }); } }; diff --git a/migrations/20220819073326-add-cooldown-fields-to-user.js b/migrations/20220819073326-add-cooldown-fields-to-user.js new file mode 100644 index 0000000..2b9c131 --- /dev/null +++ b/migrations/20220819073326-add-cooldown-fields-to-user.js @@ -0,0 +1,39 @@ +'use strict'; + +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.addColumn('Users', 'nextDrop', { + type: Sequelize.DATE, + allowNull: true, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }); + await queryInterface.addColumn('Users', 'nextPull', { + type: Sequelize.DATE, + allowNull: true, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }); + await queryInterface.addColumn('Users', 'nextDaily', { + type: Sequelize.DATE, + allowNull: true, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }); + await queryInterface.addColumn('Bots', 'pullTimeout', { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 300000 // 5 minutes + }); + await queryInterface.addColumn('Bots', 'dropTimeout', { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 900000 // 15 minutes + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.removeColumn('Users', 'nextDrop'); + await queryInterface.removeColumn('Users', 'nextPull'); + await queryInterface.removeColumn('Users', 'nextDaily'); + await queryInterface.removeColumn('Bots', 'pullTimeout'); + await queryInterface.removeColumn('Bots', 'dropTimeout'); + } +}; diff --git a/models/user.js b/models/user.js index 268db2a..dea2f93 100644 --- a/models/user.js +++ b/models/user.js @@ -17,7 +17,10 @@ module.exports = (sequelize, DataTypes) => { User.init({ discordId: DataTypes.BIGINT, active: DataTypes.INTEGER, - privacy: DataTypes.INTEGER + privacy: DataTypes.INTEGER, + nextDrop: DataTypes.DATE, + nextPull: DataTypes.DATE, + nextDaily: DataTypes.DATE }, { sequelize, modelName: 'User', diff --git a/util/users.js b/util/users.js index c464ac4..e73d30e 100644 --- a/util/users.js +++ b/util/users.js @@ -15,14 +15,55 @@ module.exports = { if (user) { return true; } + if (!interaction.isButton() && interaction.commandName === "register") { return true; } + interaction.reply({ content: `${interaction.member} You are not registered, use the /register command`, ephemeral: false }); return false; + }, + + getCooldowns: async function(user) { + /* Returns an object with the following properties: + * now: the current time in milliseconds + --- For each key in cooldownKeys --- + * nextPullTimestamp: the next time the user can pull a card in milliseconds + * pullCooldown: time in milliseconds until the user can pull again + * pullCooldownFormatted: print friendly version of pullCooldown in hours and minutes + */ + + const cooldownKeys = ["Pull", "Drop", "Daily"] + + let reply = { + now: new Date().getTime() + }; + + for (key of cooldownKeys) { + reply[`next${key}Timestamp`] = user[`next${key}`].getTime(); + let cooldown = Math.max(reply[`next${key}Timestamp`] - reply['now'], 0); + reply[`${key.toLowerCase()}Cooldown`] = cooldown; + if (cooldown > 0) { + reply[`${key.toLowerCase()}CooldownFormatted`] = `Next ${key} in ${Math.floor(cooldown / 3600000)} hours ` + + `and ${Math.floor((cooldown % 3600000) / 60000)} minutes`; + } else { + reply[`${key.toLowerCase()}CooldownFormatted`] = `${key} Ready!`; + } + } + + return reply; + }, + + setCooldown: async function(user, cooldownType, cooldown) { + /* cooldownType: "pull", "drop", "daily" + * cooldown: time in milliseconds + */ + let newCooldown = new Date(new Date().getTime() + cooldown); + user[`next${cooldownType[0].toUpperCase() + cooldownType.slice(1)}`] = newCooldown; + await user.save(); } }