last upload from radzathan

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

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

7514
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

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

@ -1,143 +1,160 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ApplicationCommandType} from "discord.js"
import {BaseChatInputCommandData, CommandWithSubcommandsData, SubcommandData} from "../types"
import {UserTable} from "../../database/users.js"
import {CharacterCreateCommand} from "./create.js"
import {
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
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 {
readonly create: CharacterCreateCommand
readonly subcommands: SubcommandData[]
private readonly _users: UserTable
readonly create: CharacterCreateCommand;
readonly subcommands: SubcommandData[];
private readonly _users: UsersManager;
constructor({users}: { users: UserTable }) {
super()
this._users = users
this.create = new CharacterCreateCommand({users})
this.subcommands = [
this.create,
]
}
constructor({ users }: { users: UsersManager }) {
super();
this._users = users;
this.create = new CharacterCreateCommand({ users });
this.subcommands = [this.create];
}
readonly baseDefinition: BaseChatInputCommandData = {
name: "character",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.",
}
readonly baseDefinition: BaseChatInputCommandData = {
name: "character",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.",
};
}
const otherSubcommands: ApplicationCommandSubCommandData[] = [
{
name: "select",
type: ApplicationCommandOptionType.Subcommand,
description: "Selects (and activates, if necessary) a character for use with other commands used in this server.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to switch to. If not supplied, shows you the current selected character.",
autocomplete: true,
required: false,
},
],
},
{
name: "activate",
type: ApplicationCommandOptionType.Subcommand,
description: "Allows a character to be targeted on the current server.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to activate on this server.",
autocomplete: true,
required: false,
},
],
},
{
name: "deactivate",
type: ApplicationCommandOptionType.Subcommand,
description: "Prevents a character from being targeted on the current server. Deselects them if they're selected.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to deactivate on this server.",
},
],
},
{
name: "edit",
type: ApplicationCommandOptionType.Subcommand,
description: "Edits an existing character. Note that characters currently in battle cannot be edited.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "An existing character of yours to edit. Omit to edit the current character.",
autocomplete: true,
required: false,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Lists your characters.",
options: [
{
name: "public",
type: ApplicationCommandOptionType.Boolean,
description: "True to share your character list with the current channel.",
required: false,
},
],
},
{
name: "show",
type: ApplicationCommandOptionType.Subcommand,
description: "Shows the data about one of your characters.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "An existing character of yours to display in this channel. Omit to display the current character.",
autocomplete: true,
required: false,
},
{
name: "public",
type: ApplicationCommandOptionType.Boolean,
description: "True to share your character list with the current channel.",
required: false,
},
],
},
{
name: "archive",
type: ApplicationCommandOptionType.Subcommand,
description: "Removes a character from all servers and hides it from the default list view.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "An existing unarchived character of yours to add to the archive.",
autocomplete: true,
required: false,
},
],
},
{
name: "unarchive",
type: ApplicationCommandOptionType.Subcommand,
description: "Unarchives a character.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "An existing archived character of yours to take back out of the archive.",
autocomplete: true,
required: false,
},
],
},
]
{
name: "select",
type: ApplicationCommandOptionType.Subcommand,
description:
"Selects (and activates, if necessary) a character for use with other commands used in this server.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"The character to switch to. If not supplied, shows you the current selected character.",
autocomplete: true,
required: false,
},
],
},
{
name: "activate",
type: ApplicationCommandOptionType.Subcommand,
description: "Allows a character to be targeted on the current server.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to activate on this server.",
autocomplete: true,
required: false,
},
],
},
{
name: "deactivate",
type: ApplicationCommandOptionType.Subcommand,
description:
"Prevents a character from being targeted on the current server. Deselects them if they're selected.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to deactivate on this server.",
},
],
},
{
name: "edit",
type: ApplicationCommandOptionType.Subcommand,
description:
"Edits an existing character. Note that characters currently in battle cannot be edited.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"An existing character of yours to edit. Omit to edit the current character.",
autocomplete: true,
required: false,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Lists your characters.",
options: [
{
name: "public",
type: ApplicationCommandOptionType.Boolean,
description:
"True to share your character list with the current channel.",
required: false,
},
],
},
{
name: "show",
type: ApplicationCommandOptionType.Subcommand,
description: "Shows the data about one of your characters.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"An existing character of yours to display in this channel. Omit to display the current character.",
autocomplete: true,
required: false,
},
{
name: "public",
type: ApplicationCommandOptionType.Boolean,
description:
"True to share your character list with the current channel.",
required: false,
},
],
},
{
name: "archive",
type: ApplicationCommandOptionType.Subcommand,
description:
"Removes a character from all servers and hides it from the default list view.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"An existing unarchived character of yours to add to the archive.",
autocomplete: true,
required: false,
},
],
},
{
name: "unarchive",
type: ApplicationCommandOptionType.Subcommand,
description: "Unarchives a character.",
options: [
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"An existing archived character of yours to take back out of the archive.",
autocomplete: true,
required: false,
},
],
},
];

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

