last upload from radzathan

main
Mari 4 months ago
parent 0291c73e80
commit 00115a98cd
  1. 2
      .idea/codeStyles/codeStyleConfig.xml
  2. 10270
      package-lock.json
  3. 1
      package.json
  4. 144
      src/bin/bot.ts
  5. 6
      src/commands/character/create.ts
  6. 71
      src/commands/character/index.ts
  7. 23
      src/commands/index.spec.ts
  8. 158
      src/commands/index.ts
  9. 61
      src/database/index.ts
  10. 38
      src/database/users.spec.ts
  11. 20
      src/database/users.ts
  12. 20
      src/managers/characters.ts
  13. 7
      src/testing/integration.ts

@ -1,5 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<state> <state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> <option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state> </state>
</component> </component>

10270
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -35,6 +35,7 @@
"@types/pg": "^8.6.5", "@types/pg": "^8.6.5",
"babel-jest": "^29.1.2", "babel-jest": "^29.1.2",
"jest": "^29.1.2", "jest": "^29.1.2",
"standard": "^17.0.0",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.8.4" "typescript": "^4.8.4"

@ -1,79 +1,80 @@
import {BaseInteraction, Client} from "discord.js" import {BaseInteraction, Client as DiscordClient} from "discord.js";
import {config} from "dotenv" import {config as loadEnvironment} from "dotenv";
import {isAutocomplete, isChatInputCommand} from "../types/interactions.js" import {isAutocomplete, isChatInputCommand} from "../types/interactions.js";
import {Commands} from "../commands/index.js" import {Commands} from "../commands";
import {checkIsRestart, reportFailed, reportReady, reportStarted} from "../ipc/restart.js" import {
import {defaultPresence} from "../defaultPresence.js" checkIsRestart,
import {Pool} from "pg" reportFailed,
import {Database, DatabaseImpl, makeTransact} from "../database" reportReady,
reportStarted,
} from "../ipc/restart.js";
import {defaultPresence} from "../defaultPresence.js";
import {Pool as PostgresPool} from "pg";
import {Database, DatabaseImpl, makeTransact} from "../database";
import {Managers} from "../managers";
async function bot() { async function bot() {
await checkIsRestart() await checkIsRestart();
config() loadEnvironment();
const c = new Client({ const c = new DiscordClient({
intents: [], intents: [],
}) });
const p = new Pool({
application_name: "VoreRPG Bot", const db: Database = DatabaseImpl.fromPool({
}) application_name: process.env.APP_NAME ?? "VoreRPG Bot"
p.on("error", (err) => {
console.log(err)
}) })
async function cleanUp() { async function cleanUp() {
await p.end() await db.tearDown();
c.destroy() c.destroy();
} }
const db: Database = new DatabaseImpl({ const mgr = new Managers(db);
query: p.query.bind(p), const cmd = new Commands({users: mgr.users, cleanUp});
transact: makeTransact(p),
})
const cmd = new Commands({users: db.users, cleanUp})
c.on("ready", async () => { c.on("ready", async () => {
try { try {
await db.users.createBotOwnerAsAdmin(process.env.BOT_OWNER_ID || "") await mgr.users.setSnowflakeAdmin(process.env.BOT_OWNER_ID ?? "");
} catch (ex) { } catch (ex) {
await reportFailed(c, ex) await reportFailed(c, ex);
return return;
} }
const app = c.application const app = c.application;
if (!app) { if (!app) {
c.destroy() c.destroy();
await reportFailed(c, "No application was given") await reportFailed(c, "No application was given");
return return;
} else { } else {
try { try {
cmd.setCache(await app.commands.set(cmd.definitions)) cmd.setCache(await app.commands.set(cmd.definitions));
const g = await c.guilds.fetch() const g = await c.guilds.fetch();
for (const guild of g.values()) { for (const guild of g.values()) {
cmd.setCache(await app.commands.set(cmd.definitions, guild.id)) cmd.setCache(await app.commands.set(cmd.definitions, guild.id));
} }
} catch (ex) { } catch (ex) {
c.destroy() c.destroy();
await reportFailed(c, ex) await reportFailed(c, ex);
return return;
} }
} }
const user = c.user const user = c.user;
if (!user) { if (!user) {
c.destroy() c.destroy();
await reportFailed(c, "No user found") await reportFailed(c, "No user found");
return return;
} else { } else {
user.setPresence(defaultPresence) user.setPresence(defaultPresence);
} }
try { try {
await reportReady(c) await reportReady(c);
} catch (ex) { } catch (ex) {
console.log(ex) console.log(ex);
} }
}) });
c.on("error", async (ex) => { c.on("error", async (ex) => {
c.destroy() c.destroy();
await reportFailed(c, ex) await reportFailed(c, ex);
}) });
c.on("interactionCreate", async (ev: BaseInteraction) => { c.on("interactionCreate", async (ev: BaseInteraction) => {
if (ev.client.user.presence.status !== "online") { if (ev.client.user.presence.status !== "online") {
if (ev.isRepliable()) { if (ev.isRepliable()) {
@ -81,62 +82,63 @@ async function bot() {
await ev.reply({ await ev.reply({
content: "Shhh... I'm sleeping!! Try again later.", content: "Shhh... I'm sleeping!! Try again later.",
ephemeral: true, ephemeral: true,
}) });
} catch (ex) { } catch (ex) {
console.log("failed sending busy reply", ex) console.log("failed sending busy reply", ex);
} }
} }
return return;
} }
if (isChatInputCommand(ev)) { if (isChatInputCommand(ev)) {
try { try {
await cmd.execute(ev) await cmd.execute(ev);
} catch (ex) { } catch (ex) {
console.log("failed executing command", ev, ex) console.log("failed executing command", ev, ex);
} }
if (!ev.replied) { if (!ev.replied) {
console.log("never replied to command", ev) console.log("never replied to command", ev);
try { try {
await ev.reply({ await ev.reply({
ephemeral: true, ephemeral: true,
content: "Uuguuu... I can't think straight... try again later, 'kay?", content:
}) "Uuguuu... I can't think straight... try again later, 'kay?",
});
} catch (ex) { } catch (ex) {
console.log("failed sending error reply", ex) console.log("failed sending error reply", ex);
} }
} }
} else if (isAutocomplete(ev)) { } else if (isAutocomplete(ev)) {
try { try {
await cmd.autocomplete(ev) await cmd.autocomplete(ev);
} catch (ex) { } catch (ex) {
console.log("failed autocompleting for command", ev, ex) console.log("failed autocompleting for command", ev, ex);
} }
if (!ev.responded) { if (!ev.responded) {
console.log("never autocompleted for command", ev) console.log("never autocompleted for command", ev);
try { try {
await ev.respond([]) await ev.respond([]);
} catch (ex) { } catch (ex) {
console.log("failed sending error response", ex) console.log("failed sending error response", ex);
} }
} }
} else if (ev.isRepliable()) { } else if (ev.isRepliable()) {
try { try {
console.log("unknown command", ev) console.log("unknown command", ev);
await ev.reply({ await ev.reply({
ephemeral: true, ephemeral: true,
content: "Huuuuuuuh? ... I don't know what to do with that yet.", content: "Huuuuuuuh? ... I don't know what to do with that yet.",
}) });
} catch (ex) { } catch (ex) {
console.log("failed sending unknown command reply", ex) console.log("failed sending unknown command reply", ex);
} }
} else { } else {
console.log("got an interaction but can't reply to it") console.log("got an interaction but can't reply to it");
} }
}) });
await reportStarted() await reportStarted();
await c.login(process.env.DISCORD_TOKEN || "") await c.login(process.env.DISCORD_TOKEN || "");
} }
bot().catch((ex) => { bot().catch((ex) => {
console.log("main thread failed", ex) console.log("main thread failed", ex);
}) });

