Gacha game centered around vore.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
vore-gacha/src/SavedWebhook.ts

152 lines
5.2 KiB

/** */
import {APIWebhook} from "discord-api-types";
import {FastifyLoggerInstance} from "fastify";
import {mkdir as fsMkdir, readFile as fsReadFile, rm as fsRm, writeFile as fsWriteFile} from "fs/promises";
import axios from "axios";
import {join} from "path";
import {isENOENT} from "./NodeErrorHelpers.js";
const WebhookPath = "runtime/webhooks"
/** File-based storage for webhook instances. */
export class SavedWebhook {
readonly filename: string
private readonly logger: FastifyLoggerInstance | null
private readonly readFile: typeof fsReadFile
private readonly writeFile: typeof fsWriteFile
private readonly mkdir: typeof fsMkdir
private readonly rm: typeof fsRm
private readonly delRequest: typeof axios["delete"]
/** Initializes a SavedWebhook pointing at the given location. */
constructor(filename: string, {
logger,
state,
readFile,
writeFile,
mkdir,
rm,
delRequest
}: { logger?: FastifyLoggerInstance | null, state?: APIWebhook | null, readFile?: typeof fsReadFile, writeFile?: typeof fsWriteFile, mkdir?: typeof fsMkdir, rm?: typeof fsRm, delRequest?: typeof axios["delete"] }) {
this.filename = filename
this._state = state
this.logger = logger?.child({webhookFile: filename}) ?? null
this.readFile = readFile ?? fsReadFile
this.writeFile = writeFile ?? fsWriteFile
this.mkdir = mkdir ?? fsMkdir
this.rm = rm ?? fsRm
this.delRequest = delRequest ?? axios.delete.bind(axios)
}
private _state: APIWebhook | null | undefined
/** Gets the current state of the webhook, either a webhook instance or null. */
get state(): APIWebhook | null {
if (this._state === undefined) {
this.logger?.warn("SavedWebhook was not initialized before having its state checked")
return null
}
return this._state
}
/** The path of the webhook, including WebhookPath. */
get path(): string {
return join(WebhookPath, this.filename)
}
/** Gets whether a webhook has been added to this SavedWebhook. */
get isPresent(): boolean {
return this.state !== null
}
/** Loads the current state from the disk, including a null state if not present. */
async load(): Promise<APIWebhook | null> {
if (this._state !== undefined) {
this.logger?.warn(`SavedWebhook was double-initialized`)
}
try {
const text = await this.readFile(this.path, {encoding: "utf-8"})
const state: APIWebhook = JSON.parse(text)
this._state = state
return state
} catch (e) {
if (isENOENT(e)) {
this._state = null
return null
}
throw e
}
}
/** Replaces the webhook with a new one. */
async replaceHook(newHook: APIWebhook): Promise<void> {
if (this._state === undefined) {
this.logger?.warn("SavedWebhook was not initialized before being replaced")
return
}
if (this._state !== null) {
await this.clearHook()
}
this._state = newHook
await this.save()
}
/** Removes the webhook entirely. */
async clearHook(): Promise<void> {
if (this._state === undefined) {
this.logger?.warn("SavedWebhook was not initialized before being cleared")
return
}
if (this._state === null) {
this.logger?.warn("SavedWebhook was already cleared and cleared again")
return
}
await this.deleteHook()
this._state = null
await this.save()
}
/** Saves the current state to disk, deleting the file if the state is null. */
private async save(): Promise<void> {
if (this._state === undefined) {
this.logger?.warn("SavedWebhook was not initialized before being saved")
return
}
if (this._state === null) {
try {
await this.rm(this.path)
} catch (e) {
if (!isENOENT(e)) {
throw e
}
this.logger?.warn("SavedWebhook was re-deleted despite not existing to begin with.")
}
return
}
try {
const text = JSON.stringify(this._state)
await this.writeFile(this.path, text, {encoding: "utf-8"})
} catch (e) {
if (isENOENT(e)) {
await this.mkdir(WebhookPath, {recursive: true})
const text = JSON.stringify(this._state)
await this.writeFile(this.path, text, {encoding: "utf-8"})
}
throw e
}
}
/** Deletes the webhook from the server. */
private async deleteHook(): Promise<void> {
if (this._state === undefined) {
this.logger?.warn("SavedWebhook was not initialized before being deleted")
return
}
if (this._state === null) {
this.logger?.warn("SavedWebhook was deleted despite being empty")
return
}
await this.delRequest(`https://discord.com/api/webhooks/${this._state.id}/${this._state.token}`,
{validateStatus: (s) => s === 204 || s === 404})
}
}