Enable Gulp-chan to shut down, restart, and rebuild herself.

main
Mari 2 years ago
parent f11b5b28bf
commit fe448ce99c
  1. 21
      src/commands/bot/index.ts
  2. 78
      src/commands/bot/rebuild.ts
  3. 39
      src/commands/bot/restart.ts
  4. 27
      src/commands/bot/shutdown.ts
  5. 25
      src/commands/character/create.ts
  6. 135
      src/commands/character/index.ts
  7. 356
      src/commands/index.ts
  8. 154
      src/commands/types.ts
  9. 11
      src/defaultPresence.ts
  10. 200
      src/ipc/restart.ts
  11. 2
      src/main.spec.ts
  12. 546
      src/main.ts
  13. 5
      src/types/interactions.ts
  14. 3
      tsconfig.json

@ -0,0 +1,21 @@
import {BaseChatInputCommandData, CommandWithSubcommandsData} from "../types.js"
import {ApplicationCommandType} from "discord.js"
import {commandBotRestart} from "./restart.js"
import {commandBotShutdown} from "./shutdown.js"
import {commandBotRebuild} from "./rebuild.js"
class BotCommandData extends CommandWithSubcommandsData {
readonly baseDefinition: BaseChatInputCommandData = {
name: "bot",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage the bot's status.",
}
readonly subcommands = [
commandBotRebuild,
commandBotRestart,
commandBotShutdown,
]
}
export const commandBot = new BotCommandData()

@ -0,0 +1,78 @@
import {adminId, SubcommandData} from "../types.js"
import {
ActivityType,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ChatInputCommandInteraction,
} from "discord.js"
import {wrappedRestart} from "../../ipc/restart.js"
import {spawn} from "child_process"
import {defaultPresence} from "../../defaultPresence.js"
import {resolve as resolvePath} from "path"
class RebuildCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "rebuild",
description: "Rebuilds and restarts the bot.",
type: ApplicationCommandOptionType.Subcommand,
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
await b.reply("Mmm... let's see... this goes here...")
const self = b.client.user
self.setPresence({
status: "online",
afk: false,
activities: [
{
type: ActivityType.Watching,
name: "Compilers at Work",
},
],
})
try {
await new Promise<void>((resolve, reject) => {
const result = spawn(resolvePath("./node_modules/.bin/tsc"), {
cwd: process.cwd(),
stdio: "inherit",
detached: true,
shell: true,
})
result.on("error", (ex) => {
reject(ex)
})
result.on("exit", (code) => {
if (code !== 0) {
reject(`Bad exit code ${code}`)
} else {
resolve()
}
})
})
} catch (ex) {
console.log(ex)
self.setPresence(defaultPresence)
await b.followUp("Oops... I think it's broke...")
return
}
self.setPresence({
status: "idle",
afk: true,
activities: [
{
type: ActivityType.Listening,
name: "Born-Again Bot Girl",
},
],
})
await b.followUp("Phewwww... now I'll just... take a quick nap after all that hard work...")
await wrappedRestart(b)
} else {
await b.reply("Heeey... I don't gotta do what you say...")
}
}
}
export const commandBotRebuild = new RebuildCommand()

@ -0,0 +1,39 @@
import {adminId, SubcommandData} from "../types.js"
import {
ActivityType,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ChatInputCommandInteraction,
} from "discord.js"
import {wrappedRestart} from "../../ipc/restart.js"
class RestartCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "restart",
type: ApplicationCommandOptionType.Subcommand,
description: "Tells the bot to restart.",
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
await b.reply("Yaaaawwn... Okay... Just a quick nap then...")
const self = b.client.user
self.setPresence({
status: "idle",
afk: true,
activities: [
{
type: ActivityType.Listening,
name: "A New Day",
},
],
})
await wrappedRestart(b)
} else {
await b.reply("Heeey... I don't gotta do what you say...")
}
}
}
export const commandBotRestart = new RestartCommand()

