item transfer, item to buy other items, item income, unbuyable items

This commit is contained in:
stjet
2024-09-13 18:17:44 +00:00
parent 3145f52df7
commit c2d1cd2af1
18 changed files with 407 additions and 56 deletions

View File

@@ -30,4 +30,3 @@ const data: CommandData = {
export default data; export default data;

View File

@@ -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
View 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;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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"

View File

@@ -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
View 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;

View File

@@ -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
View 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;

View File

@@ -30,5 +30,3 @@ const data: CommandData = {
export default data; export default data;

View File

@@ -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
View File

@@ -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 });
}

View File

@@ -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);

View File

@@ -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
View 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);
}

View File

@@ -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
View File

@@ -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;
}