From c2d1cd2af1ee8b6ed408393d97ab74320b12e35f Mon Sep 17 00:00:00 2001 From: stjet <49297268+stjet@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:17:44 +0000 Subject: [PATCH] item transfer, item to buy other items, item income, unbuyable items --- commands/bal.ts | 1 - commands/buy.ts | 13 ++++- commands/common/common.ts | 16 ++++++ commands/create_item.ts | 18 ++++--- commands/edit_item.ts | 4 +- commands/index.ts | 11 ++--- commands/items.ts | 20 +------- commands/role_income.ts | 94 +++++++++++++++++++++++++++++++++++ commands/store.ts | 4 +- commands/transfer_item.ts | 35 +++++++++++++ commands/use_item.ts | 2 - config.json.example | 3 +- db.ts | 49 +++++++++++++++++-- index.ts | 6 ++- register.ts | 100 +++++++++++++++++++++++++++++++++++--- role_income.ts | 50 +++++++++++++++++++ tsconfig.json | 4 +- util.ts | 33 +++++++++++++ 18 files changed, 407 insertions(+), 56 deletions(-) create mode 100644 commands/common/common.ts create mode 100644 commands/role_income.ts create mode 100644 commands/transfer_item.ts create mode 100644 role_income.ts diff --git a/commands/bal.ts b/commands/bal.ts index e01bbbd..3588772 100644 --- a/commands/bal.ts +++ b/commands/bal.ts @@ -30,4 +30,3 @@ const data: CommandData = { export default data; - diff --git a/commands/buy.ts b/commands/buy.ts index 3645be0..5e1d2d4 100644 --- a/commands/buy.ts +++ b/commands/buy.ts @@ -2,7 +2,7 @@ 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 { get_item, add_item_to_user, sub_item_to_user, sub_balance } from "../db"; import { BotError } from "./common/error"; import { item_name_autocomplete } from "./common/autocompletes"; import { has_role } from "../util"; @@ -15,15 +15,24 @@ async function run(interaction: ChatInputCommandInteraction, user: User) { 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.price) throw new BotError("Item is not buyable"); 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"); } } + if (item.items) { + if (!item.items.every((itemm) => user.items[itemm[0]] >= itemm[1])) throw new BotError("Don't have enough of one of the items"); + } const total_cost = item.price * quantity; if (!(await sub_balance(user.user, total_cost))) throw new BotError("Not enough balance to buy this item"); + if (item.items) { + for (const itemm of item.items) { + await sub_item_to_user(user.user, itemm[0], itemm[1] * quantity); + } + } 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 }`); + return await interaction.editReply(`Bought ${quantity} of \`${name}\` for ${total_cost} ${ total_cost === 1 ? config.currency : config.currency_plural }${ item.items ? " and other items" : "" }`); } const data: CommandData = { diff --git a/commands/common/common.ts b/commands/common/common.ts new file mode 100644 index 0000000..d267343 --- /dev/null +++ b/commands/common/common.ts @@ -0,0 +1,16 @@ +import { get_item } from "../../db"; + +//if return string, that is error message +export async function items_string_to_items(items_string: string): Promise<[string, number][] | string> { + let items = []; + for (const item of items_string.split("|")) { + const parts = item.split(","); + if (parts.length !== 2) return "Incorrect items format, must be `name,quantity|name,quantity`, etc"; + const quantity = Number(parts[1]); + if (isNaN(quantity) || Math.floor(quantity) !== quantity) return "Item quantity was not an integer"; + if (!await get_item(parts[0])) return `Item \`${parts[0].replaceAll("`", "\\`")}\` does not exist`; + items.push([parts[0], quantity]); + } + return items; +} + diff --git a/commands/create_item.ts b/commands/create_item.ts index 28b464e..59138c0 100644 --- a/commands/create_item.ts +++ b/commands/create_item.ts @@ -1,33 +1,39 @@ -//also: edit_item - import type { ChatInputCommandInteraction } from "discord.js"; import type { CommandData } from "./index"; import type { Items, StoreItem, User } from "../db"; import { create_item, get_item } from "../db"; import { BotError } from "./common/error"; +import { items_string_to_items } from "./common/common"; async function run(interaction: ChatInputCommandInteraction) { await interaction.deferReply(); const options = interaction.options; const name: string = (await options.get("name")).value as string; - const price: number = (await options.get("price")).value as number; + const price = (await options.get("price"))?.value as (number | undefined); const description: string = (await options.get("description")).value as string; const usable: boolean = ((await options.get("usable"))?.value ?? true) as boolean; - if (name.includes("`")) throw new BotError("Item name cannot include the ` character"); //don't want to escape shit + const items_string = (await options.get("items"))?.value as (string | undefined); + if (name.includes("`") || name.includes(",") || name.includes("|")) throw new BotError("Item name cannot include the following characters:`|,"); //don't want to escape shit //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"); + if (price <= 0) throw new BotError("Price cannot be zero or negative"); //undefined < 0 is false btw //name and description char limits (based on discord embed field name/value limits) if (name.length > 200) throw new BotError("Item name cannot be more than 256 characters"); //true limit is 256 (still might error if currency name is more than like 50 characters, or price is absurdly huge) 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 if (await get_item(name)) throw new BotError("Item with that name already exists. Use a different name, or /edit_item or /delete_item"); + let items; + if (items_string) { + items = await items_string_to_items(items_string); + if (typeof items === "string") throw new BotError(items); + } const store_item: StoreItem = { name, price, description, roles_required: required_role ? [required_role.id] : [], usable, + items, }; await create_item(store_item); return await interaction.editReply("Item created"); @@ -35,7 +41,7 @@ async function run(interaction: ChatInputCommandInteraction) { const data: CommandData = { name: "create_item", - description: "Create item", + description: "Create item, cannot be made unbuyable after creation", registered_only: false, ephemeral: false, admin_only: true, diff --git a/commands/edit_item.ts b/commands/edit_item.ts index a3e52fe..f72fcfa 100644 --- a/commands/edit_item.ts +++ b/commands/edit_item.ts @@ -13,12 +13,12 @@ async function run(interaction: ChatInputCommandInteraction) { 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 price = ((await options.get("price"))?.value ?? item.price) as (number | undefined); 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"); + if (price <= 0) throw new BotError("Price cannot be zero or 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; diff --git a/commands/index.ts b/commands/index.ts index 8eab354..078df35 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -4,8 +4,7 @@ import { EmbedBuilder } from "discord.js"; 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 { is_admin } from "../util"; import say from "./say"; import roll from "./roll"; @@ -21,6 +20,8 @@ import buy from "./buy"; import use_item from "./use_item"; import delete_item from "./delete_item"; import edit_item from "./edit_item"; +import role_income from "./role_income"; +import transfer_item from "./transfer_item"; export interface CommandData { name: string; @@ -32,11 +33,7 @@ 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, buy, use_item, delete_item, edit_item]; - -function is_admin(interaction: ChatInputCommandInteraction): boolean { - return has_role(interaction, config.admin_role); -} +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, role_income, transfer_item]; async function run(interaction: ChatInputCommandInteraction, found: CommandData, name: string) { //help command is "auto-generated" diff --git a/commands/items.ts b/commands/items.ts index 7b4a0ea..c7fb051 100644 --- a/commands/items.ts +++ b/commands/items.ts @@ -1,10 +1,11 @@ import type { ChatInputCommandInteraction } from "discord.js"; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { EmbedBuilder } from "discord.js"; import type { CommandData } from "./index"; import type { Items, User } from "../db"; import { get_user } from "../db"; import { BotError } from "./common/error"; +import { gen_action_row } from "../util"; async function run(interaction: ChatInputCommandInteraction, user: User) { function gen_items_embed(items_target, items: Items, page: number, pages: number) { @@ -27,23 +28,6 @@ async function run(interaction: ChatInputCommandInteraction, user: User) { } return items_embed; } - function gen_action_row(page: number, pages: number) { - let action_row = new ActionRowBuilder(); - let action_prev = new ButtonBuilder() - .setCustomId(String(page - 1)) - .setLabel("Prev") - .setEmoji("⬅️") - .setStyle(ButtonStyle.Primary) - .setDisabled(page - 1 === 0); - let action_next = new ButtonBuilder() - .setCustomId(String(page + 1)) - .setLabel("Next") - .setEmoji("➡️") - .setStyle(ButtonStyle.Primary) - .setDisabled(page + 1 > pages); - action_row.addComponents(action_prev, action_next); - return action_row; - } const options = interaction.options; const target = (await options.get("target"))?.user; let items_target = target ?? interaction.user; diff --git a/commands/role_income.ts b/commands/role_income.ts new file mode 100644 index 0000000..2c8b033 --- /dev/null +++ b/commands/role_income.ts @@ -0,0 +1,94 @@ +import type { ChatInputCommandInteraction } from "discord.js"; +import { EmbedBuilder } from "discord.js"; + +import type { CommandData } from "./index"; +import { BotError } from "./common/error"; +import type { RoleIncome } from "../db"; +import { create_role_income, delete_role_income, get_all_role_income, get_item } from "../db"; +import { gen_action_row, is_admin } from "../util"; +import { items_string_to_items } from "./common/common"; +import config from "../config.json"; + +//subcommands: list, create, delete + +async function run(interaction: ChatInputCommandInteraction) { + await interaction.deferReply(); + const options = interaction.options; + const subcommand = options.getSubcommand(); + if (subcommand === "list") { + const role_incomes = await get_all_role_income(); + function gen_role_incomes_embed(role_incomes: RoleIncome[], page: number, pages: number) { + let role_income_embed = new EmbedBuilder(); + role_income_embed.setTitle(`Role Incomes (Page ${page}/${pages})`); + if (Object.keys(role_incomes).length === 0) { + role_income_embed.setDescription("No role incomes"); + } else { + role_income_embed.addFields( + role_incomes + .slice((page - 1) * 10, page * 10) + .map( + (role_income) => + ({ + name: `${role_income.income} ${config.currency} every ${role_income.hours} hour(s)`, + value: `<@&${role_income.role}>${ role_income.items ? " (also gives " + role_income.items.map((item) => item[1] + " of `" + item[0] + "`").join(" + ") + ")" : "" }`, + }) + ) + ); + } + return role_income_embed; + } + const pages: number = Math.ceil(role_incomes.length / 10) || 1; //min of 1 + let page = 1; + const dresp = await interaction.editReply({ + embeds: [ gen_role_incomes_embed(role_incomes, page, pages) ], + components: [ gen_action_row(page, pages) ], + }); + while (true) { + try { + let dresp_bin = await dresp.awaitMessageComponent({ filter: (bin) => bin.user.id === interaction.user.id, time: 60000 }); //bin = button interaction + page = Number(dresp_bin.customId); + await dresp_bin.update({ + embeds: [ gen_role_incomes_embed(role_incomes, page, pages) ], + components: [ gen_action_row(page, pages) ], + }); + } catch (_) { + //errors when people stop pressing the button + return; + } + } + return await interaction.editReply("```json\n"+JSON.stringify(role_incomes)+"\n```"); + } else if (is_admin(interaction)) { + const role_id: string = (await options.get("role")).role.id; + if (subcommand === "create") { + //hour, income + const hours: number = (await options.get("hours")).value as number; + //can be negative or zero + const income: number = (await options.get("income")).value as number; + const items_string = (await options.get("items"))?.value as (string | undefined); + let items; + if (items_string) { + items = await items_string_to_items(items_string); + if (typeof items === "string") throw new BotError(items); + } + await create_role_income(role_id, hours, income, items); + return await interaction.editReply("Created role income"); + } else if (subcommand === "delete") { + await delete_role_income(role_id); + return await interaction.editReply("Deleted role income"); + } + } else { + throw new BotError("Admin permission needed to run that command"); + } +} + +const data: CommandData = { + name: "role_income", + description: "View, create, or delete role incomes", + registered_only: false, + ephemeral: false, + admin_only: false, + run, +}; + +export default data; + diff --git a/commands/store.ts b/commands/store.ts index c431c13..e8258fa 100644 --- a/commands/store.ts +++ b/commands/store.ts @@ -22,8 +22,8 @@ async function run(interaction: ChatInputCommandInteraction) { .map( (store_item: StoreItem) => ({ - name: `${store_item.name} (${store_item.price} ${ store_item.price === 1 ? config.currency : config.currency_plural })`, - value: `${store_item.description}\nUsable: ${store_item.usable}${ store_item.roles_required.length === 0 ? "" : `\nRoles required: ${store_item.roles_required.map((role_id) => `<@&${role_id}>`).join("")}` }`, + name: `${store_item.name} (${ !store_item.price ? "unbuyable" : `${store_item.price} ${ store_item.price === 1 ? config.currency : config.currency_plural }` })`, + value: `${store_item.description}\nUsable: ${store_item.usable}${ store_item.roles_required.length === 0 ? "" : `\nRoles required: ${store_item.roles_required.map((role_id) => `<@&${role_id}>`).join("")}` }${ store_item.items ? "\nItems used: " + store_item.items.map((item) => `${item[1]} of \`${item[0]}\``).join(" + ") : "" }`, }) ) ); diff --git a/commands/transfer_item.ts b/commands/transfer_item.ts new file mode 100644 index 0000000..9cb2261 --- /dev/null +++ b/commands/transfer_item.ts @@ -0,0 +1,35 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { StoreItem, User } from "../db"; +import { get_item, get_user, add_item_to_user, 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 target_id: string = (await options.get("target")).user.id; + const quantity: number = (await options.get("quantity")).value as number; + if (quantity <= 0) throw new BotError("Can't transfer 0 or less of an item"); + let trans_user = await get_user(target_id); + if (!trans_user) throw new BotError("Target is not registered"); + const item = await get_item(name); + if (!item) throw new BotError("Item does not exist"); + if (!(await sub_item_to_user(user.user, name, quantity))) throw new BotError("You did not have enough of that item to transfer"); + await add_item_to_user(target_id, item.name, quantity); + return await interaction.editReply(`Transferred ${quantity} of \`${name}\` to <@${target_id}>`); +} + +const data: CommandData = { + name: "transfer_item", + description: "Give a(n) item(s) to another user", + 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/use_item.ts b/commands/use_item.ts index fc40c44..dba2261 100644 --- a/commands/use_item.ts +++ b/commands/use_item.ts @@ -30,5 +30,3 @@ const data: CommandData = { export default data; - - diff --git a/config.json.example b/config.json.example index a79326b..76646eb 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,6 @@ { + "server": "0000000000000000001", "currency": "credit", "currency_plural": "credits", - "admin_role": "000000000000000000" + "admin_role": "0000000000000000001" } diff --git a/db.ts b/db.ts index fbcf8e2..912c84d 100644 --- a/db.ts +++ b/db.ts @@ -5,14 +5,14 @@ import { did_update } from "./util"; //figure out the options and whatnot later const client = new MongoClient(process.env.MONGO_CONNECTION_STRING); -let store, users, roleincome; +let store, users, role_income; client.connect().then(() => { console.log("Connected to the database"); const db = client.db("db"); store = db.collection("store"); users = db.collection("users"); - roleincome = db.collection("roleincome"); + role_income = db.collection("role_income"); // }); @@ -22,10 +22,11 @@ export type Items = Record; export interface StoreItem { name: string; - price: number; + price?: number; //if no price, not buyable and technically not a store item, but whatever description: string; roles_required: string[]; usable: boolean; + items?: [string, number][]; // }; @@ -37,6 +38,11 @@ export interface User { }; export interface RoleIncome { + role: `${number}`; //role id + hours: number; //every x hours, give income + income: number; + last_claim: number; + items?: [string, number][]; // }; @@ -58,6 +64,10 @@ export async function get_user(user: string): Promise { return await users.findOne({ user }); } +export async function get_all_users(): Promise { + return await (await users.find()).toArray(); +} + export async function add_balance(user: string, amount: number) { return await users.updateOne({ user }, { $inc: { @@ -142,5 +152,36 @@ export async function delete_item(item: string) { return await store.deleteOne({ name: item }); } -// +//role income collection db functions +//actual role income payouts done with setTimeout and setInterval + +export async function get_all_role_income(): Promise { + return await (await role_income.find()).toArray(); +} + +export async function get_role_income(role: string): Promise { + return await role_income.findOne({ role }); +} + +export async function create_role_income(role: string, hours: number, income: number, items?: string[]) { + return await role_income.insertOne({ + role, + hours, + income, + items, + last_claim: Date.now(), + }); +} + +export async function update_role_income_last_claim(role: string) { + return await role_income.updateOne({ role }, { + $set: { + last_claim: Date.now(), + }, + }); +} + +export async function delete_role_income(role: string) { + return await role_income.deleteOne({ role }); +} diff --git a/index.ts b/index.ts index a9d0a54..0743a3a 100644 --- a/index.ts +++ b/index.ts @@ -5,11 +5,13 @@ config(); import {} from "./db"; import handle_interaction from "./commands"; +import role_income_poll from "./role_income"; -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); client.on("ready", async () => { console.log(`Logged in as ${client.user.tag}`); + role_income_poll(client); // }); @@ -19,5 +21,5 @@ client.on("interactionCreate", async (interaction: BaseInteraction) => { } }); -setTimeout(() => client.login(process.env.DISCORD_TOKEN), 3000); +setTimeout(() => client.login(process.env.DISCORD_TOKEN), 3500); diff --git a/register.ts b/register.ts index 3dff01f..55e5a57 100644 --- a/register.ts +++ b/register.ts @@ -138,7 +138,7 @@ const commands = [ }, { name: "create_item", - description: "Create item (admin only)", + description: "Create item, cannot be made unbuyable after creation (admin only)", options: [ { type: 3, @@ -146,18 +146,18 @@ const commands = [ description: "Name of the item", required: true, }, - { - type: 4, - name: "price", - description: "Price of the item", - required: true, - }, { type: 3, name: "description", description: "Description of the item", required: true, }, + { + type: 4, + name: "price", + description: "Price of the item (omit to make unbuyable)", + required: false, + }, { type: 5, name: "usable", @@ -170,6 +170,12 @@ const commands = [ description: "Roles that are required to buy this item. /edit_item to add multiple", required: false, }, + { + type: 3, + name: "items", + description: "Items to give along with role income. In format name,quantity|name,quantity", + required: false, + }, // ], }, @@ -297,6 +303,86 @@ const commands = [ }, ], }, + { + name: "role_income", + description: "See various role income related actions", + options: [ + { + type: 1, + name: "list", + description: "List all role incomes", + }, + { + type: 1, + name: "create", + description: "Create a role income (admin only)", + options: [ + { + type: 8, + name: "role", + description: "Role to give role income to", + required: true, + }, + { + type: 4, + name: "hours", + description: "Number of hours between payouts", + required: true, + }, + { + type: 4, + name: "income", + description: "Amount to give per user per payout", + required: true, + }, + { + type: 3, + name: "items", + description: "Items to give along with role income. In format name,quantity|name,quantity", + required: false, + }, + ], + }, + { + type: 1, + name: "delete", + description: "Delete a role income (admin only)", + options: [ + { + type: 8, + name: "role", + description: "Role to give delete", + required: true, + }, + ], + }, + ] + }, + { + name: "transfer_item", + description: "Give a(n) item(s) to another user", + options: [ + { + type: 3, + name: "name", + description: "Name of the item", + required: true, + autocomplete: true, + }, + { + type: 6, + name: "target", + description: "The user to send to", + required: true, + }, + { + type: 4, + name: "quantity", + description: "Amount to transfer", + required: true, + }, + ], + }, ]; (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/role_income.ts b/role_income.ts new file mode 100644 index 0000000..e00fc32 --- /dev/null +++ b/role_income.ts @@ -0,0 +1,50 @@ +import type { Client, TextChannel } from "discord.js"; +import { add_balance, add_item_to_user, get_all_role_income, get_all_users, update_role_income_last_claim } from "./db"; +import { calc_role_income_claim } from "./util"; +import config from "./config.json"; +//some random discord imports too + +export default function main(client: Client) { + //possible: get all role_incomes, set up setTimeouts and setIntervals specifically for each of them + //for now just poll at startup and every 1/3 hour + async function role_income_poll() { + const all_users = await get_all_users(); + const guild = await client.guilds.fetch(config.server); + await guild.members.fetch(); + const server_roles = guild.roles; + const role_incomes = await get_all_role_income(); + for (const role_income of role_incomes) { + //see if it is the right time (at least x hours has passed) + if (Date.now() < role_income.last_claim + role_income.hours * 60 * 60 * 1000) continue; + //get discord users with that role + const role_members = (await server_roles.fetch(role_income.role)).members; + const [payout, cycles] = calc_role_income_claim(role_income.last_claim, role_income.hours, role_income.income); + //filter out non-registered + role_members.filter( + (user) => all_users.some((user_info) => user_info.user === user.id) + ).each(async (user) => { + //pay them + await add_balance(user.id, payout); + await (client.channels.cache.get(config.role_income_channel) as TextChannel).send({ + content: `Paid ${payout} (${role_income.income} * ${cycles}) ${ payout === 1 ? config.currency : config.currency_plural } to <@${user.id}> for <@&${role_income.role}>`, + allowedMentions: {}, + }); + if (role_income.items) { + for (const item of role_income.items) { + const [given, _] = calc_role_income_claim(role_income.last_claim, role_income.hours, item[1]); + await add_item_to_user(user.id, item[0], given); + await (client.channels.cache.get(config.role_income_channel) as TextChannel).send({ + content: `Gave ${given} (${item[1]} * ${cycles}) of \`${item[0]}\` to <@${user.id}> for <@&${role_income.role}>`, + allowedMentions: {}, + }); + } + } + }); + //then update db with new latest claim time + await update_role_income_last_claim(role_income.role); + } + } + role_income_poll(); + setInterval(role_income_poll, 20 * 60 * 1000); +} + diff --git a/tsconfig.json b/tsconfig.json index dfd0076..d54dbd9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2020", + "target": "es2021", "module": "node16", "moduleResolution": "node16", "typeRoots": ["./node_modules/@types"], @@ -9,6 +9,6 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, - "lib": ["ES2020"], + "lib": ["ES2021"], "exclude": ["node_modules"] } diff --git a/util.ts b/util.ts index 3e28ab9..990ac88 100644 --- a/util.ts +++ b/util.ts @@ -1,6 +1,9 @@ import type { ChatInputCommandInteraction, GuildMember } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import type { UpdateResult } from "mongodb"; +import config from "./config.json"; + export function did_update(result: UpdateResult): boolean { return result.modifiedCount > 0; } @@ -8,3 +11,33 @@ export function did_update(result: UpdateResult): boolean { export function has_role(interaction: ChatInputCommandInteraction, role_id: string): boolean { return (interaction.member as GuildMember).roles.cache.some((r) => r.id === role_id); } + +export function is_admin(interaction: ChatInputCommandInteraction): boolean { + return has_role(interaction, config.admin_role); +} + +//calculate payout +export function calc_role_income_claim(last_claim: number, hours: number, income: number): [number, number] { + const hours_since = (Date.now() - last_claim) / (60 * 60 * 1000); + const cycles = Math.floor(hours_since / hours); + return [cycles * income, cycles]; +} + +export function gen_action_row(page: number, pages: number) { + let action_row = new ActionRowBuilder(); + let action_prev = new ButtonBuilder() + .setCustomId(String(page - 1)) + .setLabel("Prev") + .setEmoji("⬅️") + .setStyle(ButtonStyle.Primary) + .setDisabled(page - 1 === 0); + let action_next = new ButtonBuilder() + .setCustomId(String(page + 1)) + .setLabel("Next") + .setEmoji("➡️") + .setStyle(ButtonStyle.Primary) + .setDisabled(page + 1 > pages); + action_row.addComponents(action_prev, action_next); + return action_row; +} +