@ -0,0 +1,27 @@
import {adminId, SubcommandData} from "../types.js"
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js"
class ShutdownCommand extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "shutdown",
type: ApplicationCommandOptionType.Subcommand,
description: "Tells the bot to shut down.",
}
async execute(b: ChatInputCommandInteraction): Promise<void> {
const user = b.user || b.member
if (!!user && user.id === adminId) {
await b.reply("Good night =w=")
const self = b.client.user
self.presence.set({
status: "invisible",
activities: [],
})
b.client.destroy()
} else {
await b.reply("Heeey... I don't gotta do what you say...")
}
}
}
export const commandBotShutdown = new ShutdownCommand()

@ -0,0 +1,25 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ChatInputCommandInteraction} from "discord.js"
import {SubcommandData} from "../types.js"
class CharacterCreateCommandData extends SubcommandData {
readonly definition: ApplicationCommandSubCommandData = {
name: "create",
type: ApplicationCommandOptionType.Subcommand,
description: "Creates a new character. Activates them on the current server.",
options: [
{
name: "template",
type: ApplicationCommandOptionType.String,
description: "Optionally, an existing character of yours to use as a template.",
autocomplete: true,
required: false,
},
],
}
async execute(b: ChatInputCommandInteraction) {
await b.reply("Okaaaay, I'll make you a character ❤\n\nRight after this nap...")
}
}
export const commandCharacterCreate = new CharacterCreateCommandData()

@ -0,0 +1,135 @@
import {ApplicationCommandOptionType, ApplicationCommandSubCommandData, ApplicationCommandType} from "discord.js"
import {commandCharacterCreate} from "./create.js"
import {BaseChatInputCommandData, CommandWithSubcommandsData} from "../types.js"
class CharacterCommandData extends CommandWithSubcommandsData {
readonly baseDefinition: BaseChatInputCommandData = {
name: "character",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.",
}
readonly subcommands = [
commandCharacterCreate,
]
}
export const commandCharacter = new CharacterCommandData()
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,
},
],
},
]

@ -0,0 +1,356 @@
import {
ApplicationCommand,
ApplicationCommandDataResolvable,
ApplicationCommandOptionType,
ApplicationCommandType,
ChatInputCommandInteraction,
Collection,
} from "discord.js"
import {commandCharacter} from "./character/index.js"
import {CommandData} from "./types.js"
import {commandBot} from "./bot/index.js"
const commands: CommandData[] = [
commandCharacter,
commandBot,
]
export const commandDefinitions: ApplicationCommandDataResolvable[] = commands.map((c) => c.definition)
const _commandNameCache: Record<string, CommandData> = Object.fromEntries(
commands.map((c) => [c.definition.name, c]),
)
export const invalidCommandError = "No command by that name exists."
export function storeCachedCommands(data: Collection<string, ApplicationCommand>) {
for (const command of data.values()) {
if (_commandNameCache.hasOwnProperty(command.name)) {
_commandNameCache[command.name].setCached(command)
} else {
console.log("No such command when caching commands: " + command.name)
}
}
}
export async function executeCommand(ev: ChatInputCommandInteraction) {
const name = ev.commandName
if (!_commandNameCache.hasOwnProperty(name)) {
throw invalidCommandError
}
await _commandNameCache[name].execute(ev)
}
const otherCommands: ApplicationCommandDataResolvable[] = [
{
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: "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: "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: "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.",
},
],
},
]