@ -1,6 +1,6 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js" import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js"
import {SubcommandData} from "../types" import {SubcommandData} from "../types"
import {UserTable} from "../../database/users.js" import {UsersManager} from "../../managers/users";
export class CharacterCreateCommand extends SubcommandData { export class CharacterCreateCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = { readonly definition: ApplicationCommandSubCommandData = {
@ -68,9 +68,9 @@ export class CharacterCreateCommand extends SubcommandData {
}, },
], ],
} }
private readonly _users: UserTable private readonly _users: UsersManager
constructor({users}: { users: UserTable }) { constructor({users}: { users: UsersManager }) {
super() super()
this._users = users this._users = users
} }

@ -1,39 +1,47 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ApplicationCommandType} from "discord.js" import {
import {BaseChatInputCommandData, CommandWithSubcommandsData, SubcommandData} from "../types" ApplicationCommandOptionType,
import {UserTable} from "../../database/users.js" ApplicationCommandSubCommandData,
import {CharacterCreateCommand} from "./create.js" ApplicationCommandType,
} from "discord.js";
import {
BaseChatInputCommandData,
CommandWithSubcommandsData,
SubcommandData,
} from "../types";
import { CharacterCreateCommand } from "./create.js";
import { UsersManager } from "../../managers/users";
export class CharacterCommand extends CommandWithSubcommandsData { export class CharacterCommand extends CommandWithSubcommandsData {
readonly create: CharacterCreateCommand readonly create: CharacterCreateCommand;
readonly subcommands: SubcommandData[] readonly subcommands: SubcommandData[];
private readonly _users: UserTable private readonly _users: UsersManager;
constructor({users}: { users: UserTable }) { constructor({ users }: { users: UsersManager }) {
super() super();
this._users = users this._users = users;
this.create = new CharacterCreateCommand({users}) this.create = new CharacterCreateCommand({ users });
this.subcommands = [ this.subcommands = [this.create];
this.create,
]
} }
readonly baseDefinition: BaseChatInputCommandData = { readonly baseDefinition: BaseChatInputCommandData = {
name: "character", name: "character",
type: ApplicationCommandType.ChatInput, type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.", description: "Commands to manage your characters.",
} };
} }
const otherSubcommands: ApplicationCommandSubCommandData[] = [ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "select", name: "select",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Selects (and activates, if necessary) a character for use with other commands used in this server.", description:
"Selects (and activates, if necessary) a character for use with other commands used in this server.",
options: [ options: [
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The character to switch to. If not supplied, shows you the current selected character.", description:
"The character to switch to. If not supplied, shows you the current selected character.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
@ -56,7 +64,8 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "deactivate", name: "deactivate",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Prevents a character from being targeted on the current server. Deselects them if they're selected.", description:
"Prevents a character from being targeted on the current server. Deselects them if they're selected.",
options: [ options: [
{ {
name: "character", name: "character",
@ -68,12 +77,14 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "edit", name: "edit",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Edits an existing character. Note that characters currently in battle cannot be edited.", description:
"Edits an existing character. Note that characters currently in battle cannot be edited.",
options: [ options: [
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "An existing character of yours to edit. Omit to edit the current character.", description:
"An existing character of yours to edit. Omit to edit the current character.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
@ -87,7 +98,8 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "public", name: "public",
type: ApplicationCommandOptionType.Boolean, type: ApplicationCommandOptionType.Boolean,
description: "True to share your character list with the current channel.", description:
"True to share your character list with the current channel.",
required: false, required: false,
}, },
], ],
@ -100,14 +112,16 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "An existing character of yours to display in this channel. Omit to display the current character.", description:
"An existing character of yours to display in this channel. Omit to display the current character.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
{ {
name: "public", name: "public",
type: ApplicationCommandOptionType.Boolean, type: ApplicationCommandOptionType.Boolean,
description: "True to share your character list with the current channel.", description:
"True to share your character list with the current channel.",
required: false, required: false,
}, },
], ],
@ -115,12 +129,14 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "archive", name: "archive",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Removes a character from all servers and hides it from the default list view.", description:
"Removes a character from all servers and hides it from the default list view.",
options: [ options: [
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "An existing unarchived character of yours to add to the archive.", description:
"An existing unarchived character of yours to add to the archive.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
@ -134,10 +150,11 @@ const otherSubcommands: ApplicationCommandSubCommandData[] = [
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "An existing archived character of yours to take back out of the archive.", description:
"An existing archived character of yours to take back out of the archive.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
], ],
}, },
] ];