@ -1,380 +1,414 @@
import {
ApplicationCommand,
ApplicationCommandDataResolvable,
ApplicationCommandOptionType,
ApplicationCommandType,
AutocompleteInteraction,
ChatInputCommandInteraction,
Collection,
} from "discord.js"
import {CharacterCommand} from "./character/index"
import {autocompleteNotImplementedError, CommandData} from "./types"
import {BotCommand} from "./bot/index"
import {UserTable} from "../database/users.js"
ApplicationCommand,
ApplicationCommandDataResolvable,
ApplicationCommandOptionType,
ApplicationCommandType,
AutocompleteInteraction,
ChatInputCommandInteraction,
Collection,
} from "discord.js";
import { CharacterCommand } from "./character";
import { autocompleteNotImplementedError, CommandData } from "./types";
import { BotCommand } from "./bot";
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 {
readonly all: CommandData[]
readonly character: CharacterCommand
readonly bot: BotCommand
readonly definitions: ApplicationCommandDataResolvable[]
private readonly _nameCache: Record<string, CommandData>
readonly all: CommandData[];
readonly character: CharacterCommand;
readonly bot: BotCommand;
readonly definitions: ApplicationCommandDataResolvable[];
private readonly _nameCache: Record<string, CommandData>;
constructor({users, cleanUp}: { users: UserTable, cleanUp: () => Promise<void> }) {
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.all.map((c) => [c.definition.name, c]),
)
}
constructor({
users,
cleanUp,
}: {
users: UsersManager;
cleanUp: () => Promise<void>;
}) {
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.all.map((c) => [c.definition.name, c])
);
}
setCache(data: Collection<string, ApplicationCommand>) {
for (const command of data.values()) {
if (this._nameCache.hasOwnProperty(command.name)) {
this._nameCache[command.name].setCached(command)
} else {
console.log("No such command when caching commands: " + command.name)
}
}
setCache(data: Collection<string, ApplicationCommand>) {
for (const command of data.values()) {
if (this._nameCache.hasOwnProperty(command.name)) {
this._nameCache[command.name].setCached(command);
} else {
console.log("No such command when caching commands: " + command.name);
}
}
}
async execute(ev: ChatInputCommandInteraction) {
const name = ev.commandName
if (!this._nameCache.hasOwnProperty(name)) {
throw invalidCommandError
}
await this._nameCache[name].execute(ev)
async execute(ev: ChatInputCommandInteraction) {
const name = ev.commandName;
if (!this._nameCache.hasOwnProperty(name)) {
throw invalidCommandError;
}
await this._nameCache[name].execute(ev);
}
async autocomplete(ev: AutocompleteInteraction) {
const name = ev.commandName
if (this._nameCache.hasOwnProperty(name)) {
const command = this._nameCache[name]
if (command.autocomplete) {
await command.autocomplete(ev)
return
}
}
throw autocompleteNotImplementedError
async autocomplete(ev: AutocompleteInteraction) {
const name = ev.commandName;
if (this._nameCache.hasOwnProperty(name)) {
const command = this._nameCache[name];
if (command.autocomplete) {
await command.autocomplete(ev);
return;
}
}
throw autocompleteNotImplementedError;
}
}
const otherCommands: ApplicationCommandDataResolvable[] = [
{
name: "playstyle",
type: ApplicationCommandType.ChatInput,
description: "Updates your play style.",
{
name: "playstyle",
type: ApplicationCommandType.ChatInput,
description: "Updates your play style.",
options: [
{
name: "difficulty",
type: ApplicationCommandOptionType.Subcommand,
description:
"Alters or displays the reformation difficulty of your currently selected character.",
},
{
name: "preference",
type: ApplicationCommandOptionType.Subcommand,
description:
"Alters or displays the pred/prey preference of your currently selected character.",
},
],
},
{
name: "team",
type: ApplicationCommandType.ChatInput,
description: "Affects teams in battle.",
options: [
{
name: "join",
type: ApplicationCommandOptionType.Subcommand,
description:
"Leaves the current team, if any, and requests to join the same team (and battle) as another player.",
},
{
name: "leave",
type: ApplicationCommandOptionType.Subcommand,
description:
"Leaves the current team, if any, and strikes it out alone.",
},
],
},
{
name: "battle",
type: ApplicationCommandType.ChatInput,
description: "Takes actions in battle.",
options: [
{
name: "challenge",
type: ApplicationCommandOptionType.Subcommand,
description: "Challenges another character to a battle.",
options: [
{
name: "target",
type: ApplicationCommandOptionType.String,
description: "The character you want to challenge.",
autocomplete: true,
},
],
},
{
name: "menu",
type: ApplicationCommandOptionType.Subcommand,
description: "Opens the battle menu to select an action for this turn.",
},
{
name: "skill",
type: ApplicationCommandOptionType.Subcommand,
description: "Uses a skill.",
options: [
{
name: "name",
type: ApplicationCommandOptionType.String,
description: "The name of the skill to use. Omit to see a list.",
autocomplete: true,
required: false,
},
{
name: "target",
type: ApplicationCommandOptionType.String,
description:
"The target to use the skill on, if there are multiple options. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "item",
type: ApplicationCommandOptionType.Subcommand,
description: "Uses an item.",
options: [
{
name: "name",
type: ApplicationCommandOptionType.String,
description: "The name of the item to use. Omit to see a list.",
autocomplete: true,
required: false,
},
{
name: "target",
type: ApplicationCommandOptionType.String,
description:
"The target to use the item on, if there are multiple options. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "vore",
type: ApplicationCommandOptionType.Subcommand,
description:
"Devours your opponent. Requires your opponent to be at low Confidence.",
options: [
{
name: "style",
type: ApplicationCommandOptionType.String,
description: "The style of devouring to use. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "rest",
type: ApplicationCommandOptionType.Subcommand,
description:
"Takes a breather, recovering your Confidence and Energy at a faster-than-normal rate for this turn.",
},
{
name: "escape",
type: ApplicationCommandOptionType.Subcommand,
description:
"Tries to escape from a battle or stomach. Requires your opponent to be at low Confidence.",
},
{
name: "surrender",
type: ApplicationCommandOptionType.Subcommand,
description:
"Lets your opponent do as they will with you by reducing your Confidence or Health to 0.",
},
{
name: "prey",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Commands for when you have a full belly.",
options: [
{
name: "difficulty",
type: ApplicationCommandOptionType.Subcommand,
description: "Alters or displays the reformation difficulty of your currently selected character.",
},
{
name: "preference",
type: ApplicationCommandOptionType.Subcommand,
description: "Alters or displays the pred/prey preference of your currently selected character.",
},
{
name: "release",
type: ApplicationCommandOptionType.Subcommand,
description: "Releases your prey from your stomach.",
},
{
name: "squish",
type: ApplicationCommandOptionType.Subcommand,
description:
"Squishes your prey, ending the combat. Can only be performed when the prey is at or below 0% Health.",
},
],
},
{
name: "team",
type: ApplicationCommandType.ChatInput,
description: "Affects teams in battle.",
},
],
},
{
name: "safety",
type: ApplicationCommandType.ChatInput,
description: "Safety commands for protecting consent and user data.",
options: [
{
name: "exit",
type: ApplicationCommandOptionType.SubcommandGroup,
description:
"Exits battle(s) and reverts its/their effects on you. To flee in-character, use /battle escape.",
},
{
name: "block",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages your block list.",
options: [
{
name: "join",
type: ApplicationCommandOptionType.Subcommand,
description: "Leaves the current team, if any, and requests to join the same team (and battle) as another player.",
},
{
name: "leave",
type: ApplicationCommandOptionType.Subcommand,
description: "Leaves the current team, if any, and strikes it out alone.",
},
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description:
"Prevents a user or character from challenging you or joining battles you're in.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description:
"The user to block from challenging you or joining battles you're in.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"The character to block from challenging you or joining battles you're in.",
required: false,
autocomplete: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description:
"Allows a user or character to challenge you and join battles you're in once more.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description:
"The user to allow to challenge you and join battles you're in once more.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"The character to allow to challenge you and join battles you're in once more.",
required: false,
autocomplete: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Displays your current block list.",
},
],
},
{
name: "battle",
type: ApplicationCommandType.ChatInput,
description: "Takes actions in battle.",
},
{
name: "kick",
type: ApplicationCommandOptionType.SubcommandGroup,
description:
"Manages the list of users kicked from the current battle.",
options: [
{
name: "challenge",
type: ApplicationCommandOptionType.Subcommand,
description: "Challenges another character to a battle.",
options: [
{
name: "target",
type: ApplicationCommandOptionType.String,
description: "The character you want to challenge.",
autocomplete: true,
},
],
},
{
name: "menu",
type: ApplicationCommandOptionType.Subcommand,
description: "Opens the battle menu to select an action for this turn.",
},
{
name: "skill",
type: ApplicationCommandOptionType.Subcommand,
description: "Uses a skill.",
options: [
{
name: "name",
type: ApplicationCommandOptionType.String,
description: "The name of the skill to use. Omit to see a list.",
autocomplete: true,
required: false,
},
{
name: "target",
type: ApplicationCommandOptionType.String,
description: "The target to use the skill on, if there are multiple options. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "item",
type: ApplicationCommandOptionType.Subcommand,
description: "Uses an item.",
options: [
{
name: "name",
type: ApplicationCommandOptionType.String,
description: "The name of the item to use. Omit to see a list.",
autocomplete: true,
required: false,
},
{
name: "target",
type: ApplicationCommandOptionType.String,
description: "The target to use the item on, if there are multiple options. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "vore",
type: ApplicationCommandOptionType.Subcommand,
description: "Devours your opponent. Requires your opponent to be at low Confidence.",
options: [
{
name: "style",
type: ApplicationCommandOptionType.String,
description: "The style of devouring to use. Omit to see a list.",
autocomplete: true,
required: false,
},
],
},
{
name: "rest",
type: ApplicationCommandOptionType.Subcommand,
description: "Takes a breather, recovering your Confidence and Energy at a faster-than-normal rate for this turn.",
},
{
name: "escape",
type: ApplicationCommandOptionType.Subcommand,
description: "Tries to escape from a battle or stomach. Requires your opponent to be at low Confidence.",
},
{
name: "surrender",
type: ApplicationCommandOptionType.Subcommand,
description: "Lets your opponent do as they will with you by reducing your Confidence or Health to 0.",
},
{
name: "prey",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Commands for when you have a full belly.",
options: [
{
name: "release",
type: ApplicationCommandOptionType.Subcommand,
description: "Releases your prey from your stomach.",
},
{
name: "squish",
type: ApplicationCommandOptionType.Subcommand,
description: "Squishes your prey, ending the combat. Can only be performed when the prey is at or below 0% Health.",
},
],
},
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description:
"Removes a user or character from the current battle and prevents them from rejoining.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description:
"The user to remove from the current battle and prevent from rejoining.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description:
"The character to remove from the current battle and prevent from rejoining.",
required: false,
autocomplete: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description:
"Allows a user or character to rejoin the current battle.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to allow to rejoin the current battle.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The user to allow to rejoin the current battle.",
required: false,
autocomplete: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description:
"Displays the list of users forbidden from rejoining the current battle.",
},
],
},
{
name: "safety",
type: ApplicationCommandType.ChatInput,
description: "Safety commands for protecting consent and user data.",
},
{
name: "ban",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages the ban list of the current server.",
options: [
{
name: "exit",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Exits battle(s) and reverts its/their effects on you. To flee in-character, use /battle escape.",
},
{
name: "block",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages your block list.",
options: [
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description: "Prevents a user or character from challenging you or joining battles you're in.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to block from challenging you or joining battles you're in.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to block from challenging you or joining battles you're in.",
required: false,
autocomplete: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user or character to challenge you and join battles you're in once more.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to allow to challenge you and join battles you're in once more.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to allow to challenge you and join battles you're in once more.",
required: false,
autocomplete: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Displays your current block list.",
},
],
},
{
name: "kick",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages the list of users kicked from the current battle.",
options: [
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description: "Removes a user or character from the current battle and prevents them from rejoining.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to remove from the current battle and prevent from rejoining.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The character to remove from the current battle and prevent from rejoining.",
required: false,
autocomplete: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user or character to rejoin the current battle.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to allow to rejoin the current battle.",
required: false,
},
{
name: "character",
type: ApplicationCommandOptionType.String,
description: "The user to allow to rejoin the current battle.",
required: false,
autocomplete: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Displays the list of users forbidden from rejoining the current battle.",
},
],
},
{
name: "ban",
type: ApplicationCommandOptionType.SubcommandGroup,
description: "Manages the ban list of the current server.",
options: [
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description: "Bans a user from participating in battles in the current server and kicks them from any they're in.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to ban from participating in battles in the current server.",
required: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description: "Allows a user to resume participating in battles in the current server.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description: "The user to allow to participate in battles in the current server once more.",
required: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Displays this server's current ban list.",
},
],
},
{
name: "delete",
type: ApplicationCommandOptionType.Subcommand,
description: "PERMANENTLY deletes a character, exiting any battles they were part of. See also /character archive.",
},
{
name: "wipe",
type: ApplicationCommandOptionType.Subcommand,
description: "PERMANENTLY deletes ALL data about you in the system, erasing you from the system's knowledge.",
},
{
name: "add",
type: ApplicationCommandOptionType.Subcommand,
description:
"Bans a user from participating in battles in the current server and kicks them from any they're in.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description:
"The user to ban from participating in battles in the current server.",
required: true,
},
],
},
{
name: "remove",
type: ApplicationCommandOptionType.Subcommand,
description:
"Allows a user to resume participating in battles in the current server.",
options: [
{
name: "user",
type: ApplicationCommandOptionType.User,
description:
"The user to allow to participate in battles in the current server once more.",
required: true,
},
],
},
{
name: "list",
type: ApplicationCommandOptionType.Subcommand,
description: "Displays this server's current ban list.",
},
],
},
]
},
{
name: "delete",
type: ApplicationCommandOptionType.Subcommand,
description:
"PERMANENTLY deletes a character, exiting any battles they were part of. See also /character archive.",
},
{
name: "wipe",
type: ApplicationCommandOptionType.Subcommand,
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 {UserTable, UserTableImpl} from "./users.js"
import {Pool, PoolConfig, QueryResult, QueryResultRow} from "pg"
import {UserTable, UserTableImpl, UserTableInMemory} from "./users.js"
import {BattleTypeTable, BattleTypeTableImpl} from "./battle_types";
import {GenderTable, GenderTablesImpl} from "./genders";
import {CharacterTable, CharacterTableImpl} from "./characters";
export interface PoolLike {
export interface PoolLike extends Queryable {
connect(): Promise<PoolClientLike>
on(type: 'error', handler: (err: unknown) => void): void
end(): Promise<void>
}
export interface PoolClientLike extends Queryable {
@ -13,7 +17,10 @@ export interface PoolClientLike extends Queryable {
}
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 query = client.query.bind(client)
let committed = false
@ -52,6 +59,19 @@ export interface Database {
readonly users: UserTable
readonly genders: GenderTable
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 {
@ -62,18 +82,49 @@ export interface Queryable {
}
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 {
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 users: UserTableImpl
readonly genders: GenderTable
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.users = new UserTableImpl({query})
this.genders = new GenderTablesImpl({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>
}
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 {
private readonly _query: QueryType

@ -2,19 +2,27 @@ import {Snowflake} from "discord-api-types/globals";
import {CharacterTable} from "../database/characters";
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