diff --git a/commands/buy.ts b/commands/buy.ts new file mode 100644 index 0000000..3645be0 --- /dev/null +++ b/commands/buy.ts @@ -0,0 +1,41 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { StoreItem, User } from "../db"; +import { get_item, add_item_to_user, sub_balance } from "../db"; +import { BotError } from "./common/error"; +import { item_name_autocomplete } from "./common/autocompletes"; +import { has_role } from "../util"; +import config from "../config.json"; + +async function run(interaction: ChatInputCommandInteraction, user: User) { + const options = interaction.options; + const name: string = (await options.get("name")).value as string; + const quantity: number = (await options.get("quantity")).value as number; + if (quantity <= 0) throw new BotError("Can't buy 0 or less of an item. Nice try"); + const item = await get_item(name); + if (!item) throw new BotError("Item does not exist"); + if (item.roles_required.length > 0) { + for (const role_id of item.roles_required) { + if (!has_role(interaction, role_id)) throw new BotError("Missing one of the required roles to buy this item"); + } + } + const total_cost = item.price * quantity; + if (!(await sub_balance(user.user, total_cost))) throw new BotError("Not enough balance to buy this item"); + await add_item_to_user(user.user, item.name, quantity); + return await interaction.editReply(`Bought ${quantity} of \`${name}\` for ${total_cost} ${ total_cost === 1 ? config.currency : config.currency_plural }`); +} + +const data: CommandData = { + name: "buy", + description: "Buy an item from the store", + registered_only: true, + ephemeral: false, + admin_only: false, + run, + autocomplete: item_name_autocomplete, //autocompletes for the "name" option +}; + +export default data; + + diff --git a/commands/create_item.ts b/commands/create_item.ts index 74df95b..28b464e 100644 --- a/commands/create_item.ts +++ b/commands/create_item.ts @@ -1,4 +1,4 @@ -//also: edit_item, delete_item, store, buy, use_item, (admin: /take_item, /add_item) +//also: edit_item import type { ChatInputCommandInteraction } from "discord.js"; diff --git a/commands/delete_item.ts b/commands/delete_item.ts new file mode 100644 index 0000000..6e02bdf --- /dev/null +++ b/commands/delete_item.ts @@ -0,0 +1,28 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import { delete_item, get_item } from "../db"; +import { BotError } from "./common/error"; +import { item_name_autocomplete } from "./common/autocompletes"; + +async function run(interaction: ChatInputCommandInteraction) { + await interaction.deferReply(); + const options = interaction.options; + const name: string = (await options.get("name")).value as string; + if (!(await get_item(name))) throw new BotError("No item with that name exists to delete"); + await delete_item(name); + return await interaction.editReply(`Deleted item \`${name}\``); +} + +const data: CommandData = { + name: "delete_item", + description: "Delete item from the store and all users", + registered_only: false, + ephemeral: false, + admin_only: true, + run, + autocomplete: item_name_autocomplete, //autocompletes for the "name" option +}; + +export default data; + diff --git a/commands/edit_item.ts b/commands/edit_item.ts new file mode 100644 index 0000000..a3e52fe --- /dev/null +++ b/commands/edit_item.ts @@ -0,0 +1,47 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { Items, StoreItem, User } from "../db"; +import { edit_item, get_item } from "../db"; +import { BotError } from "./common/error"; +import { item_name_autocomplete } from "./common/autocompletes"; + +async function run(interaction: ChatInputCommandInteraction) { + await interaction.deferReply(); + const options = interaction.options; + const name: string = (await options.get("name")).value as string; + const delete_existing_roles: boolean = (await options.get("delete_existing_roles")).value as boolean; + const item = await get_item(name); + if (!item) throw new BotError("No item of that name exists"); + const price: number = ((await options.get("price"))?.value ?? item.price) as number; + const description: string = ((await options.get("description"))?.value ?? item.description) as string; + const usable: boolean = ((await options.get("usable"))?.value ?? item.usable) as boolean; + //to add multiple roles, people will have to use /edit_item, I guess? augh + const required_role = (await options.get("required_role"))?.role; + if (price < 0) throw new BotError("Price cannot be negative"); + //name and description char limits (based on discord embed field name/value limits) + if (description.length > 900) throw new BotError("Item description cannot be more than 1024 characters"); //true limit is 1024 but we want some margin for other info + const existing = delete_existing_roles ? [] : item.roles_required; + const store_item: StoreItem = { + name, + price, + description, + roles_required: required_role ? [...existing, required_role.id] : existing, + usable, + }; + await edit_item(store_item); + return await interaction.editReply("Item edited"); +} + +const data: CommandData = { + name: "edit_item", + description: "Edit item", + registered_only: false, + ephemeral: false, + admin_only: true, + run, + autocomplete: item_name_autocomplete, //autocompletes for the "name" option +}; + +export default data; + diff --git a/commands/index.ts b/commands/index.ts index 271d431..8eab354 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -5,6 +5,7 @@ import type { User } from "../db"; import { get_user } from "../db"; import { BotError } from "./common/error"; import config from "../config.json"; +import { has_role } from "../util"; import say from "./say"; import roll from "./roll"; @@ -16,6 +17,10 @@ import items from "./items"; import create_item from "./create_item"; import store from "./store"; import change_item_balance from "./change_item_balance"; +import buy from "./buy"; +import use_item from "./use_item"; +import delete_item from "./delete_item"; +import edit_item from "./edit_item"; export interface CommandData { name: string; @@ -27,10 +32,10 @@ export interface CommandData { autocomplete?: (interaction: AutocompleteInteraction) => Promise; }; -const commands: CommandData[] = [say, roll, register_user, bal, change_bal, transfer, items, create_item, store, change_item_balance]; +const commands: CommandData[] = [say, roll, register_user, bal, change_bal, transfer, items, create_item, store, change_item_balance, buy, use_item, delete_item, edit_item]; function is_admin(interaction: ChatInputCommandInteraction): boolean { - return (interaction.member as GuildMember).roles.cache.some((r) => r.id === config.admin_role); + return has_role(interaction, config.admin_role); } async function run(interaction: ChatInputCommandInteraction, found: CommandData, name: string) { diff --git a/commands/use_item.ts b/commands/use_item.ts new file mode 100644 index 0000000..fc40c44 --- /dev/null +++ b/commands/use_item.ts @@ -0,0 +1,34 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { StoreItem, User } from "../db"; +import { get_item, sub_item_to_user } from "../db"; +import { BotError } from "./common/error"; +import { item_name_autocomplete } from "./common/autocompletes"; + +async function run(interaction: ChatInputCommandInteraction, user: User) { + const options = interaction.options; + const name: string = (await options.get("name")).value as string; + const quantity: number = (await options.get("quantity")).value as number; + if (quantity <= 0) throw new BotError("Can't use 0 or less of an item"); + const item = await get_item(name); + if (!item) throw new BotError("Item does not exist"); + if (!item.usable) throw new BotError("That item is not usable"); + if (!(await sub_item_to_user(user.user, name, quantity))) throw new BotError("You did not have enough of that item to use"); + return await interaction.editReply(`Used ${quantity} of \`${name}\``); +} + +const data: CommandData = { + name: "use_item", + description: "Use an item (subtracts from your items)", + registered_only: true, + ephemeral: false, + admin_only: false, + run, + autocomplete: item_name_autocomplete, //autocompletes for the "name" option +}; + +export default data; + + + diff --git a/db.ts b/db.ts index 864ae0f..fbcf8e2 100644 --- a/db.ts +++ b/db.ts @@ -124,7 +124,7 @@ export async function get_all_items(): Promise { return await (await store.find()).toArray(); } -export async function get_item(item: string): Promise { +export async function get_item(item: string): Promise { return await store.findOne({ name: item }); } @@ -134,7 +134,7 @@ export async function create_item(store_item: StoreItem) { //assume name cannot be edited export async function edit_item(store_item: StoreItem) { - return await store.updateOne({ name: store_item.name }, store_item); + return await store.replaceOne({ name: store_item.name }, store_item); } export async function delete_item(item: string) { diff --git a/register.ts b/register.ts index 1cb79b9..3dff01f 100644 --- a/register.ts +++ b/register.ts @@ -203,6 +203,100 @@ const commands = [ }, ], }, + { + name: "buy", + description: "Buy an item from the store", + options: [ + { + type: 3, + name: "name", + description: "Name of the item", + required: true, + autocomplete: true, + }, + { + type: 4, + name: "quantity", + description: "Amount of the item to buy", + required: true, + }, + ], + }, + { + name: "use_item", + description: "Use an item (subtracts from your items)", + options: [ + { + type: 3, + name: "name", + description: "Name of the item", + required: true, + autocomplete: true, + }, + { + type: 4, + name: "quantity", + description: "Amount of the item to use", + required: true, + }, + ], + }, + { + name: "delete_item", + description: "Delete item from the store and all users (admin only)", + options: [ + { + type: 3, + name: "name", + description: "Name of the item", + required: true, + autocomplete: true, + }, + ], + }, + { + name: "edit_item", + description: "Create item (admin only)", + options: [ + { + type: 3, + name: "name", + description: "Name of the item", + required: true, + autocomplete: true, + }, + { + type: 5, + name: "delete_existing_roles", + description: "If true, deletes existing required roles as requirements", + required: true, + }, + { + type: 4, + name: "price", + description: "Price of the item", + required: false, + }, + { + type: 3, + name: "description", + description: "Description of the item", + required: false, + }, + { + type: 5, + name: "usable", + description: "Whether it can be /use'd", + required: false, + }, + { + type: 8, + name: "required_role", + description: "Roles that are required to buy this item.", + required: false, + }, + ], + }, ]; (new REST().setToken(process.env.DISCORD_TOKEN)).put(Routes.applicationCommands(process.env.CLIENT_ID), { body: commands }).then(() => console.log("Finished reloading slash commands")); diff --git a/util.ts b/util.ts index 7656b41..3e28ab9 100644 --- a/util.ts +++ b/util.ts @@ -1,6 +1,10 @@ +import type { ChatInputCommandInteraction, GuildMember } from "discord.js"; import type { UpdateResult } from "mongodb"; export function did_update(result: UpdateResult): boolean { return result.modifiedCount > 0; } +export function has_role(interaction: ChatInputCommandInteraction, role_id: string): boolean { + return (interaction.member as GuildMember).roles.cache.some((r) => r.id === role_id); +}