@ -1,12 +1,17 @@
import {Commands} from "./index" import {Commands} from "./index";
import {describe, expect, test} from "@jest/globals" import {describe, expect, test} from "@jest/globals";
import {InMemoryDatabase} from "../database/inmemory/database.js" import {UsersManager} from "../managers/users";
import {UserTableInMemory} from "../database/users";
describe("command definitions", () => { describe("command definitions", () => {
test("has no descriptions over 100 characters", () => { test("has no descriptions over 100 characters", () => {
const db = new InMemoryDatabase() expect(
expect(new Commands({users: db.users, cleanUp: async () => undefined}).definitions) new Commands({
.not users: new UsersManager({users: new UserTableInMemory()}),
.toContain(expect.objectContaining({"description": expect.stringMatching(/.{101,}/)})) cleanUp: async () => undefined,
}) }).definitions
}) ).not.toContain(
expect.objectContaining({description: expect.stringMatching(/.{101,}/)})
);
});
});

@ -6,62 +6,66 @@ import {
AutocompleteInteraction, AutocompleteInteraction,
ChatInputCommandInteraction, ChatInputCommandInteraction,
Collection, Collection,
} from "discord.js" } from "discord.js";
import {CharacterCommand} from "./character/index" import { CharacterCommand } from "./character";
import {autocompleteNotImplementedError, CommandData} from "./types" import { autocompleteNotImplementedError, CommandData } from "./types";
import {BotCommand} from "./bot/index" import { BotCommand } from "./bot";
import {UserTable} from "../database/users.js" import { UserTable } from "../database/users.js";
import { UsersManager } from "../managers/users";
export const invalidCommandError = "No command by that name exists." export const invalidCommandError = "No command by that name exists.";
export class Commands { export class Commands {
readonly all: CommandData[] readonly all: CommandData[];
readonly character: CharacterCommand readonly character: CharacterCommand;
readonly bot: BotCommand readonly bot: BotCommand;
readonly definitions: ApplicationCommandDataResolvable[] readonly definitions: ApplicationCommandDataResolvable[];
private readonly _nameCache: Record<string, CommandData> private readonly _nameCache: Record<string, CommandData>;
constructor({users, cleanUp}: { users: UserTable, cleanUp: () => Promise<void> }) { constructor({
this.character = new CharacterCommand({users}) users,
this.bot = new BotCommand({users, cleanUp}) cleanUp,
this.all = [ }: {
this.character, users: UsersManager;
this.bot, cleanUp: () => Promise<void>;
] }) {
this.definitions = this.all.map((c) => c.definition) this.character = new CharacterCommand({ users });
this.bot = new BotCommand({ users, cleanUp });
this.all = [this.character, this.bot];
this.definitions = this.all.map((c) => c.definition);
this._nameCache = Object.fromEntries( this._nameCache = Object.fromEntries(
this.all.map((c) => [c.definition.name, c]), this.all.map((c) => [c.definition.name, c])
) );
} }
setCache(data: Collection<string, ApplicationCommand>) { setCache(data: Collection<string, ApplicationCommand>) {
for (const command of data.values()) { for (const command of data.values()) {
if (this._nameCache.hasOwnProperty(command.name)) { if (this._nameCache.hasOwnProperty(command.name)) {
this._nameCache[command.name].setCached(command) this._nameCache[command.name].setCached(command);
} else { } else {
console.log("No such command when caching commands: " + command.name) console.log("No such command when caching commands: " + command.name);
} }
} }
} }
async execute(ev: ChatInputCommandInteraction) { async execute(ev: ChatInputCommandInteraction) {
const name = ev.commandName const name = ev.commandName;
if (!this._nameCache.hasOwnProperty(name)) { if (!this._nameCache.hasOwnProperty(name)) {
throw invalidCommandError throw invalidCommandError;
} }
await this._nameCache[name].execute(ev) await this._nameCache[name].execute(ev);
} }
async autocomplete(ev: AutocompleteInteraction) { async autocomplete(ev: AutocompleteInteraction) {
const name = ev.commandName const name = ev.commandName;
if (this._nameCache.hasOwnProperty(name)) { if (this._nameCache.hasOwnProperty(name)) {
const command = this._nameCache[name] const command = this._nameCache[name];
if (command.autocomplete) { if (command.autocomplete) {
await command.autocomplete(ev) await command.autocomplete(ev);
return return;
} }
} }
throw autocompleteNotImplementedError throw autocompleteNotImplementedError;
} }
} }
@ -74,12 +78,14 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "difficulty", name: "difficulty",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Alters or displays the reformation difficulty of your currently selected character.", description:
"Alters or displays the reformation difficulty of your currently selected character.",
}, },
{ {
name: "preference", name: "preference",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Alters or displays the pred/prey preference of your currently selected character.", description:
"Alters or displays the pred/prey preference of your currently selected character.",
}, },
], ],
}, },
@ -91,12 +97,14 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "join", name: "join",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Leaves the current team, if any, and requests to join the same team (and battle) as another player.", description:
"Leaves the current team, if any, and requests to join the same team (and battle) as another player.",
}, },
{ {
name: "leave", name: "leave",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Leaves the current team, if any, and strikes it out alone.", description:
"Leaves the current team, if any, and strikes it out alone.",
}, },
], ],
}, },
@ -138,7 +146,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "target", name: "target",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The target to use the skill on, if there are multiple options. Omit to see a list.", description:
"The target to use the skill on, if there are multiple options. Omit to see a list.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
@ -159,7 +168,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "target", name: "target",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The target to use the item on, if there are multiple options. Omit to see a list.", description:
"The target to use the item on, if there are multiple options. Omit to see a list.",
autocomplete: true, autocomplete: true,
required: false, required: false,
}, },
@ -168,7 +178,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "vore", name: "vore",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Devours your opponent. Requires your opponent to be at low Confidence.", description:
"Devours your opponent. Requires your opponent to be at low Confidence.",
options: [ options: [
{ {
name: "style", name: "style",
@ -182,17 +193,20 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "rest", name: "rest",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Takes a breather, recovering your Confidence and Energy at a faster-than-normal rate for this turn.", description:
"Takes a breather, recovering your Confidence and Energy at a faster-than-normal rate for this turn.",
}, },
{ {
name: "escape", name: "escape",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Tries to escape from a battle or stomach. Requires your opponent to be at low Confidence.", description:
"Tries to escape from a battle or stomach. Requires your opponent to be at low Confidence.",
}, },
{ {
name: "surrender", name: "surrender",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Lets your opponent do as they will with you by reducing your Confidence or Health to 0.", description:
"Lets your opponent do as they will with you by reducing your Confidence or Health to 0.",
}, },
{ {
name: "prey", name: "prey",
@ -207,7 +221,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "squish", name: "squish",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Squishes your prey, ending the combat. Can only be performed when the prey is at or below 0% Health.", description:
"Squishes your prey, ending the combat. Can only be performed when the prey is at or below 0% Health.",
}, },
], ],
}, },
@ -221,7 +236,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "exit", name: "exit",
type: ApplicationCommandOptionType.SubcommandGroup, type: ApplicationCommandOptionType.SubcommandGroup,
description: "Exits battle(s) and reverts its/their effects on you. To flee in-character, use /battle escape.", description:
"Exits battle(s) and reverts its/their effects on you. To flee in-character, use /battle escape.",
}, },
{ {
name: "block", name: "block",
@ -231,18 +247,21 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "add", name: "add",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Prevents a user or character from challenging you or joining battles you're in.", description:
"Prevents a user or character from challenging you or joining battles you're in.",
options: [ options: [
{ {
name: "user", name: "user",
type: ApplicationCommandOptionType.User, type: ApplicationCommandOptionType.User,
description: "The user to block from challenging you or joining battles you're in.", description:
"The user to block from challenging you or joining battles you're in.",
required: false, required: false,
}, },
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The character to block from challenging you or joining battles you're in.", description:
"The character to block from challenging you or joining battles you're in.",
required: false, required: false,
autocomplete: true, autocomplete: true,
}, },
@ -251,18 +270,21 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "remove", name: "remove",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user or character to challenge you and join battles you're in once more.", description:
"Allows a user or character to challenge you and join battles you're in once more.",
options: [ options: [
{ {
name: "user", name: "user",
type: ApplicationCommandOptionType.User, type: ApplicationCommandOptionType.User,
description: "The user to allow to challenge you and join battles you're in once more.", description:
"The user to allow to challenge you and join battles you're in once more.",
required: false, required: false,
}, },
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The character to allow to challenge you and join battles you're in once more.", description:
"The character to allow to challenge you and join battles you're in once more.",
required: false, required: false,
autocomplete: true, autocomplete: true,
}, },
@ -278,23 +300,27 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "kick", name: "kick",
type: ApplicationCommandOptionType.SubcommandGroup, type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages the list of users kicked from the current battle.", description:
"Manages the list of users kicked from the current battle.",
options: [ options: [
{ {
name: "add", name: "add",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Removes a user or character from the current battle and prevents them from rejoining.", description:
"Removes a user or character from the current battle and prevents them from rejoining.",
options: [ options: [
{ {
name: "user", name: "user",
type: ApplicationCommandOptionType.User, type: ApplicationCommandOptionType.User,
description: "The user to remove from the current battle and prevent from rejoining.", description:
"The user to remove from the current battle and prevent from rejoining.",
required: false, required: false,
}, },
{ {
name: "character", name: "character",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description: "The character to remove from the current battle and prevent from rejoining.", description:
"The character to remove from the current battle and prevent from rejoining.",
required: false, required: false,
autocomplete: true, autocomplete: true,
}, },
@ -303,7 +329,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "remove", name: "remove",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user or character to rejoin the current battle.", description:
"Allows a user or character to rejoin the current battle.",
options: [ options: [
{ {
name: "user", name: "user",
@ -323,7 +350,8 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "list", name: "list",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Displays the list of users forbidden from rejoining the current battle.", description:
"Displays the list of users forbidden from rejoining the current battle.",
}, },
], ],
}, },
@ -335,12 +363,14 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "add", name: "add",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Bans a user from participating in battles in the current server and kicks them from any they're in.", description:
"Bans a user from participating in battles in the current server and kicks them from any they're in.",
options: [ options: [
{ {
name: "user", name: "user",
type: ApplicationCommandOptionType.User, type: ApplicationCommandOptionType.User,
description: "The user to ban from participating in battles in the current server.", description:
"The user to ban from participating in battles in the current server.",
required: true, required: true,
}, },
], ],
@ -348,12 +378,14 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "remove", name: "remove",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user to resume participating in battles in the current server.", description:
"Allows a user to resume participating in battles in the current server.",
options: [ options: [
{ {
name: "user", name: "user",
type: ApplicationCommandOptionType.User, type: ApplicationCommandOptionType.User,
description: "The user to allow to participate in battles in the current server once more.", description:
"The user to allow to participate in battles in the current server once more.",
required: true, required: true,
}, },
], ],
@ -368,13 +400,15 @@ const otherCommands: ApplicationCommandDataResolvable[] = [
{ {
name: "delete", name: "delete",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "PERMANENTLY deletes a character, exiting any battles they were part of. See also /character archive.", description:
"PERMANENTLY deletes a character, exiting any battles they were part of. See also /character archive.",
}, },
{ {
name: "wipe", name: "wipe",
type: ApplicationCommandOptionType.Subcommand, type: ApplicationCommandOptionType.Subcommand,
description: "PERMANENTLY deletes ALL data about you in the system, erasing you from the system's knowledge.", description:
"PERMANENTLY deletes ALL data about you in the system, erasing you from the system's knowledge.",
}, },
], ],
}, },
] ];

@ -1,11 +1,15 @@
import {QueryResult, QueryResultRow} from "pg" import {Pool, PoolConfig, QueryResult, QueryResultRow} from "pg"
import {UserTable, UserTableImpl} from "./users.js" import {UserTable, UserTableImpl, UserTableInMemory} from "./users.js"
import {BattleTypeTable, BattleTypeTableImpl} from "./battle_types"; import {BattleTypeTable, BattleTypeTableImpl} from "./battle_types";
import {GenderTable, GenderTablesImpl} from "./genders"; import {GenderTable, GenderTablesImpl} from "./genders";
import {CharacterTable, CharacterTableImpl} from "./characters"; import {CharacterTable, CharacterTableImpl} from "./characters";
export interface PoolLike { export interface PoolLike extends Queryable {
connect(): Promise<PoolClientLike> connect(): Promise<PoolClientLike>
on(type: 'error', handler: (err: unknown) => void): void
end(): Promise<void>
} }
export interface PoolClientLike extends Queryable { export interface PoolClientLike extends Queryable {
@ -13,7 +17,10 @@ export interface PoolClientLike extends Queryable {
} }
export function makeTransact(p: PoolLike): TransactType { export function makeTransact(p: PoolLike): TransactType {
return async function transact({readonly=false, transaction}: {readonly?: boolean, transaction: (q: QueryType, attempts: number) => Promise<void>}): Promise<void> { return async function transact({
readonly = false,
transaction
}: { readonly?: boolean, transaction: (q: QueryType, attempts: number) => Promise<void> }): Promise<void> {
const client = await p.connect() const client = await p.connect()
const query = client.query.bind(client) const query = client.query.bind(client)
let committed = false let committed = false
@ -52,6 +59,19 @@ export interface Database {
readonly users: UserTable readonly users: UserTable
readonly genders: GenderTable readonly genders: GenderTable
readonly battleTypes: BattleTypeTable readonly battleTypes: BattleTypeTable
tearDown(): Promise<void>
}
export class DatabaseInMemory implements Database {
readonly users = new UserTableInMemory()
readonly characters = new CharacterTableInMemory()
readonly genders = new GenderTableInMemory()
readonly battleTypes = new BattleTypeTableInMemory()
tearDown(): Promise<void> {
return Promise.resolve()
}
} }
export interface Queryable { export interface Queryable {
@ -64,16 +84,47 @@ export interface Queryable {
export type QueryType = Queryable["query"] export type QueryType = Queryable["query"]
export type TransactType = (config: { readonly?: boolean, transaction: (client: QueryType, attempts: number) => Promise<void> }) => Promise<void> export type TransactType = (config: { readonly?: boolean, transaction: (client: QueryType, attempts: number) => Promise<void> }) => Promise<void>
function isPoolLike(p: PoolLike | PoolConfig): p is PoolLike {
return Object.hasOwn(p, "query")
&& Object.hasOwn(p, "end")
&& Object.hasOwn(p, "connect")
&& Object.hasOwn(p, "on")
}
export class DatabaseImpl implements Database { export class DatabaseImpl implements Database {
static fromPool(p?: PoolLike | PoolConfig, errHandler?: (err: unknown) => void): DatabaseImpl {
if (typeof p === "undefined") {
p = {}
}
if (!isPoolLike(p)) {
p = new Pool(p)
}
if (typeof errHandler === "undefined") {
errHandler = (x) => console.log(x)
}
p.on("error", errHandler);
return new DatabaseImpl({
query: p.query.bind(p),
transact: makeTransact(p),
tearDown: p.end.bind(p)
})
}
readonly characters: CharacterTableImpl readonly characters: CharacterTableImpl
readonly users: UserTableImpl readonly users: UserTableImpl
readonly genders: GenderTable readonly genders: GenderTable
readonly battleTypes: BattleTypeTableImpl readonly battleTypes: BattleTypeTableImpl
readonly tearDown: () => Promise<void>
constructor({query, transact}: { query: QueryType, transact: TransactType }) { constructor({
query,
transact,
tearDown
}: { query: QueryType, transact: TransactType, tearDown: () => Promise<void> }) {
this.characters = new CharacterTableImpl({transact}) this.characters = new CharacterTableImpl({transact})
this.users = new UserTableImpl({query}) this.users = new UserTableImpl({query})
this.genders = new GenderTablesImpl({transact}) this.genders = new GenderTablesImpl({transact})
this.battleTypes = new BattleTypeTableImpl({transact}) this.battleTypes = new BattleTypeTableImpl({transact})
this.tearDown = tearDown
} }
} }

@ -0,0 +1,38 @@
import {describe, test, beforeAll, beforeEach, afterAll} from "@jest/globals"
import {UserTable, UserTableInMemory} from "./users";
import {integrationDescribe as xdescribe} from "../testing/integration"
import {Database, DatabaseImpl} from "./index";
import {v5 as uuidV5} from "uuid";
describe('UserTableInMemory', function () {
userTableSpec(() => new UserTableInMemory())
});
xdescribe('UserTableImpl', function () {
let db: Database
beforeAll(async () => {
db = DatabaseImpl.fromPool({
application_name: "VoreRPG Bot User Table Tests"
})
})
afterAll(async () => {
await db.tearDown()
})
userTableSpec(() => db.users)
})
function userTableSpec(setUp: () => UserTable) {
let table: UserTable
beforeEach(() => {
table = setUp()
})
test("", async () => {
const userId = uuidV5()
await table.createOrSetUserAsAdmin()
})
}

@ -8,6 +8,26 @@ export interface UserTable {
createOrSetUserAsAdmin(id: string): Promise<void> createOrSetUserAsAdmin(id: string): Promise<void>
} }
export class UserTableInMemory implements UserTable {
readonly users: { [id: string]: boolean } = {}
createOrSetUserAsAdmin(id: string): Promise<void> {
this.users[id] = true
return Promise.resolve();
}
createUserOrUpdateActiveTime(id: string): Promise<void> {
if (!Object.hasOwn(this.users, id)) {
this.users[id] = false
}
return Promise.resolve();
}
getUserExistsAndIsAdmin(id: string): Promise<boolean> {
return Promise.resolve(Object.hasOwn(this.users, id) && this.users[id]);
}
}
export class UserTableImpl implements UserTable { export class UserTableImpl implements UserTable {
private readonly _query: QueryType private readonly _query: QueryType

@ -2,19 +2,27 @@ import {Snowflake} from "discord-api-types/globals";
import {CharacterTable} from "../database/characters"; import {CharacterTable} from "../database/characters";
export class CharacterManager { export class CharacterManager {
private
constructor({characters}: { characters: CharacterTable }) { constructor({characters}: { characters: CharacterTable }) {
} }
async startNewCharacter(user: Snowflake, overwrite: boolean): Promise<{alreadyEditing: boolean, created: boolean}> { async startNewCharacter(
user: Snowflake,
overwrite: boolean
): Promise<{ alreadyEditing: boolean; created: boolean }> {
throw Error("not yet implemented");
} }
async abandonEditedCharacter(user: Snowflake): Promise<{abandoned: boolean}> { async abandonEditedCharacter(
user: Snowflake
): Promise<{ abandoned: boolean }> {
throw Error("not yet implemented");
} }
async saveEditedCharacter(user: Snowflake, asNew?: boolean): Promise<{ id: string, name: string, title: string }|{ id: null }> { async saveEditedCharacter(
user: Snowflake,
asNew?: boolean
): Promise<{ id: string; name: string; title: string } | { id: null }> {
throw Error("not yet implemented");
} }
} }

@ -0,0 +1,7 @@
import {describe} from "@jest/globals";
export const integrationDescribe = (process.env.RUN_INTEGRATION_TESTS === "1") ? describe : describe.skip
export function nonDiscordTestUserId() {
}
Loading…
Cancel
Save