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.
152 lines
5.2 KiB
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})
|
|
}
|
|
} |