diff --git a/src/classes/Bot.ts b/src/classes/Bot.ts index 7a14b272e..822ccf5a2 100644 --- a/src/classes/Bot.ts +++ b/src/classes/Bot.ts @@ -779,7 +779,7 @@ export default class Bot { void sendStats(this); } else { this.getAdmins.forEach(admin => { - this.handler.commands.useStatsCommand(admin); + this.handler.commandHandler.useStatsCommand(admin); }); } } diff --git a/src/classes/Commands/CommandHandler.ts b/src/classes/Commands/CommandHandler.ts new file mode 100644 index 000000000..db96a54d3 --- /dev/null +++ b/src/classes/Commands/CommandHandler.ts @@ -0,0 +1,261 @@ +import SteamID from 'steamid'; +import Bot from '../Bot'; +import IPricer from '../IPricer'; +import log from '../../lib/logger'; +import CommandParser from '../CommandParser'; +import CartQueue from '../Carts/CartQueue'; +import Cart from '../Carts/Cart'; +import { UnknownDictionary } from '../../types/common'; +import Inventory from '../Inventory'; +import * as c from './sub-classes/export'; + +// Import all commands +import InformationCommands from './commands/information'; +import CartCommands from './commands/cart'; +import InventoryCommands from './commands/inventory'; + +export interface ICommand { + /** + * Name of the command + */ + name: string; + /** + * The description of the command + */ + description: string; + /** + * Instance of the bot class + */ + bot: Bot; + /** + * Instance of the pricer class + */ + pricer: IPricer; + /** + * The command can have aliases which will point to the command + */ + aliases?: string[]; + /** + * How to use the command + */ + usage?: string; + /** + * Allow the steamID to be invalid + */ + dontAllowInvalidType?: boolean; + /** + * Is only allowed for admins + */ + isAdminCommand?: boolean; + /** + * Is only allowed for whitelisted users + */ + isWhitelistCommand?: boolean; + /** + * When the command is called + */ + execute: (steamID: SteamID, message: string, command?: any) => Promise | void; +} + +export type Instant = 'buy' | 'b' | 'sell' | 's'; +export type CraftUncraft = 'craftweapon' | 'uncraftweapon'; +export type Misc = 'time' | 'uptime' | 'pure' | 'rate' | 'owner' | 'discord' | 'stock'; +export type BlockUnblock = 'block' | 'unblock'; +export type NameAvatar = 'name' | 'avatar'; +export type TF2GC = 'expand' | 'use' | 'delete'; +export type ActionOnTrade = 'accept' | 'accepttrade' | 'decline' | 'declinetrade'; +export type ForceAction = 'faccept' | 'fdecline'; + +function hasAliases(command: ICommand): command is ICommand & { aliases: string[] } { + return command.aliases !== undefined; +} + +export default class CommandHandler { + manager: c.ManagerCommands; + + private message: c.MessageCommand; + + help: c.HelpCommands; + + misc: c.MiscCommands; + + private opt: c.OptionsCommand; + + private pManager: c.PricelistManager; + + private request: c.RequestCommands; + + private review: c.ReviewCommands; + + private status: c.StatusCommands; + + private crafting: c.CraftingCommands; + + isDonating = false; + + adminInventoryReset: NodeJS.Timeout; + + adminInventory: UnknownDictionary = {}; + + private readonly commands: Map; + + private readonly commandsPointAliases: Map; + + constructor(private readonly bot: Bot, private readonly pricer: IPricer) { + this.commands = new Map(); + this.commandsPointAliases = new Map(); + this.help = new c.HelpCommands(this.bot); + this.manager = new c.ManagerCommands(bot); + this.message = new c.MessageCommand(bot); + this.misc = new c.MiscCommands(bot); + this.opt = new c.OptionsCommand(bot); + this.pManager = new c.PricelistManager(bot, pricer); + this.request = new c.RequestCommands(bot, pricer); + this.review = new c.ReviewCommands(bot); + this.status = new c.StatusCommands(bot); + this.crafting = new c.CraftingCommands(bot); + } + + public registerCommands(): void { + log.info('Registering commands'); + // We will later on register all our commands here, by importing them + // We will also include aliases and point them to the command + + // We want to initialize all our commands + const commands = [...InformationCommands, ...CartCommands, ...InventoryCommands]; + for (const command of commands) { + const cmd = new command(this.bot, this.pricer, this); + this.commands.set(cmd.name, cmd); + if (hasAliases(cmd)) { + for (const alias of cmd.aliases) { + this.commandsPointAliases.set(alias, cmd.name); + } + } + } + } + + public async handleCommand(steamID: SteamID, message: string): Promise { + const prefix = this.bot.getPrefix(steamID); + const command = CommandParser.getCommand(message.toLowerCase(), prefix); + const isAdmin = this.bot.isAdmin(steamID); + const isWhitelisted = this.bot.isWhitelisted(steamID); + const isInvalidType = steamID.type === 0; + + const checkMessage = message.split(' ').filter(word => word.includes(`!${command}`)).length; + + if (checkMessage > 1 && !isAdmin) { + return this.bot.sendMessage(steamID, "⛔ Don't spam"); + } + + log.debug(`Received command ${command} from ${steamID.getSteamID64()}`); + + if (command === null) { + const custom = this.bot.options.customMessage.commandNotFound; + + this.bot.sendMessage( + steamID, + custom ? custom.replace('%command%', command) : `❌ Command "${command}" not found!` + ); + return; + } + + const cmd = this.commands.get(command) ?? this.commands.get(this.commandsPointAliases.get(command) ?? ''); + + if (cmd === undefined) { + return; + } + + // Check if the command is an admin command + if (cmd.isAdminCommand && !isAdmin) { + // But we should also check if the command is a whitelist command + if (cmd.isWhitelistCommand && !isWhitelisted) { + return; + } + return; + } + + // By default dontAllowInvalidType is false + if (cmd.dontAllowInvalidType && isInvalidType) { + return this.bot.sendMessage(steamID, '❌ Command not available.'); + } + + await cmd.execute(steamID, message, command); + } + + get cartQueue(): CartQueue { + return this.bot.handler.cartQueue; + } + + get weaponsAsCurrency(): { enable: boolean; withUncraft: boolean } { + return { + enable: this.bot.options.miscSettings.weaponsAsCurrency.enable, + withUncraft: this.bot.options.miscSettings.weaponsAsCurrency.withUncraft + }; + } + + addCartToQueue(cart: Cart, isDonating: boolean, isBuyingPremium: boolean): void { + const activeOfferID = this.bot.trades.getActiveOffer(cart.partner); + + const custom = this.bot.options.commands.addToQueue; + + if (activeOfferID !== null) { + return this.bot.sendMessage( + cart.partner, + custom.alreadyHaveActiveOffer + ? custom.alreadyHaveActiveOffer.replace( + /%tradeurl%/g, + `https://steamcommunity.com/tradeoffer/${activeOfferID}/` + ) + : `❌ You already have an active offer! Please finish it before requesting a new one: https://steamcommunity.com/tradeoffer/${activeOfferID}/` + ); + } + + const currentPosition = this.cartQueue.getPosition(cart.partner); + + if (currentPosition !== -1) { + if (currentPosition === 0) { + this.bot.sendMessage( + cart.partner, + custom.alreadyInQueueProcessingOffer + ? custom.alreadyInQueueProcessingOffer + : '⚠️ You are already in the queue! Please wait while I process your offer.' + ); + } else { + this.bot.sendMessage( + cart.partner, + custom.alreadyInQueueWaitingTurn + ? custom.alreadyInQueueWaitingTurn + .replace(/%isOrAre%/g, currentPosition !== 1 ? 'are' : 'is') + .replace(/%currentPosition%/g, String(currentPosition)) + : '⚠️ You are already in the queue! Please wait your turn, there ' + + (currentPosition !== 1 ? 'are' : 'is') + + ` ${currentPosition} in front of you.` + ); + } + return; + } + + const position = this.cartQueue.enqueue(cart, isDonating, isBuyingPremium); + + if (position !== 0) { + this.bot.sendMessage( + cart.partner, + custom.addedToQueueWaitingTurn + ? custom.addedToQueueWaitingTurn + .replace(/%isOrAre%/g, position !== 1 ? 'are' : 'is') + .replace(/%position%/g, String(position)) + : '✅ You have been added to the queue! Please wait your turn, there ' + + (position !== 1 ? 'are' : 'is') + + ` ${position} in front of you.` + ); + } + } + + useStatsCommand(steamID: SteamID): void { + void this.status.statsCommand(steamID); + } + + useUpdateOptionsCommand(steamID: SteamID | null, message: string): void { + this.opt.updateOptionsCommand(steamID, message); + } +} diff --git a/src/classes/Commands/commands/cart/BuyAndSell.ts b/src/classes/Commands/commands/cart/BuyAndSell.ts new file mode 100644 index 000000000..08d6c74db --- /dev/null +++ b/src/classes/Commands/commands/cart/BuyAndSell.ts @@ -0,0 +1,71 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand, Instant } from '../../CommandHandler'; +import { getItemAndAmount } from '../../functions/utils'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import CommandParser from '../../../CommandParser'; +import UserCart from '../../../Carts/UserCart'; + +export default class BuyAndSellCommand implements ICommand { + name = 'buy'; + + aliases = ['b', 'sell', 's']; + + usage = '[amount] '; + + description = 'Instantly buy an item 💲'; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute(steamID: SteamID, message: string, command: Instant) { + const opt = this.bot.options.commands[command === 'b' ? 'buy' : command === 's' ? 'sell' : command]; + const prefix = this.bot.getPrefix(steamID); + + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : '❌ This command is disabled by the owner.'); + } + } + + const info = getItemAndAmount( + steamID, + CommandParser.removeCommand(message), + this.bot, + prefix, + command === 'b' ? 'buy' : command === 's' ? 'sell' : command + ); + + if (info === null) { + return; + } + + const cart = new UserCart( + steamID, + this.bot, + this.commandHandler.weaponsAsCurrency.enable ? this.bot.craftWeapons : [], + this.commandHandler.weaponsAsCurrency.enable && this.commandHandler.weaponsAsCurrency.withUncraft + ? this.bot.uncraftWeapons + : [] + ); + + cart.setNotify = true; + if (['b', 'buy'].includes(command)) { + cart.addOurItem(info.priceKey, info.amount); + } else { + cart.addTheirItem(info.match.sku, info.amount); + } + + this.commandHandler.addCartToQueue(cart, false, false); + } +} diff --git a/src/classes/Commands/commands/cart/BuyCart.ts b/src/classes/Commands/commands/cart/BuyCart.ts new file mode 100644 index 000000000..e49d09325 --- /dev/null +++ b/src/classes/Commands/commands/cart/BuyCart.ts @@ -0,0 +1,115 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import pluralize from 'pluralize'; +import { getItemAndAmount } from '../../functions/utils'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import Cart from '../../../Carts/Cart'; +import UserCart from '../../../Carts/UserCart'; +import CommandParser from '../../../CommandParser'; + +export default class BuyCartCommand implements ICommand { + name = 'buycart'; + + description = 'Add an item you want to buy to your cart 🛒'; + + usage = '[amount] '; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const currentCart = Cart.getCart(steamID); + const prefix = this.bot.getPrefix(steamID); + + if (currentCart !== null && !(currentCart instanceof UserCart)) { + return this.bot.sendMessage( + steamID, + '❌ You already have an active cart, please finalize it before making a new one. 🛒' + ); + } + + const opt = this.bot.options.commands.buycart; + + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : '❌ This command is disabled by the owner.'); + } + } + + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, prefix, 'buycart'); + + if (info === null) { + return; + } + + let amount = info.amount; + const cart = + Cart.getCart(steamID) || + new UserCart( + steamID, + this.bot, + this.commandHandler.weaponsAsCurrency.enable ? this.bot.craftWeapons : [], + this.commandHandler.weaponsAsCurrency.enable && this.commandHandler.weaponsAsCurrency.withUncraft + ? this.bot.uncraftWeapons + : [] + ); + + const cartAmount = cart.getOurCount(info.priceKey); + const ourAmount = this.bot.inventoryManager.getInventory.getAmount({ + priceKey: info.priceKey, + includeNonNormalized: false, + tradableOnly: true + }); + const amountCanTrade = + this.bot.inventoryManager.amountCanTrade({ priceKey: info.priceKey, tradeIntent: 'selling' }) - cartAmount; + + const name = info.match.name; + + // Correct trade if needed + if (amountCanTrade <= 0) { + return this.bot.sendMessage( + steamID, + 'I ' + + (ourAmount > 0 ? "can't sell" : "don't have") + + ` any ${(cartAmount > 0 ? 'more ' : '') + pluralize(name, 0)}.` + ); + } + + if (amount > amountCanTrade) { + amount = amountCanTrade; + + if (amount === cartAmount && cartAmount > 0) { + return this.bot.sendMessage( + steamID, + `I don't have any ${(ourAmount > 0 ? 'more ' : '') + pluralize(name, 0)}.` + ); + } + + this.bot.sendMessage( + steamID, + `I can only sell ${pluralize(name, amount, true)}. ` + + (amount > 1 ? 'They have' : 'It has') + + ` been added to your cart. Type "${prefix}cart" to view your cart summary or "${prefix}checkout" to checkout. 🛒` + ); + } else + this.bot.sendMessage( + steamID, + `✅ ${pluralize(name, Math.abs(amount), true)}` + + ` has been added to your cart. Type "${prefix}cart" to view your cart summary or "${prefix}checkout" to checkout. 🛒` + ); + + cart.addOurItem(info.priceKey, amount); + Cart.addCart(cart); + }; +} diff --git a/src/classes/Commands/commands/cart/Cancel.ts b/src/classes/Commands/commands/cart/Cancel.ts new file mode 100644 index 000000000..047e3dfcb --- /dev/null +++ b/src/classes/Commands/commands/cart/Cancel.ts @@ -0,0 +1,107 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import log from '../../../../lib/logger'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class CancelCommand implements ICommand { + name = 'cancel'; + + description = 'Cancel the trade offer ❌'; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + // Maybe have the cancel command only cancel the offer in the queue, and have a command for canceling the offer? + const positionInQueue = this.commandHandler.cartQueue.getPosition(steamID); + + // If a user is in the queue, then they can't have an active offer + + const custom = this.bot.options.commands.cancel.customReply; + if (positionInQueue === 0) { + // The user is in the queue and the offer is already being processed + const cart = this.commandHandler.cartQueue.getCart(steamID); + + if (cart.isMade) { + return this.bot.sendMessage( + steamID, + custom.isBeingSent + ? custom.isBeingSent + : '⚠️ Your offer is already being sent! Please try again when the offer is active.' + ); + } else if (cart.isCanceled) { + return this.bot.sendMessage( + steamID, + custom.isCancelling + ? custom.isCancelling + : '⚠️ Your offer is already being canceled. Please wait a few seconds for it to be canceled.' + ); + } + + cart.setCanceled = 'BY_USER'; + } else if (positionInQueue !== -1) { + // The user is in the queue + this.commandHandler.cartQueue.dequeue(steamID); + this.bot.sendMessage( + steamID, + custom.isRemovedFromQueue ? custom.isRemovedFromQueue : '✅ You have been removed from the queue.' + ); + + clearTimeout(this.commandHandler.adminInventoryReset); + delete this.commandHandler.adminInventory[steamID.getSteamID64()]; + } else { + // User is not in the queue, check if they have an active offer + + const activeOffer = this.bot.trades.getActiveOffer(steamID); + + if (activeOffer === null) { + return this.bot.sendMessage( + steamID, + custom.noActiveOffer ? custom.noActiveOffer : "❌ You don't have an active offer." + ); + } + + void this.bot.trades.getOffer(activeOffer).asCallback((err, offer) => { + if (err || !offer) { + const errStringify = JSON.stringify(err); + const errMessage = errStringify === '' ? (err as Error)?.message : errStringify; + return this.bot.sendMessage( + steamID, + `❌ Ohh nooooes! Something went wrong while trying to get the offer: ${errMessage}` + + (!offer ? ` (or the offer might already be canceled)` : '') + ); + } + + offer.data('canceledByUser', true); + + offer.cancel(err => { + // Only react to error, if the offer is canceled then the user + // will get an alert from the onTradeOfferChanged handler + + if (err) { + log.warn('Error while trying to cancel an offer: ', err); + return this.bot.sendMessage( + steamID, + `❌ Ohh nooooes! Something went wrong while trying to cancel the offer: ${err.message}` + ); + } + + return this.bot.sendMessage( + steamID, + `✅ Offer sent (${offer.id}) has been successfully cancelled.` + ); + }); + }); + } + }; +} diff --git a/src/classes/Commands/commands/cart/Cart.ts b/src/classes/Commands/commands/cart/Cart.ts new file mode 100644 index 000000000..8324f687c --- /dev/null +++ b/src/classes/Commands/commands/cart/Cart.ts @@ -0,0 +1,44 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Cart from '../../../Carts/Cart'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class CartCommand implements ICommand { + name = 'cart'; + + description = 'View your cart 🛒'; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + const opt = this.bot.options.commands.cart; + + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : '❌ This command is disabled by the owner.'); + } + } + + if (this.commandHandler.isDonating) { + return this.bot.sendMessage( + steamID, + `You're about to send donation. Send "${prefix}donatecart" to view your donation cart summary or "${prefix}donatenow" to send donation now.` + ); + } + + this.bot.sendMessage(steamID, Cart.stringify(steamID, false, prefix)); + }; +} diff --git a/src/classes/Commands/commands/cart/Checkout.ts b/src/classes/Commands/commands/cart/Checkout.ts new file mode 100644 index 000000000..539ec317d --- /dev/null +++ b/src/classes/Commands/commands/cart/Checkout.ts @@ -0,0 +1,46 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Cart from '../../../Carts/Cart'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class CheckoutCommand implements ICommand { + name = 'checkout'; + + description = 'Have the bot send an offer with the items in your cart ✅🛒\n\n✨=== Trade actions ===✨'; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + if (this.commandHandler.isDonating) { + return this.bot.sendMessage( + steamID, + `You're about to send donation. Send "${prefix}donatecart" to view your donation cart summary or "${prefix}donatenow" to send donation now.` + ); + } + + const cart = Cart.getCart(steamID); + if (cart === null) { + const custom = this.bot.options.commands.checkout.customReply.empty; + return this.bot.sendMessage(steamID, custom ? custom : '🛒 Your cart is empty.'); + } + + cart.setNotify = true; + cart.isDonating = false; + this.commandHandler.addCartToQueue(cart, false, false); + + clearTimeout(this.commandHandler.adminInventoryReset); + delete this.commandHandler.adminInventory[steamID.getSteamID64()]; + }; +} diff --git a/src/classes/Commands/commands/cart/ClearCart.ts b/src/classes/Commands/commands/cart/ClearCart.ts new file mode 100644 index 000000000..637989ff6 --- /dev/null +++ b/src/classes/Commands/commands/cart/ClearCart.ts @@ -0,0 +1,29 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import Cart from '../../../Carts/Cart'; + +export default class ClearCartCommand implements ICommand { + name = 'clearcart'; + + description = 'View your cart 🛒'; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + Cart.removeCart(steamID); + const custom = this.bot.options.commands.clearcart.customReply.reply; + this.bot.sendMessage(steamID, custom ? custom : '🛒 Your cart has been cleared.'); + }; +} diff --git a/src/classes/Commands/commands/cart/SellCart.ts b/src/classes/Commands/commands/cart/SellCart.ts new file mode 100644 index 000000000..4086a30e3 --- /dev/null +++ b/src/classes/Commands/commands/cart/SellCart.ts @@ -0,0 +1,110 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import pluralize from 'pluralize'; +import { getItemAndAmount } from '../../functions/utils'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import Cart from '../../../Carts/Cart'; +import UserCart from '../../../Carts/UserCart'; +import CommandParser from '../../../CommandParser'; +import { getSkuAmountCanTrade } from '../../../Inventory'; + +export default class SellCartCommand implements ICommand { + name = 'sellcart'; + + description = 'Add an item you want to sell to your cart 🛒'; + + usage = '[amount] '; + + dontAllowInvalidType = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + + const currentCart = Cart.getCart(steamID); + if (currentCart !== null && !(currentCart instanceof UserCart)) { + return this.bot.sendMessage( + steamID, + '❌ You already have an active cart, please finalize it before making a new one. 🛒' + ); + } + + const opt = this.bot.options.commands.sellcart; + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : '❌ This command is disabled by the owner.'); + } + } + + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, prefix, 'sellcart'); + if (info === null) { + return; + } + + let amount = info.amount; + + const cart = + Cart.getCart(steamID) || + new UserCart( + steamID, + this.bot, + this.commandHandler.weaponsAsCurrency.enable ? this.bot.craftWeapons : [], + this.commandHandler.weaponsAsCurrency.enable && this.commandHandler.weaponsAsCurrency.withUncraft + ? this.bot.uncraftWeapons + : [] + ); + const skuCount = getSkuAmountCanTrade(info.match.sku, this.bot); + + const cartAmount = + skuCount.amountCanTrade >= skuCount.amountCanTradeGeneric + ? cart.getTheirCount(info.match.sku) + : cart.getTheirGenericCount(info.match.sku); + + const amountCanTrade = skuCount.mostCanTrade - cartAmount; + + // Correct trade if needed + if (amountCanTrade <= 0) { + return this.bot.sendMessage( + steamID, + 'I ' + + (skuCount.mostCanTrade > 0 ? "can't buy" : "don't want") + + ` any ${(cartAmount > 0 ? 'more ' : '') + pluralize(skuCount.name, 0)}.` + ); + } + + if (amount > amountCanTrade) { + amount = amountCanTrade; + + if (amount === cartAmount && cartAmount > 0) { + return this.bot.sendMessage(steamID, `I unable to trade any more ${pluralize(skuCount.name, 0)}.`); + } + + this.bot.sendMessage( + steamID, + `I can only buy ${pluralize(skuCount.name, amount, true)}. ` + + (amount > 1 ? 'They have' : 'It has') + + ` been added to your cart. Type "${prefix}cart" to view your cart summary or "${prefix}checkout" to checkout. 🛒` + ); + } else { + this.bot.sendMessage( + steamID, + `✅ ${pluralize(skuCount.name, Math.abs(amount), true)}` + + ` has been added to your cart. Type "${prefix}cart" to view your cart summary or "${prefix}checkout" to checkout. 🛒` + ); + } + + cart.addTheirItem(info.match.sku, amount); + Cart.addCart(cart); + }; +} diff --git a/src/classes/Commands/commands/cart/index.ts b/src/classes/Commands/commands/cart/index.ts new file mode 100644 index 000000000..c2ba6bdf6 --- /dev/null +++ b/src/classes/Commands/commands/cart/index.ts @@ -0,0 +1,17 @@ +import BuyAndSellCommand from './BuyAndSell'; +import BuyCartCommand from './BuyCart'; +import CancelCommand from './Cancel'; +import CartCommand from './Cart'; +import CheckoutCommand from './Checkout'; +import ClearCartCommand from './ClearCart'; +import SellCartCommand from './SellCart'; + +export default [ + CartCommand, + CheckoutCommand, + SellCartCommand, + ClearCartCommand, + CancelCommand, + BuyCartCommand, + BuyAndSellCommand +]; diff --git a/src/classes/Commands/commands/information/Help.ts b/src/classes/Commands/commands/information/Help.ts new file mode 100644 index 000000000..be9178c5f --- /dev/null +++ b/src/classes/Commands/commands/information/Help.ts @@ -0,0 +1,25 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class HelpCommand implements ICommand { + name = 'help'; + + description = 'Shows all available commands'; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + void this.commandHandler.help.helpCommand(steamID, prefix); + }; +} diff --git a/src/classes/Commands/commands/information/Links.ts b/src/classes/Commands/commands/information/Links.ts new file mode 100644 index 000000000..25faa78ef --- /dev/null +++ b/src/classes/Commands/commands/information/Links.ts @@ -0,0 +1,26 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class LinkCommands implements ICommand { + name = 'link'; + + aliases = ['links']; + + description = ''; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + this.commandHandler.misc.links(steamID); + }; +} diff --git a/src/classes/Commands/commands/information/Message.ts b/src/classes/Commands/commands/information/Message.ts new file mode 100644 index 000000000..61ef52e3a --- /dev/null +++ b/src/classes/Commands/commands/information/Message.ts @@ -0,0 +1,189 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import CommandParser from '../../../CommandParser'; +import log from '../../../../lib/logger'; +import sendAdminMessage from '../../../../lib/DiscordWebhook/sendAdminMessage'; +import generateLinks from '../../../../lib/tools/links'; +import { timeNow } from '../../../../lib/tools/time'; +import sendPartnerMessage from '../../../../lib/DiscordWebhook/sendPartnerMessage'; + +export default class MessageCommand implements ICommand { + name = 'message'; + + description = 'Send a message to the owner of the bot 💬'; + + allowInvalidTypes = false; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + + const isAdmin = this.bot.isAdmin(steamID); + const optComm = this.bot.options.commands.message; + const custom = optComm.customReply; + + if (!optComm.enable) { + if (isAdmin) { + this.bot.sendMessage( + steamID, + '❌ The message command is disabled. Enable it by sending `!config commands.message.enable=true`.' + ); + } else { + this.bot.sendMessage( + steamID, + custom.disabled ? custom.disabled : '❌ The owner has disabled messages.' + ); + } + return; + } + + const senderDetails = this.bot.friends.getFriend(steamID); + const optDW = this.bot.options.discordWebhook.messages; + + if (isAdmin) { + const steamIdAndMessage = CommandParser.removeCommand(message); + const parts = steamIdAndMessage.split(' '); + let recipientSteamID: SteamID; + + try { + recipientSteamID = new SteamID(parts[0]); + } catch (err) { + log.error('Wrong input (SteamID): ', err); + return this.bot.sendMessage( + steamID, + `❌ Your syntax is wrong or the SteamID is incorrectly formatted. Here's an example: "${prefix}message 76561198120070906 Hi"` + + "\n\nHow to get the targeted user's SteamID?" + + '\n1. Go to his/her profile page.' + + '\n2. Go to https://steamrep.com/' + + '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' + ); + } + + const steamIDString = recipientSteamID.getSteamID64(); + + if (!recipientSteamID.isValid()) { + return this.bot.sendMessage( + steamID, + `❌ "${steamIDString}" is not a valid SteamID.` + + "\n\nHow to get the targeted user's SteamID?" + + '\n1. Go to his/her profile page.' + + '\n2. Go to https://steamrep.com/' + + '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' + ); + } else if (!this.bot.friends.isFriend(recipientSteamID)) { + return this.bot.sendMessage(steamID, `❌ I am not friends with the user.`); + } + + const recipientDetails = this.bot.friends.getFriend(recipientSteamID); + const adminDetails = this.bot.friends.getFriend(steamID); + const reply = steamIdAndMessage.substring(steamIDString.length).trim(); + const isShowOwner = optComm.showOwnerName; + + // Send message to recipient + this.bot.sendMessage( + recipientSteamID, + custom.fromOwner + ? custom.fromOwner.replace(/%reply%/g, reply) + : `/quote 💬 Message from ${ + isShowOwner && adminDetails ? adminDetails.player_name : 'the owner' + }: ${reply}` + + '\n\n❔ Hint: You can use the !message command to respond to the owner of this bot.' + + '\nExample: !message Hi Thanks!' + ); + + // Send a notification to the admin with message contents & details + if (optDW.enable && optDW.url !== '') { + sendAdminMessage( + recipientSteamID.toString(), + reply, + recipientDetails, + generateLinks(recipientSteamID.toString()), + timeNow(this.bot.options).time, + this.bot + ); + } else { + const customInitializer = this.bot.options.steamChat.customInitializer.message.toOtherAdmins; + this.bot.messageAdmins( + `${ + customInitializer ? customInitializer : '/quote' + } 💬 Message sent to #${recipientSteamID.toString()}${ + recipientDetails ? ` (${recipientDetails.player_name})` : '' + }: "${reply}". `, + [] + ); + } + + this.bot.sendMessage(steamID, custom.success ? custom.success : '✅ Your message has been sent.'); + + // Send message to all other admins that an admin replied + return this.bot.messageAdmins( + `${ + senderDetails ? `${senderDetails.player_name} (${steamID.toString()})` : steamID.toString() + } sent a message to ${ + recipientDetails + ? recipientDetails.player_name + ` (${recipientSteamID.toString()})` + : recipientSteamID.toString() + } with "${reply}".`, + [steamID] + ); + } else { + const admins = this.bot.getAdmins; + if (!admins || admins.length === 0) { + // Just default to same message as if it was disabled + return this.bot.sendMessage( + steamID, + custom.disabled ? custom.disabled : '❌ The owner has disabled messages.' + ); + } + + const msg = message.substring(8).trim(); // "message" + if (!msg) { + return this.bot.sendMessage( + steamID, + custom.wrongSyntax + ? custom.wrongSyntax + : `❌ Please include a message. Here's an example: "${prefix}message Hi"` + ); + } + + const links = generateLinks(steamID.toString()); + if (optDW.enable && optDW.url !== '') { + sendPartnerMessage( + steamID.toString(), + msg, + senderDetails, + links, + timeNow(this.bot.options).time, + this.bot + ); + } else { + const customInitializer = this.bot.options.steamChat.customInitializer.message.onReceive; + this.bot.messageAdmins( + `${ + customInitializer ? customInitializer : '/quote' + } 💬 You've got a message from #${steamID.toString()}${ + senderDetails ? ` (${senderDetails.player_name})` : '' + }:` + + `"${msg}". ` + + `\nSteam: ${links.steam}` + + `\nBackpack.tf: ${links.bptf}` + + `\nSteamREP: ${links.steamrep}`, + [] + ); + } + + this.bot.sendMessage(steamID, custom.success ? custom.success : '✅ Your message has been sent.'); + } + }; +} diff --git a/src/classes/Commands/commands/information/Miscs.ts b/src/classes/Commands/commands/information/Miscs.ts new file mode 100644 index 000000000..d58fd37bd --- /dev/null +++ b/src/classes/Commands/commands/information/Miscs.ts @@ -0,0 +1,31 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand, Misc } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class MiscCommands implements ICommand { + name = 'stock'; + + aliases = ['time', 'uptime', 'pure', 'rate', 'owner', 'discord', 'stock']; + + description = ''; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + // I don't like this approach, and better to remove this aliases and turn them into specific commands + // In their own classes + execute = (steamID: SteamID, message: string, command) => { + if (command === 'stock') { + this.commandHandler.misc.miscCommand(steamID, command as Misc, message); + } + this.commandHandler.misc.miscCommand(steamID, command as Misc); + }; +} diff --git a/src/classes/Commands/commands/information/More.ts b/src/classes/Commands/commands/information/More.ts new file mode 100644 index 000000000..3f8f9e821 --- /dev/null +++ b/src/classes/Commands/commands/information/More.ts @@ -0,0 +1,25 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class MoreCommand implements ICommand { + name = 'more'; + + description = 'Show the advanced commands list'; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + void this.commandHandler.help.moreCommand(steamID, prefix); + }; +} diff --git a/src/classes/Commands/commands/information/Price.ts b/src/classes/Commands/commands/information/Price.ts new file mode 100644 index 000000000..0227914bd --- /dev/null +++ b/src/classes/Commands/commands/information/Price.ts @@ -0,0 +1,122 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import { getItemAndAmount } from '../../functions/utils'; +import pluralize from 'pluralize'; +import Currencies from '@tf2autobot/tf2-currencies'; +import dayjs from 'dayjs'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import CommandParser from '../../../CommandParser'; + +export default class PriceCommand implements ICommand { + name = 'price'; + + aliases = ['p']; + + usage = '[amount] '; + + description = 'Get the price and stock of an item.'; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const opt = this.bot.options.commands.price; + const prefix = this.bot.getPrefix(steamID); + + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : '❌ This command is disabled by the owner.'); + } + } + + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, prefix); + if (info === null) { + return; + } + + const match = info.match; + const amount = info.amount; + + let reply = ''; + + const isBuying = match.intent === 0 || match.intent === 2; + const isSelling = match.intent === 1 || match.intent === 2; + + const keyPrice = this.bot.pricelist.getKeyPrice; + + if (isBuying) { + reply = '💲 I am buying '; + + if (amount !== 1) { + reply += `${amount} `; + } + + // If the amount is 1, then don't convert to value and then to currencies. If it is for keys, then don't use conversion rate + reply += `${pluralize(match.name, 2)} for ${(amount === 1 + ? match.buy + : Currencies.toCurrencies( + match.buy.toValue(keyPrice.metal) * amount, + match.sku === '5021;6' ? undefined : keyPrice.metal + ) + ).toString()}`; + } + + if (isSelling) { + const currencies = + amount === 1 + ? match.sell + : Currencies.toCurrencies( + match.sell.toValue(keyPrice.metal) * amount, + match.sku === '5021;6' ? undefined : keyPrice.metal + ); + + if (reply === '') { + reply = '💲 I am selling '; + + if (amount !== 1) { + reply += `${amount} `; + } else { + reply += 'a '; + } + + reply += `${pluralize(match.name, amount)} for ${currencies.toString()}`; + } else { + reply += ` and selling for ${currencies.toString()}`; + } + } + + reply += `.\n📦 I have ${this.bot.inventoryManager.getInventory.getAmount({ + priceKey: match.id ?? match.sku, + includeNonNormalized: false, + tradableOnly: true + })}`; + + if (match.max !== -1 && isBuying) { + reply += ` / ${match.max}`; + } + + if (isSelling && match.min !== 0) { + reply += ` and I can sell ${this.bot.inventoryManager.amountCanTrade({ + priceKey: match.sku, + tradeIntent: 'selling' + })}`; + } + + reply += '. '; + + if (match.autoprice && this.bot.isAdmin(steamID)) { + reply += ` (price last updated ${dayjs.unix(match.time).fromNow()})`; + } + + this.bot.sendMessage(steamID, reply); + }; +} diff --git a/src/classes/Commands/commands/information/Queue.ts b/src/classes/Commands/commands/information/Queue.ts new file mode 100644 index 000000000..8d58ed993 --- /dev/null +++ b/src/classes/Commands/commands/information/Queue.ts @@ -0,0 +1,41 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class QueueCommand implements ICommand { + name = 'queue'; + + description = 'Check your position in the queue\n\n✨=== Contact Owner ===✨'; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const position = this.bot.handler.cartQueue.getPosition(steamID); + const custom = this.bot.options.commands.queue.customReply; + + if (position === -1) { + this.bot.sendMessage(steamID, custom.notInQueue ? custom.notInQueue : '❌ You are not in the queue.'); + } else if (position === 0) { + this.bot.sendMessage( + steamID, + custom.offerBeingMade ? custom.offerBeingMade : '⌛ Your offer is being made.' + ); + } else { + this.bot.sendMessage( + steamID, + custom.hasPosition + ? custom.hasPosition.replace(/%position%/g, String(position)) + : `There are ${position} users ahead of you.` + ); + } + }; +} diff --git a/src/classes/Commands/commands/information/how2trade.ts b/src/classes/Commands/commands/information/how2trade.ts new file mode 100644 index 000000000..5a92c7a9f --- /dev/null +++ b/src/classes/Commands/commands/information/how2trade.ts @@ -0,0 +1,35 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class How2TradeCommand implements ICommand { + name = 'how2trade'; + + description = 'Guide on how to trade with the bot.'; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string): void => { + const custom = this.bot.options.commands.how2trade.customReply.reply; + const prefix = this.bot.getPrefix(steamID); + this.bot.sendMessage( + steamID, + custom + ? custom + : '/quote You can either send me an offer yourself, or use one of my commands to request a trade. ' + + `Say you want to buy a Team Captain, just type "${prefix}buy Team Captain", if want to buy more, ` + + `just add the [amount] - "${prefix}buy 2 Team Captain". Type "${prefix}help" for all the commands.` + + `\nYou can also buy or sell multiple items by using the "${prefix}buycart [amount] " or ` + + `"${prefix}sellcart [amount] " commands.` + ); + }; +} diff --git a/src/classes/Commands/commands/information/index.ts b/src/classes/Commands/commands/information/index.ts new file mode 100644 index 000000000..909f1967e --- /dev/null +++ b/src/classes/Commands/commands/information/index.ts @@ -0,0 +1,19 @@ +import HelpCommand from './Help'; +import LinkCommands from './Links'; +import MessageCommand from './Message'; +import MiscCommands from './Miscs'; +import MoreCommand from './More'; +import PriceCommand from './Price'; +import QueueCommand from './Queue'; +import How2TradeCommand from './how2trade'; + +export default [ + HelpCommand, + How2TradeCommand, + PriceCommand, + QueueCommand, + MiscCommands, + LinkCommands, + MessageCommand, + MoreCommand +]; diff --git a/src/classes/Commands/commands/inventory/Autokeys.ts b/src/classes/Commands/commands/inventory/Autokeys.ts new file mode 100644 index 000000000..2effab47f --- /dev/null +++ b/src/classes/Commands/commands/inventory/Autokeys.ts @@ -0,0 +1,28 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class AutokeysCommand implements ICommand { + name = 'autokeys'; + + description = "Get info on the bot's current autokeys settings 🔑"; + + isAdminCommand = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + // I don't like this approach, and better to remove this aliases and turn them into specific commands + // In their own classes + execute = (steamID: SteamID, message: string, command) => { + this.commandHandler.manager.autokeysCommand(steamID); + }; +} diff --git a/src/classes/Commands/commands/inventory/Crafting.ts b/src/classes/Commands/commands/inventory/Crafting.ts new file mode 100644 index 000000000..faaf73fa0 --- /dev/null +++ b/src/classes/Commands/commands/inventory/Crafting.ts @@ -0,0 +1,37 @@ +import SteamID from 'steamid'; +import CommandHandler, { CraftUncraft, ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class CraftingCommand implements ICommand { + name = 'craftweapon'; + + aliases = ['craftweapons', 'uncraftweapon', 'uncraftweapons']; + + description = ''; + + isAdminCommand = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + // I don't like this approach, and better to remove this aliases and turn them into specific commands + // In their own classes + execute = (steamID: SteamID, message: string, command) => { + void this.commandHandler.misc.weaponCommand( + steamID, + command === 'craftweapons' + ? 'craftweapon' + : command === 'uncraftweapons' + ? 'uncraftweapon' + : (command as CraftUncraft) + ); + }; +} diff --git a/src/classes/Commands/commands/inventory/Deposit.ts b/src/classes/Commands/commands/inventory/Deposit.ts new file mode 100644 index 000000000..c8f40af88 --- /dev/null +++ b/src/classes/Commands/commands/inventory/Deposit.ts @@ -0,0 +1,141 @@ +import SteamID from 'steamid'; +import SKU from '@tf2autobot/tf2-sku'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import Cart from '../../../Carts/Cart'; +import AdminCart from '../../../Carts/AdminCart'; +import CommandParser from '../../../CommandParser'; +import { getItemFromParams, removeLinkProtocol } from '../../functions/utils'; +import { fixItem } from '../../../../lib/items'; +import Inventory from '../../../Inventory'; +import log from '../../../../lib/logger'; +import pluralize from 'pluralize'; + +export default class DepositCommand implements ICommand { + name = 'deposit'; + + aliases = ['d']; + + description = ''; + + isAdminCommand = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + // I don't like this approach, and better to remove this aliases and turn them into specific commands + // In their own classes + execute = async (steamID: SteamID, message: string) => { + const prefix = this.bot.getPrefix(steamID); + const currentCart = Cart.getCart(steamID); + if (currentCart !== null && !(currentCart instanceof AdminCart)) { + return this.bot.sendMessage( + steamID, + '❌ You already have an active cart, please finalize it before making a new one. 🛒' + ); + } + + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } else { + params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku as string), this.bot.schema)); + } + + const sku = params.sku as string; + + const amount = typeof params.amount === 'number' ? params.amount : 1; + if (!Number.isInteger(amount)) { + return this.bot.sendMessage(steamID, `❌ amount should only be an integer.`); + } + + const itemName = this.bot.schema.getName(SKU.fromString(sku), false); + + const steamid = steamID.getSteamID64(); + + const adminInventory = + this.commandHandler.adminInventory[steamid] || + new Inventory(steamID, this.bot, 'their', this.bot.boundInventoryGetter); + + if (this.commandHandler.adminInventory[steamid] === undefined) { + try { + log.debug('fetching admin inventory'); + await adminInventory.fetch(); + this.commandHandler.adminInventory[steamid] = adminInventory; + + clearTimeout(this.commandHandler.adminInventoryReset); + this.commandHandler.adminInventoryReset = setTimeout(() => { + delete this.commandHandler.adminInventory[steamid]; + }, 5 * 60 * 1000); + } catch (err) { + log.error('Error fetching inventory: ', err); + return this.bot.sendMessage( + steamID, + `❌ Error fetching inventory, steam might down. Please try again later. ` + + `If you have private profile/inventory, please set to public and try again.` + ); + } + } + + const dict = adminInventory.getItems; + + if (dict[params.sku as string] === undefined) { + clearTimeout(this.commandHandler.adminInventoryReset); + delete this.commandHandler.adminInventory[steamid]; + return this.bot.sendMessage(steamID, `❌ You don't have any ${itemName}.`); + } + + const currentAmount = dict[params.sku as string].length; + if (currentAmount < amount) { + clearTimeout(this.commandHandler.adminInventoryReset); + delete this.commandHandler.adminInventory[steamid]; + return this.bot.sendMessage(steamID, `❌ You only have ${pluralize(itemName, currentAmount, true)}.`); + } + + const cart = + AdminCart.getCart(steamID) || + new AdminCart( + steamID, + this.bot, + this.commandHandler.weaponsAsCurrency.enable ? this.bot.craftWeapons : [], + this.commandHandler.weaponsAsCurrency.enable && this.commandHandler.weaponsAsCurrency.withUncraft + ? this.bot.uncraftWeapons + : [] + ); + + if (amount > 0) { + const cartAmount = cart.getTheirCount(sku); + + if (cartAmount > currentAmount || cartAmount + amount > currentAmount) { + return this.bot.sendMessage( + steamID, + `❌ You can't add ${pluralize(itemName, amount, true)} ` + + `because you already have ${cartAmount} in cart and you only have ${currentAmount}.` + ); + } + } + + cart.addTheirItem(sku, amount); + Cart.addCart(cart); + + this.bot.sendMessage( + steamID, + `✅ ${pluralize(itemName, Math.abs(amount), true)} has been ` + + (amount >= 0 ? 'added to' : 'removed from') + + ` your cart. Type "${prefix}cart" to view your cart summary or "${prefix}checkout" to checkout. 🛒` + ); + }; +} diff --git a/src/classes/Commands/commands/inventory/Paints.ts b/src/classes/Commands/commands/inventory/Paints.ts new file mode 100644 index 000000000..920df4b3f --- /dev/null +++ b/src/classes/Commands/commands/inventory/Paints.ts @@ -0,0 +1,28 @@ +import SteamID from 'steamid'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; + +export default class PaintsCommand implements ICommand { + name = 'paints'; + + description = 'Get a list of paints partial sku 🎨'; + + isAdminCommand = true; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + // I don't like this approach, and better to remove this aliases and turn them into specific commands + // In their own classes + execute = (steamID: SteamID, message: string, command) => { + this.commandHandler.misc.paintsCommand(steamID); + }; +} diff --git a/src/classes/Commands/commands/inventory/Sku.ts b/src/classes/Commands/commands/inventory/Sku.ts new file mode 100644 index 000000000..d823ddc01 --- /dev/null +++ b/src/classes/Commands/commands/inventory/Sku.ts @@ -0,0 +1,71 @@ +import SteamID from 'steamid'; +import SKU from '@tf2autobot/tf2-sku'; +import CommandHandler, { ICommand } from '../../CommandHandler'; +import Bot from '../../../Bot'; +import IPricer from '../../../IPricer'; +import CommandParser from '../../../CommandParser'; +import { removeLinkProtocol } from '../../functions/utils'; +import testPriceKey from '../../../../lib/tools/testPriceKey'; + +export default class SkuCommand implements ICommand { + name = 'sku'; + + description = 'Get the sku of an item.'; + + usage = ``; + + constructor( + public readonly bot: Bot, + public readonly pricer: IPricer, + public readonly commandHandler: CommandHandler + ) { + this.bot = bot; + this.pricer = pricer; + this.commandHandler = commandHandler; + } + + execute = (steamID: SteamID, message: string) => { + const itemNamesOrSkus = CommandParser.removeCommand(removeLinkProtocol(message)); + + if (itemNamesOrSkus === '!sku') { + return this.bot.sendMessage(steamID, `❌ Missing item name or item sku!`); + } + + const itemsOrSkus = itemNamesOrSkus.split('\n'); + + if (itemsOrSkus.length === 1) { + if (!testPriceKey(itemNamesOrSkus)) { + // Receive name + const sku = this.bot.schema.getSkuFromName(itemNamesOrSkus); + + if (sku.includes('null') || sku.includes('undefined')) { + return this.bot.sendMessage( + steamID, + `Generated sku: ${sku}\nPlease check the name. If correct, please let us know. Thank you.` + ); + } + + this.bot.sendMessage(steamID, `• ${sku}\nhttps://autobot.tf/items/${sku}`); + } else { + // Receive sku + const name = this.bot.schema.getName(SKU.fromString(itemNamesOrSkus), false); + this.bot.sendMessage(steamID, `• ${name}\nhttps://autobot.tf/items/${itemNamesOrSkus}`); + } + } else { + const results: { source: string; generated: string }[] = []; + itemsOrSkus.forEach(item => { + if (!testPriceKey(item)) { + // Receive name + results.push({ source: item, generated: this.bot.schema.getSkuFromName(item) }); + } else { + results.push({ source: item, generated: this.bot.schema.getName(SKU.fromString(item), false) }); + } + }); + + this.bot.sendMessage( + steamID, + `• ${results.map(item => `${item.source} => ${item.generated}`).join('\n• ')}` + ); + } + }; +} diff --git a/src/classes/Commands/commands/inventory/index.ts b/src/classes/Commands/commands/inventory/index.ts new file mode 100644 index 000000000..740e9a667 --- /dev/null +++ b/src/classes/Commands/commands/inventory/index.ts @@ -0,0 +1,7 @@ +import AutokeysCommand from './Autokeys'; +import CraftingCommand from './Crafting'; +import DepositCommand from './Deposit'; +import PaintsCommand from './Paints'; +import SkuCommand from './Sku'; + +export default [SkuCommand, PaintsCommand, AutokeysCommand, DepositCommand, CraftingCommand]; diff --git a/src/classes/Commands/sub-classes/Status.ts b/src/classes/Commands/sub-classes/Status.ts index 7d69f6c00..1d167abac 100644 --- a/src/classes/Commands/sub-classes/Status.ts +++ b/src/classes/Commands/sub-classes/Status.ts @@ -136,7 +136,7 @@ export default class StatusCommands { this.bot.sendMessage(steamID, '✅ All stats have been deleted.'); - this.bot.handler.commands.useUpdateOptionsCommand( + this.bot.handler.commandHandler.useUpdateOptionsCommand( steamID, '!config statistics.lastTotalTrades=0&statistics.startingTimeInUnix=0&statistics.lastTotalProfitMadeInRef=0&statistics.lastTotalProfitOverpayInRef=0&statistics.profitDataSinceInUnix=0' ); diff --git a/src/classes/MyHandler/MyHandler.ts b/src/classes/MyHandler/MyHandler.ts index 445e9299d..2db3ee378 100644 --- a/src/classes/MyHandler/MyHandler.ts +++ b/src/classes/MyHandler/MyHandler.ts @@ -28,7 +28,6 @@ import { Blocked, BPTFGetUserInfo } from './interfaces'; import Handler, { OnRun } from '../Handler'; import Bot from '../Bot'; import Pricelist, { Entry, PricesDataObject, PricesObject } from '../Pricelist'; -import Commands from '../Commands/Commands'; import CartQueue from '../Carts/CartQueue'; import Inventory from '../Inventory'; import TF2Inventory from '../TF2Inventory'; @@ -52,6 +51,7 @@ import filterAxiosError from '@tf2autobot/filter-axios-error'; import sendTf2SystemMessage from '../../lib/DiscordWebhook/sendTf2SystemMessage'; import sendTf2DisplayNotification from '../../lib/DiscordWebhook/sendTf2DisplayNotification'; import sendTf2ItemBroadcast from '../../lib/DiscordWebhook/sendTf2ItemBroadcast'; +import CommandHandler from '../Commands/CommandHandler'; const filterReasons = (reasons: string[]) => { const filtered = new Set(reasons); @@ -59,7 +59,7 @@ const filterReasons = (reasons: string[]) => { }; export default class MyHandler extends Handler { - readonly commands: Commands; + readonly commandHandler: CommandHandler; readonly autokeys: Autokeys; @@ -200,7 +200,7 @@ export default class MyHandler extends Handler { constructor(public bot: Bot, private priceSource: IPricer) { super(bot); - this.commands = new Commands(bot, priceSource); + this.commandHandler = new CommandHandler(bot, priceSource); this.cartQueue = new CartQueue(bot); this.autokeys = new Autokeys(bot); @@ -208,6 +208,7 @@ export default class MyHandler extends Handler { PriceCheckQueue.setBot(this.bot); PriceCheckQueue.setRequestCheckFn(this.priceSource.requestCheck.bind(this.priceSource)); + this.commandHandler.registerCommands(); } onRun(): Promise { @@ -497,7 +498,7 @@ export default class MyHandler extends Handler { : this.recentlySentMessage[steamID.redirectAnswerTo.author.id]) + 1; } - await this.commands.processMessage(steamID, message); + await this.commandHandler.handleCommand(steamID, message); } onLoginKey(loginKey: string): void {