user registration, bal, change_bal, transfer
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
node_modules
|
||||
*.js
|
||||
config.json
|
||||
|
||||
35
commands/bal.ts
Normal file
35
commands/bal.ts
Normal 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
34
commands/change_bal.ts
Normal 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;
|
||||
|
||||
@@ -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<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 {
|
||||
//
|
||||
//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
|
||||
|
||||
26
commands/register_user.ts
Normal file
26
commands/register_user.ts
Normal 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
34
commands/transfer.ts
Normal file
34
commands/transfer.ts
Normal 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
5
config.json.example
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"currency": "credit",
|
||||
"currency_plural": "credits",
|
||||
"admin_role": "000000000000000000"
|
||||
}
|
||||
91
db.ts
91
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<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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
|
||||
12
index.ts
12
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);
|
||||
|
||||
|
||||
67
register.ts
67
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"));
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"lib": ["ES2020"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user