@ -0,0 +1,154 @@
import {
ApplicationCommand,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
ChatInputApplicationCommandData,
ChatInputCommandInteraction,
} from "discord.js"
export const noSubcommandError = "No subcommand was provided, but one is required."
export const invalidSubcommandGroupError = "The subcommand group provided does not exist."
export const invalidSubcommandError = "The subcommand provided does not exist."
export const adminId = "126936953789743104"
export abstract class CommandData {
private _cachedGlobal: ApplicationCommand | null = null
private _cacheByGuild: Record<string, ApplicationCommand> = {}
abstract get definition(): ChatInputApplicationCommandData
getCached(guildId: string | null): ApplicationCommand {
if (guildId === null) {
if (this._cachedGlobal === null) {
throw Error("Not yet cached")
}
return this._cachedGlobal
} else if (this._cacheByGuild.hasOwnProperty(guildId)) {
return this._cacheByGuild[guildId]
} else {
throw Error("Not yet cached")
}
}
setCached(command: ApplicationCommand) {
if (command.guildId === null) {
this._cachedGlobal = command
} else {
this._cacheByGuild[command.guildId] = command
}
}
abstract execute(b: ChatInputCommandInteraction): Promise<void>
}
export type BaseChatInputCommandData = Omit<ChatInputApplicationCommandData, "options">
export abstract class CommandWithSubcommandsData extends CommandData {
private _subcommandGroupsCache: Record<string, SubcommandGroupData> | null = null
private _subcommandsCache: Record<string, SubcommandData> | null = null
get definition(): ChatInputApplicationCommandData {
return {
options: this.subcommands.map((s) => s.definition),
...this.baseDefinition,
}
}
protected abstract get baseDefinition(): BaseChatInputCommandData
protected abstract get subcommands(): (SubcommandData | SubcommandGroupData)[]
async execute(b: ChatInputCommandInteraction) {
const group = b.options.getSubcommandGroup()
const subcommand = b.options.getSubcommand()
if (group !== null) {
await this.resolveSubcommandGroup(group).execute(b)
} else if (subcommand !== null) {
await this.resolveSubcommand(subcommand).execute(b)
} else {
throw noSubcommandError
}
}
protected resolveSubcommandGroup(name: string): SubcommandGroupData {
let cache = this._subcommandGroupsCache
if (cache === null) {
cache = {}
for (const item of this.subcommands) {
if (item instanceof SubcommandGroupData) {
cache[item.definition.name] = item
}
}
this._subcommandGroupsCache = cache
}
if (!cache.hasOwnProperty(name)) {
throw invalidSubcommandGroupError
}
return cache[name]
}
protected resolveSubcommand(name: string): SubcommandData {
let cache = this._subcommandsCache
if (cache === null) {
cache = {}
for (const item of this.subcommands) {
if (item instanceof SubcommandData) {
cache[item.definition.name] = item
}
}
this._subcommandsCache = cache
}
if (!cache.hasOwnProperty(name)) {
throw invalidSubcommandGroupError
}
return cache[name]
}
}
export type BaseSubcommandGroupData = Omit<ApplicationCommandSubGroupData, "options">
export abstract class SubcommandGroupData {
private _subcommandsCache: Record<string, SubcommandData> | null = null
get definition(): ApplicationCommandSubGroupData {
return {
...this.baseDefinition,
options: this.subcommands.map((s) => s.definition),
}
}
protected abstract get baseDefinition(): BaseSubcommandGroupData
protected abstract get subcommands(): SubcommandData[]
async execute(b: ChatInputCommandInteraction): Promise<void> {
const subcommand = b.options.getSubcommand()
if (subcommand !== null) {
await this.resolveSubcommand(subcommand).execute(b)
} else {
throw noSubcommandError
}
}
protected resolveSubcommand(name: string): SubcommandData {
let cache = this._subcommandsCache
if (cache === null) {
cache = {}
for (const item of this.subcommands) {
cache[item.definition.name] = item
}
this._subcommandsCache = cache
}
if (!cache.hasOwnProperty(name)) {
throw invalidSubcommandError
}
return cache[name]
}
}
export abstract class SubcommandData {
abstract get definition(): ApplicationCommandSubCommandData
abstract execute(b: ChatInputCommandInteraction): Promise<void>
}

@ -0,0 +1,11 @@
import {ActivityType, PresenceData} from "discord.js"
export const defaultPresence: PresenceData = {
status: "online",
activities: [
{
name: "Vore PvP",
type: ActivityType.Playing,
},
],
}

