user registration, bal, change_bal, transfer

This commit is contained in:
stjet
2024-07-25 05:25:40 +00:00
parent f8780249ce
commit 9226debee2
14 changed files with 328 additions and 19 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.env .env
node_modules node_modules
*.js *.js
config.json

35
commands/bal.ts Normal file
View File

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

34
commands/change_bal.ts Normal file
View File

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

View File

@@ -1,28 +1,31 @@
import type { ChatInputCommandInteraction, GuildMember } from "discord.js";
import { EmbedBuilder } 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 { BotError } from "./error";
import config from "../config.json";
import say from "./say"; import say from "./say";
import roll from "./roll"; 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 { export interface CommandData {
name: string; name: string;
description: string; description: string;
registered_only: boolean; //also means interaction will get deferReply()ed
ephemeral: boolean; ephemeral: boolean;
admin_only: boolean; admin_only: boolean;
run: (interaction: ChatInputCommandInteraction) => Promise<any>; run: (interaction: ChatInputCommandInteraction, user?: User) => Promise<any>;
//
}; };
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 { function is_admin(interaction: ChatInputCommandInteraction): boolean {
// return (interaction.member as GuildMember).roles.cache.some((r) => r.id === config.admin_role);
//placeholder
return true;
} }
export default async function run(interaction: ChatInputCommandInteraction) { export default async function run(interaction: ChatInputCommandInteraction) {
@@ -59,7 +62,15 @@ export default async function run(interaction: ChatInputCommandInteraction) {
try { try {
//admin stuff should be ideally handled by register.ts, but this is a fallback //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.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); await found.run(interaction);
return;
} catch (e) { } catch (e) {
if (e instanceof BotError) { if (e instanceof BotError) {
//send error message to that channel //send error message to that channel

26
commands/register_user.ts Normal file
View File

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

View File

@@ -29,10 +29,10 @@ async function run(interaction: ChatInputCommandInteraction) {
const data: CommandData = { const data: CommandData = {
name: "roll", name: "roll",
description: "Roll dice", description: "Roll dice",
registered_only: false,
ephemeral: false, ephemeral: false,
admin_only: false, admin_only: false,
run, run,
//
}; };
export default data; export default data;

View File

@@ -25,10 +25,10 @@ async function run(interaction: ChatInputCommandInteraction) {
const data: CommandData = { const data: CommandData = {
name: "say", name: "say",
description: "Have the bot say something in a channel", description: "Have the bot say something in a channel",
registered_only: false,
ephemeral: true, ephemeral: true,
admin_only: true, admin_only: true,
run, run,
//
}; };
export default data; export default data;

34
commands/transfer.ts Normal file
View File

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

5
config.json.example Normal file
View File

@@ -0,0 +1,5 @@
{
"currency": "credit",
"currency_plural": "credits",
"admin_role": "000000000000000000"
}

91
db.ts
View File

@@ -1,12 +1,101 @@
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
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, income; let store, users, roleincome;
client.connect().then(() => { client.connect().then(() => {
console.log("Connected to the database"); 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<string, number>;
//
};
export interface RoleIncome {
//
};
//default
const DEFAULT_USER: Omit<User, "user"> = {
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<User | undefined> {
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<boolean> {
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<boolean> {
return await users.updateOne({
user,
[`items.${item}`]: {
$gte: amount,
},
}, {
$inc: {
[`items.${item}`]: -amount,
},
});
}
//

View File

@@ -1,12 +1,12 @@
import { Client, BaseInteraction } from "discord.js"; import { BaseInteraction, Client, GatewayIntentBits } from "discord.js";
import { config } from "dotenv"; import { config } from "dotenv";
//import db from "./db";
import run from "./commands";
config(); config();
const client = new Client({ intents: [] }); import {} from "./db";
import run from "./commands";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on("ready", async () => { client.on("ready", async () => {
console.log(`Logged in as ${client.user.tag}`); 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);

View File

@@ -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")); (new REST().setToken(process.env.DISCORD_TOKEN)).put(Routes.applicationCommands(process.env.CLIENT_ID), { body: commands }).then(() => console.log("Finished reloading slash commands"));

View File

@@ -6,7 +6,8 @@
"typeRoots": ["./node_modules/@types"], "typeRoots": ["./node_modules/@types"],
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}, },
"lib": ["ES2020"], "lib": ["ES2020"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

6
util.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { UpdateResult } from "mongodb";
export function did_update(result: UpdateResult): boolean {
return result.modifiedCount > 0;
}