item transfer, item to buy other items, item income, unbuyable items
This commit is contained in:
@@ -30,4 +30,3 @@ const data: CommandData = {
|
|||||||
|
|
||||||
export default data;
|
export default data;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ChatInputCommandInteraction } from "discord.js";
|
|||||||
|
|
||||||
import type { CommandData } from "./index";
|
import type { CommandData } from "./index";
|
||||||
import type { StoreItem, User } from "../db";
|
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 { BotError } from "./common/error";
|
||||||
import { item_name_autocomplete } from "./common/autocompletes";
|
import { item_name_autocomplete } from "./common/autocompletes";
|
||||||
import { has_role } from "../util";
|
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");
|
if (quantity <= 0) throw new BotError("Can't buy 0 or less of an item. Nice try");
|
||||||
const item = await get_item(name);
|
const item = await get_item(name);
|
||||||
if (!item) throw new BotError("Item does not exist");
|
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) {
|
if (item.roles_required.length > 0) {
|
||||||
for (const role_id of item.roles_required) {
|
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 (!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;
|
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 (!(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);
|
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 = {
|
const data: CommandData = {
|
||||||
|
|||||||
16
commands/common/common.ts
Normal file
16
commands/common/common.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,33 +1,39 @@
|
|||||||
//also: edit_item
|
|
||||||
|
|
||||||
import type { ChatInputCommandInteraction } from "discord.js";
|
import type { ChatInputCommandInteraction } from "discord.js";
|
||||||
|
|
||||||
import type { CommandData } from "./index";
|
import type { CommandData } from "./index";
|
||||||
import type { Items, StoreItem, User } from "../db";
|
import type { Items, StoreItem, User } from "../db";
|
||||||
import { create_item, get_item } from "../db";
|
import { create_item, get_item } from "../db";
|
||||||
import { BotError } from "./common/error";
|
import { BotError } from "./common/error";
|
||||||
|
import { items_string_to_items } from "./common/common";
|
||||||
|
|
||||||
async function run(interaction: ChatInputCommandInteraction) {
|
async function run(interaction: ChatInputCommandInteraction) {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
const options = interaction.options;
|
const options = interaction.options;
|
||||||
const name: string = (await options.get("name")).value as string;
|
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 description: string = (await options.get("description")).value as string;
|
||||||
const usable: boolean = ((await options.get("usable"))?.value ?? true) as boolean;
|
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
|
//to add multiple roles, people will have to use /edit_item, I guess? augh
|
||||||
const required_role = (await options.get("required_role"))?.role;
|
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)
|
//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 (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 (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");
|
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 = {
|
const store_item: StoreItem = {
|
||||||
name,
|
name,
|
||||||
price,
|
price,
|
||||||
description,
|
description,
|
||||||
roles_required: required_role ? [required_role.id] : [],
|
roles_required: required_role ? [required_role.id] : [],
|
||||||
usable,
|
usable,
|
||||||
|
items,
|
||||||
};
|
};
|
||||||
await create_item(store_item);
|
await create_item(store_item);
|
||||||
return await interaction.editReply("Item created");
|
return await interaction.editReply("Item created");
|
||||||
@@ -35,7 +41,7 @@ async function run(interaction: ChatInputCommandInteraction) {
|
|||||||
|
|
||||||
const data: CommandData = {
|
const data: CommandData = {
|
||||||
name: "create_item",
|
name: "create_item",
|
||||||
description: "Create item",
|
description: "Create item, cannot be made unbuyable after creation",
|
||||||
registered_only: false,
|
registered_only: false,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
admin_only: true,
|
admin_only: true,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ async function run(interaction: ChatInputCommandInteraction) {
|
|||||||
const delete_existing_roles: boolean = (await options.get("delete_existing_roles")).value as boolean;
|
const delete_existing_roles: boolean = (await options.get("delete_existing_roles")).value as boolean;
|
||||||
const item = await get_item(name);
|
const item = await get_item(name);
|
||||||
if (!item) throw new BotError("No item of that name exists");
|
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 description: string = ((await options.get("description"))?.value ?? item.description) as string;
|
||||||
const usable: boolean = ((await options.get("usable"))?.value ?? item.usable) as boolean;
|
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
|
//to add multiple roles, people will have to use /edit_item, I guess? augh
|
||||||
const required_role = (await options.get("required_role"))?.role;
|
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)
|
//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
|
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 existing = delete_existing_roles ? [] : item.roles_required;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { EmbedBuilder } from "discord.js";
|
|||||||
import type { User } from "../db";
|
import type { User } from "../db";
|
||||||
import { get_user } from "../db";
|
import { get_user } from "../db";
|
||||||
import { BotError } from "./common/error";
|
import { BotError } from "./common/error";
|
||||||
import config from "../config.json";
|
import { is_admin } from "../util";
|
||||||
import { has_role } from "../util";
|
|
||||||
|
|
||||||
import say from "./say";
|
import say from "./say";
|
||||||
import roll from "./roll";
|
import roll from "./roll";
|
||||||
@@ -21,6 +20,8 @@ import buy from "./buy";
|
|||||||
import use_item from "./use_item";
|
import use_item from "./use_item";
|
||||||
import delete_item from "./delete_item";
|
import delete_item from "./delete_item";
|
||||||
import edit_item from "./edit_item";
|
import edit_item from "./edit_item";
|
||||||
|
import role_income from "./role_income";
|
||||||
|
import transfer_item from "./transfer_item";
|
||||||
|
|
||||||
export interface CommandData {
|
export interface CommandData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,11 +33,7 @@ export interface CommandData {
|
|||||||
autocomplete?: (interaction: AutocompleteInteraction) => Promise<any>;
|
autocomplete?: (interaction: AutocompleteInteraction) => Promise<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
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];
|
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];
|
||||||
|
|
||||||
function is_admin(interaction: ChatInputCommandInteraction): boolean {
|
|
||||||
return has_role(interaction, config.admin_role);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(interaction: ChatInputCommandInteraction, found: CommandData, name: string) {
|
async function run(interaction: ChatInputCommandInteraction, found: CommandData, name: string) {
|
||||||
//help command is "auto-generated"
|
//help command is "auto-generated"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { ChatInputCommandInteraction } from "discord.js";
|
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 { CommandData } from "./index";
|
||||||
import type { Items, User } from "../db";
|
import type { Items, User } from "../db";
|
||||||
import { get_user } from "../db";
|
import { get_user } from "../db";
|
||||||
import { BotError } from "./common/error";
|
import { BotError } from "./common/error";
|
||||||
|
import { gen_action_row } from "../util";
|
||||||
|
|
||||||
async function run(interaction: ChatInputCommandInteraction, user: User) {
|
async function run(interaction: ChatInputCommandInteraction, user: User) {
|
||||||
function gen_items_embed(items_target, items: Items, page: number, pages: number) {
|
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;
|
return items_embed;
|
||||||
}
|
}
|
||||||
function gen_action_row(page: number, pages: number) {
|
|
||||||
let action_row = new ActionRowBuilder<ButtonBuilder>();
|
|
||||||
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 options = interaction.options;
|
||||||
const target = (await options.get("target"))?.user;
|
const target = (await options.get("target"))?.user;
|
||||||
let items_target = target ?? interaction.user;
|
let items_target = target ?? interaction.user;
|
||||||
|
|||||||
94
commands/role_income.ts
Normal file
94
commands/role_income.ts
Normal file
@@ -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;
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ async function run(interaction: ChatInputCommandInteraction) {
|
|||||||
.map(
|
.map(
|
||||||
(store_item: StoreItem) =>
|
(store_item: StoreItem) =>
|
||||||
({
|
({
|
||||||
name: `${store_item.name} (${store_item.price} ${ store_item.price === 1 ? config.currency : config.currency_plural })`,
|
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("")}` }`,
|
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(" + ") : "" }`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
35
commands/transfer_item.ts
Normal file
35
commands/transfer_item.ts
Normal file
@@ -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;
|
||||||
|
|
||||||
@@ -30,5 +30,3 @@ const data: CommandData = {
|
|||||||
|
|
||||||
export default data;
|
export default data;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"server": "0000000000000000001",
|
||||||
"currency": "credit",
|
"currency": "credit",
|
||||||
"currency_plural": "credits",
|
"currency_plural": "credits",
|
||||||
"admin_role": "000000000000000000"
|
"admin_role": "0000000000000000001"
|
||||||
}
|
}
|
||||||
|
|||||||
49
db.ts
49
db.ts
@@ -5,14 +5,14 @@ import { did_update } from "./util";
|
|||||||
//figure out the options and whatnot later
|
//figure out the options and whatnot later
|
||||||
const client = new MongoClient(process.env.MONGO_CONNECTION_STRING);
|
const client = new MongoClient(process.env.MONGO_CONNECTION_STRING);
|
||||||
|
|
||||||
let store, users, roleincome;
|
let store, users, role_income;
|
||||||
|
|
||||||
client.connect().then(() => {
|
client.connect().then(() => {
|
||||||
console.log("Connected to the database");
|
console.log("Connected to the database");
|
||||||
const db = client.db("db");
|
const db = client.db("db");
|
||||||
store = db.collection("store");
|
store = db.collection("store");
|
||||||
users = db.collection("users");
|
users = db.collection("users");
|
||||||
roleincome = db.collection("roleincome");
|
role_income = db.collection("role_income");
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,10 +22,11 @@ export type Items = Record<string, number>;
|
|||||||
|
|
||||||
export interface StoreItem {
|
export interface StoreItem {
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price?: number; //if no price, not buyable and technically not a store item, but whatever
|
||||||
description: string;
|
description: string;
|
||||||
roles_required: string[];
|
roles_required: string[];
|
||||||
usable: boolean;
|
usable: boolean;
|
||||||
|
items?: [string, number][];
|
||||||
//
|
//
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,6 +38,11 @@ export interface User {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface RoleIncome {
|
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<User | undefined> {
|
|||||||
return await users.findOne({ user });
|
return await users.findOne({ user });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function get_all_users(): Promise<User[]> {
|
||||||
|
return await (await users.find()).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
export async function add_balance(user: string, amount: number) {
|
export async function add_balance(user: string, amount: number) {
|
||||||
return await users.updateOne({ user }, {
|
return await users.updateOne({ user }, {
|
||||||
$inc: {
|
$inc: {
|
||||||
@@ -142,5 +152,36 @@ export async function delete_item(item: string) {
|
|||||||
return await store.deleteOne({ name: item });
|
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<RoleIncome[]> {
|
||||||
|
return await (await role_income.find()).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_role_income(role: string): Promise<RoleIncome> {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
6
index.ts
6
index.ts
@@ -5,11 +5,13 @@ config();
|
|||||||
|
|
||||||
import {} from "./db";
|
import {} from "./db";
|
||||||
import handle_interaction from "./commands";
|
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 () => {
|
client.on("ready", async () => {
|
||||||
console.log(`Logged in as ${client.user.tag}`);
|
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);
|
||||||
|
|
||||||
|
|||||||
100
register.ts
100
register.ts
@@ -138,7 +138,7 @@ const commands = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_item",
|
name: "create_item",
|
||||||
description: "Create item (admin only)",
|
description: "Create item, cannot be made unbuyable after creation (admin only)",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
type: 3,
|
type: 3,
|
||||||
@@ -146,18 +146,18 @@ const commands = [
|
|||||||
description: "Name of the item",
|
description: "Name of the item",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 4,
|
|
||||||
name: "price",
|
|
||||||
description: "Price of the item",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 3,
|
type: 3,
|
||||||
name: "description",
|
name: "description",
|
||||||
description: "Description of the item",
|
description: "Description of the item",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 4,
|
||||||
|
name: "price",
|
||||||
|
description: "Price of the item (omit to make unbuyable)",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 5,
|
type: 5,
|
||||||
name: "usable",
|
name: "usable",
|
||||||
@@ -170,6 +170,12 @@ const commands = [
|
|||||||
description: "Roles that are required to buy this item. /edit_item to add multiple",
|
description: "Roles that are required to buy this item. /edit_item to add multiple",
|
||||||
required: false,
|
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"));
|
(new REST().setToken(process.env.DISCORD_TOKEN)).put(Routes.applicationCommands(process.env.CLIENT_ID), { body: commands }).then(() => console.log("Finished reloading slash commands"));
|
||||||
|
|||||||
50
role_income.ts
Normal file
50
role_income.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2020",
|
"target": "es2021",
|
||||||
"module": "node16",
|
"module": "node16",
|
||||||
"moduleResolution": "node16",
|
"moduleResolution": "node16",
|
||||||
"typeRoots": ["./node_modules/@types"],
|
"typeRoots": ["./node_modules/@types"],
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"lib": ["ES2020"],
|
"lib": ["ES2021"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
33
util.ts
33
util.ts
@@ -1,6 +1,9 @@
|
|||||||
import type { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
import type { ChatInputCommandInteraction, GuildMember } from "discord.js";
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||||
import type { UpdateResult } from "mongodb";
|
import type { UpdateResult } from "mongodb";
|
||||||
|
|
||||||
|
import config from "./config.json";
|
||||||
|
|
||||||
export function did_update(result: UpdateResult): boolean {
|
export function did_update(result: UpdateResult): boolean {
|
||||||
return result.modifiedCount > 0;
|
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 {
|
export function has_role(interaction: ChatInputCommandInteraction, role_id: string): boolean {
|
||||||
return (interaction.member as GuildMember).roles.cache.some((r) => r.id === role_id);
|
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<ButtonBuilder>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user