@ -0,0 +1,200 @@
import {fork} from "child_process"
import {ChatInputCommandInteraction, Client, InteractionWebhook} from "discord.js"
export enum RestartReason {
RestartCommand = "restart",
}
export enum StartState {
NeverStarted = "never_started",
Errored = "errored",
Crashed = "crashed",
Started = "started",
Ready = "ready",
Failed = "failed",
}
export interface StartData {
state: StartState,
status?: string
}
export interface RestartData {
appId: string
token: string
reason: RestartReason
}
let restartState: RestartData | null = null
export async function checkIsRestart(): Promise<RestartData | null> {
if (!process.connected) {
return null
} else {
try {
const result = await new Promise<RestartData | null>((resolve) => {
process.on("message", (value: { restart?: RestartData }) => {
if (value && value.restart) {
resolve(value.restart)
} else {
resolve(null)
}
})
setTimeout(() => resolve(null), 50)
})
if (result !== null) {
restartState = result
}
return result
} catch (ex) {
return null
}
}
}
export async function reportStarted(): Promise<void> {
if (!process.send) {
return
}
const startData: StartData = {
state: StartState.Started,
}
process.send(startData)
}
export async function reportReady(c: Client<true>): Promise<boolean> {
if (restartState) {
try {
const hook = new InteractionWebhook(c, restartState.appId, restartState.token)
await hook.send({
content: "yawwwwn... Good morning...",
})
} catch (ex) {
console.log("followup failed to send", ex)
}
}
if (!process.send) {
return false
}
const startData: StartData = {
state: StartState.Ready,
}
process.send(startData)
return true
}
export async function reportFailed(c: Client, error: unknown): Promise<void> {
console.log("failed to start", error)
if (restartState) {
try {
const hook = new InteractionWebhook(c, restartState.appId, restartState.token)
await hook.send({
content: "Ugh... I can't get up... Head hurts... sorry...",
})
} catch (ex) {
console.log("followup failed to send", ex)
}
}
if (!process.send) {
return
}
const startData: StartData = {
state: StartState.Failed,
status: `${error}`,
}
process.send(startData)
}
export async function wrappedRestart(b: ChatInputCommandInteraction) {
try {
await doRestart(b.client, b.webhook.id, b.webhook.token, RestartReason.RestartCommand)
} catch (ex) {
console.log("failed doing restart", ex)
let data: StartData | null = null
if (typeof ex === "object" && ex !== null && ex.hasOwnProperty("state")) {
data = ex as StartData
}
if ((data && data.state)) {
try {
b.client.user.presence.set({
status: "invisible",
activities: [],
})
await b.followUp("Mhhhh... I don't feel good...")
} catch (ex) {
console.log("failed resetting presence and sending followup", ex)
}
}
} finally {
b.client.destroy()
}
}
export async function doRestart(client: Client, appId: string, token: string,
restartReason: RestartReason): Promise<StartData> {
return new Promise((resolve, reject) => {
const child = fork(process.argv[1], process.argv.slice(2), {
execPath: process.execPath,
cwd: process.cwd(),
detached: true,
stdio: "inherit",
})
let result: StartData = {
state: StartState.NeverStarted,
}
child.once("error", (err) => {
result.state = StartState.Errored
result.status = `${err}`
if (child.connected) {
child.unref()
child.disconnect()
}
reject({...result})
})
child.once("exit", (code, signal) => {
result.state = StartState.Crashed
result.status = `Exited unexpectedly with code ${code} (signal ${signal})`
if (child.connected) {
child.unref()
child.disconnect()
}
reject({...result})
})
child.on("message", (message: StartData) => {
switch (typeof message === "object" && message.state) {
case StartState.Started:
result.state = StartState.Started
break
case StartState.Ready:
resolve({...message})
if (child.connected) {
child.unref()
child.disconnect()
}
break
case StartState.Failed:
reject({...message})
if (child.connected) {
child.unref()
child.disconnect()
}
break
default:
result.state = StartState.Failed
result.status = `Unexpected message: ${message}`
reject({...message})
if (child.connected) {
child.unref()
child.disconnect()
}
break
}
})
process.on("disconnect", () => {
result.state = StartState.Crashed
reject({...result})
child.unref()
})
child.send({restart: {appId, token, reason: restartReason}})
})
}

