parent
162aa1422b
commit
48b0502cd7
@ -0,0 +1,5 @@ |
|||||||
|
<component name="ProjectCodeStyleConfiguration"> |
||||||
|
<state> |
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> |
||||||
|
</state> |
||||||
|
</component> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@ |
|||||||
|
import {SavedWebhook} from "./SavedWebhook.js"; |
||||||
|
import fastify, {FastifyInstance} from "fastify"; |
||||||
|
import {SlashCreator, SlashCreatorOptions} from "slash-create"; |
||||||
|
import fastifyCookie from "fastify-cookie"; |
||||||
|
import {FastifyServerButItWorksUnlikeTheRealOne} from "./FastifyHelpers.js"; |
||||||
|
|
||||||
|
export interface BaseServerDeps { |
||||||
|
appId: string |
||||||
|
botToken: string |
||||||
|
clientSecret: string |
||||||
|
publicKey: string |
||||||
|
listenPort: number |
||||||
|
listenAddress: string |
||||||
|
cookieSecret: string |
||||||
|
gameWebhook: SavedWebhook |
||||||
|
adminWebhook: SavedWebhook |
||||||
|
} |
||||||
|
|
||||||
|
export class BaseServer { |
||||||
|
readonly server: FastifyInstance |
||||||
|
readonly gameWebhook: SavedWebhook |
||||||
|
readonly adminWebhook: SavedWebhook |
||||||
|
readonly appId: string |
||||||
|
readonly clientSecret: string |
||||||
|
readonly listenPort: number |
||||||
|
readonly listenAddress: string |
||||||
|
readonly slashcmd: SlashCreator |
||||||
|
|
||||||
|
constructor({ |
||||||
|
cookieSecret, |
||||||
|
appId, |
||||||
|
clientSecret, |
||||||
|
listenPort, |
||||||
|
listenAddress, |
||||||
|
gameWebhook, |
||||||
|
adminWebhook, |
||||||
|
publicKey, |
||||||
|
botToken, |
||||||
|
slashCreatorOptions = {} |
||||||
|
}: BaseServerDeps & { slashCreatorOptions?: Partial<SlashCreatorOptions> }) { |
||||||
|
this.slashcmd = new SlashCreator({ |
||||||
|
allowedMentions: {everyone: false, roles: false, users: false}, |
||||||
|
applicationID: appId, |
||||||
|
defaultImageFormat: "webp", |
||||||
|
handleCommandsManually: false, |
||||||
|
maxSignatureTimestamp: 3000, |
||||||
|
publicKey, |
||||||
|
requestTimeout: 10000, |
||||||
|
token: botToken, |
||||||
|
unknownCommandResponse: true, |
||||||
|
endpointPath: "/interactions", |
||||||
|
...slashCreatorOptions, |
||||||
|
}) |
||||||
|
this.server = fastify({logger: true}) |
||||||
|
this.slashcmd.withServer(new FastifyServerButItWorksUnlikeTheRealOne(this.server, {alreadyListening: true})) |
||||||
|
this.server.register(fastifyCookie, { |
||||||
|
secret: cookieSecret, |
||||||
|
}) |
||||||
|
this.appId = appId |
||||||
|
this.clientSecret = clientSecret |
||||||
|
this.listenPort = listenPort |
||||||
|
this.listenAddress = listenAddress |
||||||
|
this.gameWebhook = gameWebhook |
||||||
|
this.adminWebhook = adminWebhook |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
async _initInternal(): Promise<void> { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
async initialize(): Promise<void> { |
||||||
|
await this._initInternal() |
||||||
|
await this.slashcmd.syncCommandsAsync({ |
||||||
|
syncGuilds: true, |
||||||
|
syncPermissions: true, |
||||||
|
skipGuildErrors: false, |
||||||
|
deleteCommands: true, |
||||||
|
}) |
||||||
|
await this.slashcmd.startServer() |
||||||
|
await this.server.listen(this.listenPort, this.listenAddress) |
||||||
|
} |
||||||
|
|
||||||
|
async _shutdownInternal(): Promise<void> { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
async shutdown(): Promise<void> { |
||||||
|
await this._shutdownInternal() |
||||||
|
return this.server.close() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
import {FastifyReply, FastifyRequest} from "fastify"; |
||||||
|
import {RouteGenericInterface} from "fastify/types/route"; |
||||||
|
import cryptoRandomString from "crypto-random-string"; |
||||||
|
|
||||||
|
export interface XSRFRoute extends RouteGenericInterface { |
||||||
|
Querystring: { [key in typeof XSRFParameter]: string | string[] | undefined } |
||||||
|
} |
||||||
|
|
||||||
|
export const XSRFCookie = "__Host-XSRF-Cookie"; |
||||||
|
export const XSRFParameter = "state" as const; |
||||||
|
|
||||||
|
export function generateXSRFCookie(res: FastifyReply): string { |
||||||
|
const newState = cryptoRandomString({ |
||||||
|
length: 32, |
||||||
|
type: 'url-safe' |
||||||
|
}) |
||||||
|
res.setCookie(XSRFCookie, newState, { |
||||||
|
path: "/", |
||||||
|
sameSite: "strict", |
||||||
|
httpOnly: true, |
||||||
|
signed: true, |
||||||
|
secure: true, |
||||||
|
}) |
||||||
|
return newState |
||||||
|
} |
||||||
|
|
||||||
|
export function checkAndClearXSRFCookie(req: FastifyRequest<XSRFRoute>, res: FastifyReply): boolean { |
||||||
|
const queryState = req.query[XSRFParameter] ?? null |
||||||
|
const cookieState = req.cookies[XSRFCookie] ?? null |
||||||
|
res.clearCookie(XSRFCookie) |
||||||
|
return cookieState !== null && queryState !== null && cookieState === queryState |
||||||
|
} |
@ -1,100 +1,109 @@ |
|||||||
import {SavedWebhook} from "./SavedWebhook.js"; |
import {SetupServer} from "./SetupServer.js"; |
||||||
import fastify, {FastifyInstance} from "fastify"; |
import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; |
||||||
import {DiscordWebhookHandler} from "./DiscordWebhookHandler.js"; |
|
||||||
import {getBaseUrl, RouteWithQuerystring} from "./FastifyHelpers.js"; |
|
||||||
import pug from "pug"; |
|
||||||
import {renderError} from "./PugRenderer.js"; |
import {renderError} from "./PugRenderer.js"; |
||||||
|
import {getBaseUrl} from "./FastifyHelpers.js"; |
||||||
|
import pug from "pug"; |
||||||
|
import {BaseServer, BaseServerDeps} from "./BaseServer.js"; |
||||||
|
import {PullCommand} from "./commands/game/PullCommand.js"; |
||||||
|
|
||||||
export class GameServer { |
export class GameServer extends BaseServer { |
||||||
readonly server: FastifyInstance |
readonly setupFactory: () => SetupServer |
||||||
readonly gameWebhook: SavedWebhook |
|
||||||
readonly adminWebhook: SavedWebhook |
|
||||||
readonly appId: string |
|
||||||
readonly secret: string |
|
||||||
readonly port: number |
|
||||||
constructor({appId, secret, port}: {appId: string, secret: string, port: number}) { |
|
||||||
this.server = fastify({ |
|
||||||
logger: true |
|
||||||
}) |
|
||||||
this.appId = appId |
|
||||||
this.secret = secret |
|
||||||
this.port = port |
|
||||||
this.gameWebhook = new SavedWebhook("broadcasthook.json", {logger: this.server.log}) |
|
||||||
this.adminWebhook = new SavedWebhook("logginghook.json", {logger: this.server.log}) |
|
||||||
} |
|
||||||
|
|
||||||
async initialize(): Promise<void> { |
constructor(deps: BaseServerDeps & { setupFactory: () => SetupServer }) { |
||||||
const gameHandler = new DiscordWebhookHandler({ |
super(deps) |
||||||
webhook: this.gameWebhook, |
this.setupFactory = deps.setupFactory |
||||||
templateFilename: "setupGame.pug", |
|
||||||
appId: this.appId, |
|
||||||
secret: this.secret, |
|
||||||
destinationFunc: () => { |
|
||||||
if (this.adminWebhook.isPresent) { |
|
||||||
return "clear" |
|
||||||
} else { |
|
||||||
return "adminChannel" |
|
||||||
} |
} |
||||||
}, |
|
||||||
|
async _initInternal(): Promise<void> { |
||||||
|
this.slashcmd.registerCommand(new PullCommand(this.slashcmd, this.gameWebhook)) |
||||||
|
this.server.get("/game/started", async (req, res) => { |
||||||
|
const token = generateXSRFCookie(res) |
||||||
|
res.code(200) |
||||||
|
res.type("text/html") |
||||||
|
res.send(pug.renderFile("static/pages/game/running.pug", { |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
setupModeUrl: `game/stop?token=${token}`, |
||||||
|
shutdownUrl: `shutdown?token=${token}`, |
||||||
|
})) |
||||||
}) |
}) |
||||||
const adminHandler = new DiscordWebhookHandler({ |
this.server.get("/game", async (req, res) => { |
||||||
webhook: this.adminWebhook, |
res.redirect("game/started") |
||||||
templateFilename: "setupAdmin.pug", |
|
||||||
appId: this.appId, |
|
||||||
secret: this.secret, |
|
||||||
destinationFunc: () => { |
|
||||||
if (this.gameWebhook.isPresent) { |
|
||||||
return "clear" |
|
||||||
} else { |
|
||||||
return "gameChannel" |
|
||||||
} |
|
||||||
}, |
|
||||||
}) |
}) |
||||||
this.server.get<RouteWithQuerystring>("/setup", async (req, res) => { |
this.server.get("/game/start", async (req, res) => { |
||||||
if (!this.gameWebhook.isPresent) { |
res.redirect("started") |
||||||
res.redirect(`setup/gameChannel`) |
}) |
||||||
} else if (!this.adminWebhook.isPresent) { |
this.server.get("/setup", async (req, res) => { |
||||||
res.redirect(`setup/adminChannel`) |
res.redirect("game/started") |
||||||
} else { |
}) |
||||||
res.redirect(`setup/done`) |
this.server.get("/setup/gameChannel", async (req, res) => { |
||||||
} |
res.redirect("../game/started") |
||||||
}) |
}) |
||||||
this.server.get<RouteWithQuerystring>("/setup/gameChannel", async (req, res) => { |
this.server.get("/setup/adminChannel", async (req, res) => { |
||||||
return await gameHandler.handleRequest(req, res) |
res.redirect("../game/started") |
||||||
}) |
}) |
||||||
this.server.get<RouteWithQuerystring>("/setup/adminChannel", async (req, res) => { |
this.server.get("/setup/clear", async (req, res) => { |
||||||
return await adminHandler.handleRequest(req, res) |
res.redirect("../game/started") |
||||||
|
}) |
||||||
|
this.server.get("/setup/done", async (req, res) => { |
||||||
|
res.redirect("../game/started") |
||||||
|
}) |
||||||
|
this.server.get<XSRFRoute>("/game/stop", async (req, res) => { |
||||||
|
if (!checkAndClearXSRFCookie(req, res)) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 400, |
||||||
|
error: "Token was incorrect or not set.", |
||||||
|
context: "stopping the game", |
||||||
|
buttonText: "Return to Game", |
||||||
|
buttonUrl: "game/started" |
||||||
}) |
}) |
||||||
this.server.get<RouteWithQuerystring>("/setup/clear", async (req, res) => { |
} |
||||||
|
res.code(200) |
||||||
|
res.type("text/html") |
||||||
|
res.send(pug.renderFile("static/pages/game/stop.pug", { |
||||||
|
startedUrl: "setup" |
||||||
|
})) |
||||||
|
setImmediate(async () => { |
||||||
|
this.server.log.info("Shutting down the game server and switching to the setup server.") |
||||||
try { |
try { |
||||||
await Promise.all([this.gameWebhook, this.adminWebhook] |
await this.server.close() |
||||||
.filter((item) => item.isPresent) |
|
||||||
.map((item) => item.clearHook())) |
|
||||||
} catch (e) { |
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to shut down the game server") |
||||||
|
} |
||||||
|
try { |
||||||
|
await this.setupFactory().initialize() |
||||||
|
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to start up the setup server") |
||||||
|
} |
||||||
|
this.server.log.info("Successfully switched from the game server to the setup server.") |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
this.server.get<XSRFRoute>("/shutdown", async (req, res) => { |
||||||
|
if (!checkAndClearXSRFCookie(req, res)) { |
||||||
return renderError({ |
return renderError({ |
||||||
baseUrl: getBaseUrl(req), |
baseUrl: getBaseUrl(req), |
||||||
res, |
res, |
||||||
error: e, |
code: 400, |
||||||
context: "clearing the broadcast webhook" |
error: "Token was incorrect or not set.", |
||||||
|
context: "shutting down the game", |
||||||
|
buttonText: "Return to Game", |
||||||
|
buttonUrl: "game/started" |
||||||
}) |
}) |
||||||
} |
} |
||||||
res.redirect(".") |
|
||||||
}) |
|
||||||
this.server.get<RouteWithQuerystring>("/setup/done", async (req, res) => { |
|
||||||
if (!this.gameWebhook.isPresent) { |
|
||||||
res.redirect("gameChannel") |
|
||||||
} else if (!this.adminWebhook.isPresent) { |
|
||||||
res.redirect("adminChannel") |
|
||||||
} else { |
|
||||||
res.code(200) |
res.code(200) |
||||||
res.type("text/html") |
res.type("text/html") |
||||||
res.send(pug.renderFile("static/pages/setupDone.pug", { |
res.send(pug.renderFile("static/pages/shutdown.pug")) |
||||||
baseUrl: getBaseUrl(req), |
setImmediate(async () => { |
||||||
clearUrl: "setup/clear", |
this.server.log.info("Shutting down the game server.") |
||||||
gameSetupUrl: "setup/gameChannel", |
try { |
||||||
adminSetupUrl: "setup/adminChannel"})) |
await this.server.close() |
||||||
|
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to shut down the game server") |
||||||
} |
} |
||||||
|
this.server.log.info("Shut down. Good night...") |
||||||
|
}) |
||||||
}) |
}) |
||||||
await Promise.all([this.gameWebhook.load(), this.adminWebhook.load(), this.server.listen(this.port, "127.0.0.1")]) |
|
||||||
} |
} |
||||||
} |
} |
@ -0,0 +1,192 @@ |
|||||||
|
import {DiscordWebhookHandler, OAuthRoute} from "./DiscordWebhookHandler.js"; |
||||||
|
import {getBaseUrl} from "./FastifyHelpers.js"; |
||||||
|
import pug from "pug"; |
||||||
|
import {renderError} from "./PugRenderer.js"; |
||||||
|
import {GameServer} from "./GameServer.js"; |
||||||
|
import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; |
||||||
|
import {SetupCommand, setupComponentInteractionHandler} from "./commands/SetupCommand.js"; |
||||||
|
import {BaseServer, BaseServerDeps} from "./BaseServer.js"; |
||||||
|
|
||||||
|
export class SetupServer extends BaseServer { |
||||||
|
readonly gameFactory: () => GameServer |
||||||
|
|
||||||
|
constructor(deps: BaseServerDeps & { gameFactory: () => GameServer }) { |
||||||
|
super(deps) |
||||||
|
this.gameFactory = deps.gameFactory |
||||||
|
} |
||||||
|
|
||||||
|
async _initInternal(): Promise<void> { |
||||||
|
this.slashcmd.registerCommand(new SetupCommand(this.slashcmd)) |
||||||
|
this.slashcmd.on("componentInteraction", setupComponentInteractionHandler) |
||||||
|
const gameHandler = new DiscordWebhookHandler({ |
||||||
|
webhook: this.gameWebhook, |
||||||
|
templateFilename: "setup/game.pug", |
||||||
|
appId: this.appId, |
||||||
|
secret: this.clientSecret, |
||||||
|
destinationFunc: () => { |
||||||
|
if (this.adminWebhook.isPresent) { |
||||||
|
return "clear" |
||||||
|
} else { |
||||||
|
return "adminChannel" |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
const adminHandler = new DiscordWebhookHandler({ |
||||||
|
webhook: this.adminWebhook, |
||||||
|
templateFilename: "setup/admin.pug", |
||||||
|
appId: this.appId, |
||||||
|
secret: this.clientSecret, |
||||||
|
destinationFunc: () => { |
||||||
|
if (this.gameWebhook.isPresent) { |
||||||
|
return "clear" |
||||||
|
} else { |
||||||
|
return "gameChannel" |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
this.server.get("/setup", async (req, res) => { |
||||||
|
if (!this.gameWebhook.isPresent) { |
||||||
|
res.redirect(`setup/gameChannel`) |
||||||
|
} else if (!this.adminWebhook.isPresent) { |
||||||
|
res.redirect(`setup/adminChannel`) |
||||||
|
} else { |
||||||
|
res.redirect(`setup/done`) |
||||||
|
} |
||||||
|
}) |
||||||
|
this.server.get("/setup/start", async (req, res) => { |
||||||
|
res.redirect(".") |
||||||
|
}) |
||||||
|
this.server.get("/game", async (req, res) => { |
||||||
|
res.redirect("setup") |
||||||
|
}) |
||||||
|
this.server.get("/game/started", async (req, res) => { |
||||||
|
res.redirect("../setup") |
||||||
|
}) |
||||||
|
this.server.get("/game/stop", async (req, res) => { |
||||||
|
res.redirect("../setup") |
||||||
|
}) |
||||||
|
this.server.get<XSRFRoute>("/game/start", async (req, res) => { |
||||||
|
if (!checkAndClearXSRFCookie(req, res)) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 400, |
||||||
|
error: "Token was incorrect or not set.", |
||||||
|
context: "starting the game", |
||||||
|
buttonText: "Return to Setup", |
||||||
|
buttonUrl: "setup" |
||||||
|
}) |
||||||
|
} |
||||||
|
if (!this.gameWebhook.isPresent || !this.adminWebhook.isPresent) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 409, |
||||||
|
error: "You can't start the game while one of the channels is not set!", |
||||||
|
context: "starting the game", |
||||||
|
buttonText: "Finish Setup", |
||||||
|
buttonUrl: "setup" |
||||||
|
}) |
||||||
|
} |
||||||
|
res.code(200) |
||||||
|
res.type("text/html") |
||||||
|
res.send(pug.renderFile("static/pages/setup/start.pug", { |
||||||
|
startedUrl: "game/start" |
||||||
|
})) |
||||||
|
setImmediate(async () => { |
||||||
|
this.server.log.info("Shutting down the setup server and switching to the game server.") |
||||||
|
try { |
||||||
|
await this.server.close() |
||||||
|
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to shut down the setup server") |
||||||
|
} |
||||||
|
try { |
||||||
|
await this.gameFactory().initialize() |
||||||
|
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to start up the game server") |
||||||
|
} |
||||||
|
this.server.log.info("Successfully switched from the setup server to the game server.") |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
this.server.get<OAuthRoute>("/setup/gameChannel", async (req, res) => { |
||||||
|
return await gameHandler.handleRequest(req, res) |
||||||
|
}) |
||||||
|
this.server.get<OAuthRoute>("/setup/adminChannel", async (req, res) => { |
||||||
|
return await adminHandler.handleRequest(req, res) |
||||||
|
}) |
||||||
|
this.server.get<XSRFRoute>("/setup/clear", async (req, res) => { |
||||||
|
if (!checkAndClearXSRFCookie(req, res)) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 400, |
||||||
|
error: "Token was incorrect or not set.", |
||||||
|
context: "clearing the channel setup", |
||||||
|
buttonText: "Return to Setup", |
||||||
|
buttonUrl: "setup" |
||||||
|
}) |
||||||
|
} |
||||||
|
try { |
||||||
|
await Promise.all([this.gameWebhook, this.adminWebhook] |
||||||
|
.filter((item) => item.isPresent) |
||||||
|
.map((item) => item.clearHook())) |
||||||
|
} catch (e) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 500, |
||||||
|
error: e, |
||||||
|
context: "clearing and deleting the webhooks", |
||||||
|
buttonUrl: "setup/clear", |
||||||
|
buttonText: "Try Again" |
||||||
|
}) |
||||||
|
} |
||||||
|
res.redirect(".") |
||||||
|
}) |
||||||
|
this.server.get("/setup/done", async (req, res) => { |
||||||
|
if (!this.gameWebhook.isPresent) { |
||||||
|
res.redirect("gameChannel") |
||||||
|
} else if (!this.adminWebhook.isPresent) { |
||||||
|
res.redirect("adminChannel") |
||||||
|
} else { |
||||||
|
const token = generateXSRFCookie(res) |
||||||
|
res.code(200) |
||||||
|
res.type("text/html") |
||||||
|
res.send(pug.renderFile("static/pages/setup/done.pug", { |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
gameModeUrl: `game/start?token=${token}`, |
||||||
|
clearUrl: `setup/clear?token=${token}`, |
||||||
|
gameSetupUrl: `setup/gameChannel`, |
||||||
|
adminSetupUrl: `setup/adminChannel`, |
||||||
|
shutdownUrl: `shutdown?token=${token}` |
||||||
|
})) |
||||||
|
} |
||||||
|
}) |
||||||
|
this.server.get<XSRFRoute>("/shutdown", async (req, res) => { |
||||||
|
if (!checkAndClearXSRFCookie(req, res)) { |
||||||
|
return renderError({ |
||||||
|
baseUrl: getBaseUrl(req), |
||||||
|
res, |
||||||
|
code: 400, |
||||||
|
error: "Token was incorrect or not set.", |
||||||
|
context: "shutting the server down", |
||||||
|
buttonText: "Return to Setup", |
||||||
|
buttonUrl: "setup" |
||||||
|
}) |
||||||
|
} |
||||||
|
res.code(200) |
||||||
|
res.type("text/html") |
||||||
|
res.send(pug.renderFile("static/pages/shutdown.pug")) |
||||||
|
setImmediate(async () => { |
||||||
|
this.server.log.info("Shutting down the setup server.") |
||||||
|
try { |
||||||
|
await this.server.close() |
||||||
|
} catch (e) { |
||||||
|
this.server.log.error(e, "Failed to shut down the setup server") |
||||||
|
} |
||||||
|
this.server.log.info("Shut down. Good night...") |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import {AutocompleteContext, CommandContext, ComponentContext, Message, SlashCommand, SlashCreator} from "slash-create"; |
||||||
|
|
||||||
|
export class SetupCommand extends SlashCommand { |
||||||
|
constructor(creator: SlashCreator) { |
||||||
|
super(creator, { |
||||||
|
name: "setup-commands-disabled", |
||||||
|
unknown: true |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
autocomplete(ctx: AutocompleteContext): Promise<any> { |
||||||
|
return ctx.sendResults([]) |
||||||
|
} |
||||||
|
|
||||||
|
run(ctx: CommandContext): Promise<any> { |
||||||
|
return ctx.send({ |
||||||
|
ephemeral: true, |
||||||
|
content: "The server is currently in setup mode! You can't run commands right now..." |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function setupComponentInteractionHandler(ctx: ComponentContext): Promise<boolean | Message> { |
||||||
|
return ctx.send({ |
||||||
|
ephemeral: true, |
||||||
|
content: "The server is currently in setup mode! You can't interact with its messages right now..." |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
import {CommandContext, CommandOptionType, SlashCommand, SlashCreator} from "slash-create"; |
||||||
|
import {SavedWebhook} from "../../SavedWebhook.js"; |
||||||
|
import {Chance} from "chance"; |
||||||
|
|
||||||
|
const rand = Chance() |
||||||
|
|
||||||
|
export class PullCommand extends SlashCommand { |
||||||
|
readonly gameWebhook: SavedWebhook |
||||||
|
|
||||||
|
constructor(creator: SlashCreator, gameWebhook: SavedWebhook) { |
||||||
|
super(creator, { |
||||||
|
name: "pull", |
||||||
|
guildIDs: gameWebhook.state?.guild_id, |
||||||
|
description: "Pulls one or more new heroines from the ether.", |
||||||
|
options: [ |
||||||
|
{ |
||||||
|
name: "count", |
||||||
|
description: "The number of heroines to pull.", |
||||||
|
required: false, |
||||||
|
max_value: 10, |
||||||
|
min_value: 1, |
||||||
|
type: CommandOptionType.NUMBER, |
||||||
|
} |
||||||
|
] |
||||||
|
}); |
||||||
|
this.gameWebhook = gameWebhook |
||||||
|
} |
||||||
|
|
||||||
|
run(ctx: CommandContext): Promise<any> { |
||||||
|
if (ctx.guildID !== this.gameWebhook.state?.guild_id) { |
||||||
|
return ctx.send({ |
||||||
|
content: "Sorry, you can't do that in this guild.", |
||||||
|
ephemeral: true, |
||||||
|
}) |
||||||
|
} |
||||||
|
if (ctx.channelID !== this.gameWebhook.state?.channel_id) { |
||||||
|
return ctx.send({ |
||||||
|
content: `Sorry, you can't do that here. You have to do it in <#${this.gameWebhook.state?.channel_id}>.`, |
||||||
|
ephemeral: true, |
||||||
|
}) |
||||||
|
} |
||||||
|
const count: number = ctx.options.count ?? 1 |
||||||
|
const results: string[] = [] |
||||||
|
for (let x = 0; x < count; x += 1) { |
||||||
|
results.push(rand.weighted(["**Nicole**: D tier Predator Podcaster", |
||||||
|
"**Herja**: C tier Viking Warrior", |
||||||
|
"**Sharla**: B tier Skark Girl", |
||||||
|
"**Melpomene**: A tier Muse of Tragedy", |
||||||
|
"**Lady Bootstrap**: S tier Time Traveler"], [20, 15, 10, 5, 1])) |
||||||
|
} |
||||||
|
return ctx.send({ |
||||||
|
content: `_${ctx.user.mention}_, you pulled...\n \\* ${results.join("\n \\* ")}`, |
||||||
|
ephemeral: false, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -1,10 +0,0 @@ |
|||||||
extends ../template/template |
|
||||||
|
|
||||||
block title |
|
||||||
| Setup Error |
|
||||||
block content |
|
||||||
| Something went wrong while #{context}. What did you do? |
|
||||||
br |
|
||||||
| The error was: #{error} |
|
||||||
block link |
|
||||||
a(href="setup") Return to Setup Page |
|
@ -0,0 +1,11 @@ |
|||||||
|
extends ../../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Game Running |
||||||
|
block content |
||||||
|
| The game is up and running. |
||||||
|
br |
||||||
|
| To make changes to its configuration setup, click Return to Setup. |
||||||
|
block link |
||||||
|
a(href=setupModeUrl, rel="nofollow") Return to Setup |
||||||
|
a(href=shutdownUrl, rel="nofollow") Shut Down Server |
@ -0,0 +1,13 @@ |
|||||||
|
extends ../../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Game Stopping |
||||||
|
block content |
||||||
|
| You got it! The game is being stopped and returned to the setup phase now... |
||||||
|
| If you aren't automatically redirected after a few seconds, click the button below. |
||||||
|
script. |
||||||
|
setTimeout(() => { |
||||||
|
document.getElementById("stopButton").click() |
||||||
|
}, 3000) |
||||||
|
block link |
||||||
|
a(href=stoppedUrl, id="stopButton", rel="nofollow") Verify Game Stopped |
@ -1,4 +1,4 @@ |
|||||||
extends ../template/template |
extends ../../template/template |
||||||
|
|
||||||
block title |
block title |
||||||
| Admin Setup |
| Admin Setup |
@ -0,0 +1,14 @@ |
|||||||
|
extends ../../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Setup Complete! |
||||||
|
block content |
||||||
|
| You're all done! Time to just play the game. |
||||||
|
br |
||||||
|
| If you'd like to reconfigure the game's channels, click one of the buttons below. |
||||||
|
block link |
||||||
|
a(href=gameModeUrl, rel="nofollow") Start the Game! |
||||||
|
a(href=gameSetupUrl, rel="nofollow") Reconfigure Game Channel |
||||||
|
a(href=adminSetupUrl, rel="nofollow") Reconfigure Admin Channel |
||||||
|
a(href=clearUrl, rel="nofollow") Clear All Channels |
||||||
|
a(href=shutdownUrl, rel="nofollow") Shut Down Server |
@ -0,0 +1,13 @@ |
|||||||
|
extends ../../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Setup Error |
||||||
|
block content |
||||||
|
| Something went wrong while #{context}. What did you do? |
||||||
|
br |
||||||
|
if errorUrl |
||||||
|
| The error was: #[a(href=errorUrl) #{error}] |
||||||
|
else |
||||||
|
| The error was: #{error} |
||||||
|
block link |
||||||
|
a(href=buttonUrl, rel="nofollow") #{buttonText} |
@ -0,0 +1,13 @@ |
|||||||
|
extends ../../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Game Starting |
||||||
|
block content |
||||||
|
| You got it! The game is being started up now... |
||||||
|
| If you aren't automatically redirected after a few seconds, click the button below. |
||||||
|
script. |
||||||
|
setTimeout(() => { |
||||||
|
document.getElementById("startButton").click() |
||||||
|
}, 3000) |
||||||
|
block link |
||||||
|
a(href=startedUrl, id="startButton", rel="nofollow") Verify Game Started |
@ -1,12 +0,0 @@ |
|||||||
extends ../template/template |
|
||||||
|
|
||||||
block title |
|
||||||
| Setup Complete! |
|
||||||
block content |
|
||||||
| You're all done! Time to just play the game. |
|
||||||
br |
|
||||||
| If you'd like to reconfigure the game's channels, click one of the buttons below. |
|
||||||
block link |
|
||||||
a(href=gameSetupUrl) Reconfigure Game Channel |
|
||||||
a(href=adminSetupUrl) Reconfigure Admin Channel |
|
||||||
a(href=clearUrl) Clear All Channels |
|
@ -0,0 +1,6 @@ |
|||||||
|
extends ../template/template |
||||||
|
|
||||||
|
block title |
||||||
|
| Server Shutdown |
||||||
|
block content |
||||||
|
| You got it. The server has been shut down. Good night... |
Loading…
Reference in new issue