diff --git a/.gitignore b/.gitignore index 180bd6a..8431937 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env node_modules *.js +config.json diff --git a/commands/bal.ts b/commands/bal.ts new file mode 100644 index 0000000..b8b0111 --- /dev/null +++ b/commands/bal.ts @@ -0,0 +1,35 @@ +import type { ChatInputCommandInteraction } from "discord.js"; +import { EmbedBuilder } from "discord.js"; + +import type { CommandData } from "./index"; +import type { User } from "../db"; +import config from "../config.json"; +import { get_user } from "../db"; +import { BotError } from "./error"; + +async function run(interaction: ChatInputCommandInteraction, user: User) { + const options = interaction.options; + const target = (await options.get("target"))?.user; + let embeds = []; + let bal_embed = new EmbedBuilder(); + let bal_target = target ?? interaction.user; + let bal_user = target ? await get_user(target.id) : user; + if (!bal_user) throw new BotError("Target is not registered"); //must be target + bal_embed.setTitle(`${bal_target.tag}'s Balance`); + bal_embed.setDescription(`${bal_user.balance} ${ bal_user.balance === 1 ? config.currency : config.currency_plural }`); + embeds.push(bal_embed); + return await interaction.editReply({ embeds }); +} + +const data: CommandData = { + name: "bal", + description: "Show you or someone else's balance", + registered_only: true, + ephemeral: false, + admin_only: false, + run, +}; + +export default data; + + diff --git a/commands/change_bal.ts b/commands/change_bal.ts new file mode 100644 index 0000000..c6ba039 --- /dev/null +++ b/commands/change_bal.ts @@ -0,0 +1,34 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { User } from "../db"; +import { get_user, add_balance, sub_balance } from "../db"; +import { BotError } from "./error"; + +async function run(interaction: ChatInputCommandInteraction, _user: User) { + const options = interaction.options; + const target_id: string = (await options.get("target")).user.id; + const amount: number = (await options.get("amount")).value as number; + const negative_allowed: boolean = ((await options.get("negative_allowed"))?.value ?? false) as boolean; + let change_user = await get_user(target_id); + if (!change_user) throw new BotError("Target is not registered"); + if (amount >= 0) { + await add_balance(target_id, amount); + } else { + const success = await sub_balance(target_id, -amount, negative_allowed); + if (!success) throw new BotError("Failed because would make target balance negative, so `negative_allowed` must be true to do this"); + } + return await interaction.editReply(`Changed <@${target_id}> balance by ${amount}`); +} + +const data: CommandData = { + name: "change_bal", + description: "Change a user's balance", + registered_only: true, + ephemeral: false, + admin_only: true, + run, +}; + +export default data; + diff --git a/commands/index.ts b/commands/index.ts index 1ea2694..158a72b 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -1,28 +1,31 @@ +import type { ChatInputCommandInteraction, GuildMember } from "discord.js"; import { EmbedBuilder } from "discord.js"; -import type { ChatInputCommandInteraction } from "discord.js"; +import type { User } from "../db"; +import { get_user } from "../db"; import { BotError } from "./error"; +import config from "../config.json"; import say from "./say"; import roll from "./roll"; - +import register_user from "./register_user"; +import bal from "./bal"; +import change_bal from "./change_bal"; +import transfer from "./transfer"; export interface CommandData { name: string; description: string; + registered_only: boolean; //also means interaction will get deferReply()ed ephemeral: boolean; admin_only: boolean; - run: (interaction: ChatInputCommandInteraction) => Promise; - // + run: (interaction: ChatInputCommandInteraction, user?: User) => Promise; }; -const commands: CommandData[] = [say, roll]; +const commands: CommandData[] = [say, roll, register_user, bal, change_bal, transfer]; -//todo: look from config. also, have a config function is_admin(interaction: ChatInputCommandInteraction): boolean { - // - //placeholder - return true; + return (interaction.member as GuildMember).roles.cache.some((r) => r.id === config.admin_role); } export default async function run(interaction: ChatInputCommandInteraction) { @@ -59,7 +62,15 @@ export default async function run(interaction: ChatInputCommandInteraction) { try { //admin stuff should be ideally handled by register.ts, but this is a fallback if (found.admin_only && !is_admin(interaction)) throw new BotError("Admin permission needed to run that command"); + if (found.registered_only) { + await interaction.deferReply(); + const user = await get_user(interaction.user.id); + if (!user) throw new BotError("You must be registered by an admin to use this command"); + await found.run(interaction, user); + return; + } await found.run(interaction); + return; } catch (e) { if (e instanceof BotError) { //send error message to that channel diff --git a/commands/register_user.ts b/commands/register_user.ts new file mode 100644 index 0000000..6e82b1f --- /dev/null +++ b/commands/register_user.ts @@ -0,0 +1,26 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import { get_user, add_new_user } from "../db"; +import { BotError } from "./error"; + +async function run(interaction: ChatInputCommandInteraction) { + await interaction.deferReply(); + const options = interaction.options; + const target_id = (await options.get("target")).user.id; + if (await get_user(target_id)) throw new BotError("Target is already registered"); + await add_new_user(target_id); + return await interaction.editReply({ content: `Registered <@${target_id}>`, allowedMentions: { users: [] } }); +} + +const data: CommandData = { + name: "register_user", + description: "Register a user so they can participate in the bot economy", + registered_only: false, + ephemeral: false, + admin_only: true, + run, +}; + +export default data; + diff --git a/commands/roll.ts b/commands/roll.ts index 305165b..acdd0da 100644 --- a/commands/roll.ts +++ b/commands/roll.ts @@ -29,10 +29,10 @@ async function run(interaction: ChatInputCommandInteraction) { const data: CommandData = { name: "roll", description: "Roll dice", + registered_only: false, ephemeral: false, admin_only: false, run, - // }; export default data; diff --git a/commands/say.ts b/commands/say.ts index 7653fd8..60d1dcd 100644 --- a/commands/say.ts +++ b/commands/say.ts @@ -25,10 +25,10 @@ async function run(interaction: ChatInputCommandInteraction) { const data: CommandData = { name: "say", description: "Have the bot say something in a channel", + registered_only: false, ephemeral: true, admin_only: true, run, - // }; export default data; diff --git a/commands/transfer.ts b/commands/transfer.ts new file mode 100644 index 0000000..a6a57b5 --- /dev/null +++ b/commands/transfer.ts @@ -0,0 +1,34 @@ +import type { ChatInputCommandInteraction } from "discord.js"; + +import type { CommandData } from "./index"; +import type { User } from "../db"; +import config from "../config.json"; +import { get_user, add_balance, sub_balance } from "../db"; +import { BotError } from "./error"; + +async function run(interaction: ChatInputCommandInteraction, user: User) { + const options = interaction.options; + const target_id: string = (await options.get("target")).user.id; + const amount: number = (await options.get("amount")).value as number; + if (amount <= 0) throw new BotError("Transfer account cannot be zero or negative"); + let trans_user = await get_user(target_id); + if (!trans_user) throw new BotError("Target is not registered"); + //no checks, baby! well, let db.ts handle it + const enough_balance = await sub_balance(user.user, amount); + if (!enough_balance) throw new BotError(`You do not have enough ${config.currency_plural} to transfer that amount`); + await add_balance(target_id, amount); + return await interaction.editReply(`<@${user.user}> transferred ${amount} ${ amount === 1 ? config.currency : config.currency_plural } to <@${target_id}>`); +} + +const data: CommandData = { + name: "transfer", + description: "Send currency to another user", + registered_only: true, + ephemeral: false, + admin_only: false, + run, +}; + +export default data; + + diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..a79326b --- /dev/null +++ b/config.json.example @@ -0,0 +1,5 @@ +{ + "currency": "credit", + "currency_plural": "credits", + "admin_role": "000000000000000000" +} diff --git a/db.ts b/db.ts index 75ff928..1f6fe97 100644 --- a/db.ts +++ b/db.ts @@ -1,12 +1,101 @@ import { MongoClient } from "mongodb"; +import { did_update } from "./util"; + //figure out the options and whatnot later const client = new MongoClient(process.env.MONGO_CONNECTION_STRING); -let store, users, income; +let store, users, roleincome; 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"); // }); +//db structure types + +export interface StoreItem { + // +}; + +export interface User { + user: `${number}`; //discord user id + balance: number; + items: Record; + // +}; + +export interface RoleIncome { + // +}; + +//default + +const DEFAULT_USER: Omit = { + balance: 0, + items: {}, + // +}; + +//users collection db functions + +export async function add_new_user(user: string) { + return await users.insertOne({ user, ...DEFAULT_USER }); +} + +export async function get_user(user: string): Promise { + return await users.findOne({ user }); +} + +export async function add_balance(user: string, amount: number) { + return await users.updateOne({ user }, { + $inc: { + balance: amount, + }, + }); +} + +//if false, not enough balance +export async function sub_balance(user: string, amount: number, negative_allowed: boolean = false): Promise { + let query: any = { + user, + }; + if (!negative_allowed) { + query.balance = { + $gte: amount, + }; + } + return did_update(await users.updateOne(query, { + $inc: { + balance: -amount, + }, + })); +} + +export async function add_item_to_user(user: string, item: string, amount: number) { + return await users.updateOne({ user }, { + $inc: { + [`items.${item}`]: amount, + }, + }); +} + +export async function sub_item_to_user(user: string, item: string, amount: number): Promise { + return await users.updateOne({ + user, + [`items.${item}`]: { + $gte: amount, + }, + }, { + $inc: { + [`items.${item}`]: -amount, + }, + }); +} + +// + diff --git a/index.ts b/index.ts index 9aad70b..4ba7c6c 100644 --- a/index.ts +++ b/index.ts @@ -1,12 +1,12 @@ -import { Client, BaseInteraction } from "discord.js"; +import { BaseInteraction, Client, GatewayIntentBits } from "discord.js"; import { config } from "dotenv"; -//import db from "./db"; -import run from "./commands"; - config(); -const client = new Client({ intents: [] }); +import {} from "./db"; +import run from "./commands"; + +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.on("ready", async () => { console.log(`Logged in as ${client.user.tag}`); @@ -20,5 +20,5 @@ client.on("interactionCreate", async (interaction: BaseInteraction) => { } }); -setTimeout(() => client.login(process.env.DISCORD_TOKEN), 2000); +setTimeout(() => client.login(process.env.DISCORD_TOKEN), 3000); diff --git a/register.ts b/register.ts index 40b6ff4..a88d912 100644 --- a/register.ts +++ b/register.ts @@ -57,6 +57,73 @@ const commands = [ }, ], }, + //economy related + { + name: "register_user", + description: "Register a user so they can participate in the bot economy (admin only)", + options: [ + { + type: 6, + name: "target", + description: "The user to register", + required: true, + }, + ], + }, + { + name: "bal", + description: "Show you or someone else's balance", + options: [ + { + type: 6, + name: "target", + description: "The user to check the balance of", + required: false, + }, + ], + }, + { + name: "change_bal", + description: "Change a user's balance (admin only)", + options: [ + { + type: 6, + name: "target", + description: "The user to change the balance of", + required: true, + }, + { + type: 4, + name: "amount", + description: "Amount to add/subtract", + required: true, + }, + { + type: 5, + name: "negative_allowed", + description: "Allow user balance to become negative (default: false)", + required: false, + }, + ], + }, + { + name: "transfer", + description: "Send currency to another user", + options: [ + { + type: 6, + name: "target", + description: "The user to send to", + required: true, + }, + { + type: 4, + name: "amount", + 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/tsconfig.json b/tsconfig.json index ef3a6b1..dfd0076 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "typeRoots": ["./node_modules/@types"], "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true }, "lib": ["ES2020"], "exclude": ["node_modules"] diff --git a/util.ts b/util.ts new file mode 100644 index 0000000..7656b41 --- /dev/null +++ b/util.ts @@ -0,0 +1,6 @@ +import type { UpdateResult } from "mongodb"; + +export function did_update(result: UpdateResult): boolean { + return result.modifiedCount > 0; +} +