@ -1,4 +1,4 @@
import { describe, test, expect } from '@jest/globals'
import {describe, expect, test} from "@jest/globals"
describe("Jest testing", () => {
test("runs", () => {

@ -1,496 +1,100 @@
import {
ApplicationCommandDataResolvable,
ApplicationCommandOptionType,
ApplicationCommandType,
Client,
GatewayIntentBits
} from 'discord.js'
import {config} from 'dotenv'
const commands: ApplicationCommandDataResolvable[] = [
{
name: "character",
type: ApplicationCommandType.ChatInput,
description: "Commands to manage your characters.",
options: [
{
name: "create",
type: ApplicationCommandOptionType.Subcommand,
description: "Creates a new character. Activates them on the current server.",
options: [
{
name: "template",
type: ApplicationCommandOptionType.String,
description: "Optionally, an existing character of yours to use as a template.",
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,
},
],
},
],
},
{
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: "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: "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: "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.",
},
],
}
]
import {BaseInteraction, Client} from "discord.js"
import {config} from "dotenv"
import {isChatInputCommand} from "./types/interactions.js"
import {commandDefinitions, executeCommand, storeCachedCommands} from "./commands/index.js"
import {checkIsRestart, reportFailed, reportReady, reportStarted} from "./ipc/restart.js"
import {defaultPresence} from "./defaultPresence.js"
async function main() {
await checkIsRestart()
config()
const c = new Client({
intents: [],
})
c.on('ready', async () => {
c.on("ready", async () => {
const app = c.application
if (!c.application) {
console.log("Bot is ready, but no application")
if (!app) {
c.destroy()
await reportFailed(c, "No application was given")
return
} else {
try {
await c.application.commands.set(commands)
console.log("Commands established")
storeCachedCommands(await app.commands.set(commandDefinitions))
const g = await c.guilds.fetch()
for (const guild of g.values()) {
storeCachedCommands(await app.commands.set(commandDefinitions, guild.id))
}
} catch (ex) {
console.log("Bot is ready, but setting commands failed: " + ex)
c.destroy()
await reportFailed(c, ex)
return
}
}
const user = c.user
if (!user) {
c.destroy()
await reportFailed(c, "No user found")
return
} else {
user.setPresence(defaultPresence)
}
try {
await reportReady(c)
} catch (ex) {
console.log(ex)
}
})
c.on('error', async (ex) => {
console.log("Connection error: " + ex)
c.on("error", async (ex) => {
c.destroy()
await reportFailed(c, ex)
})
c.on('interactionCreate', async (ev) => {
c.on("interactionCreate", async (ev: BaseInteraction) => {
if (ev.client.user.presence.status !== "online") {
if (ev.isRepliable()) {
try {
await ev.reply("Huuuuuuuh? ... I don't know what to do with that yet.")
console.log("answered an interaction")
await ev.reply({
content: "Shhh... I'm sleeping!! Try again later.",
ephemeral: true,
})
} catch (ex) {
console.log("failed sending busy reply", ex)
}
}
return
}
if (isChatInputCommand(ev)) {
try {
await executeCommand(ev)
} catch (ex) {
console.log("failed executing command", ev, ex)
if (!ev.replied) {
try {
await ev.reply({
ephemeral: true,
content: "Uuguuu... I can't think straight... try again later, 'kay?",
})
} catch (innerEx) {
console.log("failed sending error response", innerEx)
}
}
}
} else if (ev.isRepliable()) {
try {
await ev.reply({
ephemeral: true,
content: "Huuuuuuuh? ... I don't know what to do with that yet.",
})
} catch (ex) {
console.log("failed answering an interaction: " + ex)
console.log("failed sending unknown command response", ex)
}
} else {
console.log("got an interaction but can't reply to it")
}
})
await reportStarted()
await c.login(process.env.DISCORD_TOKEN || "")
}
main().catch((ex) => {console.log("main thread failed: " + ex)})
main().catch((ex) => {
console.log("main thread failed", ex)
})

@ -0,0 +1,5 @@
import {BaseInteraction, ChatInputCommandInteraction} from "discord.js"
export function isChatInputCommand(x: BaseInteraction): x is ChatInputCommandInteraction {
return x.isChatInputCommand()
}

@ -16,7 +16,8 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"outDir": "build"
"outDir": "build",
"sourceMap": true
},
"include": [
"src"

Loading…
Cancel
Save