Scenario generator for vore roleplay and story ideas.
https://scenario-generator.deliciousreya.net/responses
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.
376 lines
11 KiB
376 lines
11 KiB
import { CommandContext, CommandOptionType, ComponentContext, SlashCommand, type SlashCreator } from 'slash-create/web';
|
|
import {
|
|
calculateUnlockedValues,
|
|
DELETE_ID,
|
|
DONE_ID,
|
|
generateErrorMessageFor,
|
|
generateFieldFor,
|
|
generateMessageFor,
|
|
generateValuesFor,
|
|
getEmbedFrom,
|
|
loadEmbed,
|
|
populateLocksFor,
|
|
REROLL_ID,
|
|
RollTableNames,
|
|
SELECT_ID,
|
|
selectUnlockedFrom
|
|
} from './generated.js';
|
|
import { type DbAccess, DeleteResult, UpdateResult } from './dbAccess.js';
|
|
import { isTable, RollTableOrder, ValueAccess } from './rolltable.js';
|
|
import { getTimestamp, isSnowflake, type Snowflake } from 'discord-snowflake';
|
|
|
|
export class ResponseCommand extends SlashCommand {
|
|
private readonly db: DbAccess
|
|
private readonly baseUrl: string;
|
|
|
|
constructor(creator: SlashCreator, db: DbAccess, baseUrl: string, forGuilds?: Snowflake|Snowflake[]) {
|
|
super(creator, {
|
|
name: "response",
|
|
description: "Modifies the responses available in the generator.",
|
|
nsfw: false,
|
|
guildIDs: forGuilds,
|
|
dmPermission: true,
|
|
options: [
|
|
{
|
|
type: CommandOptionType.SUB_COMMAND,
|
|
name: "add",
|
|
description: "Adds a new response to the generator.",
|
|
options: [
|
|
{
|
|
type: CommandOptionType.INTEGER,
|
|
name: "table",
|
|
description: "The table to insert the response into.",
|
|
choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})),
|
|
required: true,
|
|
},
|
|
{
|
|
type: CommandOptionType.STRING,
|
|
name: "text",
|
|
description: "The text to use as the response.",
|
|
required: true,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
type: CommandOptionType.SUB_COMMAND,
|
|
name: "list",
|
|
description: "Lists responses that will appear in /generate in the current context."
|
|
},
|
|
{
|
|
type: CommandOptionType.SUB_COMMAND,
|
|
name: "edit",
|
|
description: "Modifies a response that was previously created.",
|
|
options: [
|
|
{
|
|
type: CommandOptionType.INTEGER,
|
|
name: "table",
|
|
description: "The table to update the response from.",
|
|
choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})),
|
|
required: true,
|
|
},
|
|
{
|
|
type: CommandOptionType.STRING,
|
|
name: "old_text",
|
|
description: "The text of the response to edit.",
|
|
required: true,
|
|
},
|
|
{
|
|
type: CommandOptionType.STRING,
|
|
name: "new_text",
|
|
description: "The text to replace the response with.",
|
|
required: true,
|
|
}
|
|
]
|
|
},
|
|
{
|
|
type: CommandOptionType.SUB_COMMAND,
|
|
name: "delete",
|
|
description: "Deletes a response that was previously created.",
|
|
options: [
|
|
{
|
|
type: CommandOptionType.INTEGER,
|
|
name: "table",
|
|
description: "The table to delete the response from.",
|
|
choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})),
|
|
required: true,
|
|
},
|
|
{
|
|
type: CommandOptionType.STRING,
|
|
name: "text",
|
|
description: "The text of the response to delete.",
|
|
required: true,
|
|
},
|
|
]
|
|
},
|
|
]
|
|
});
|
|
this.baseUrl = baseUrl
|
|
this.db = db
|
|
}
|
|
|
|
async run(ctx: CommandContext): Promise<void> {
|
|
switch (ctx.subcommands[0]) {
|
|
case "add":
|
|
try {
|
|
await this.onAdd(ctx)
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "add a new response"))
|
|
}
|
|
break
|
|
case "list":
|
|
try {
|
|
await this.onList(ctx)
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "get the list URL"))
|
|
}
|
|
break
|
|
case "edit":
|
|
try {
|
|
await this.onEdit(ctx)
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "edit a response"))
|
|
}
|
|
break
|
|
case "delete":
|
|
try {
|
|
await this.onDelete(ctx)
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "delete a response"))
|
|
}
|
|
break
|
|
default:
|
|
await ctx.send(generateErrorMessageFor(Error("I don't know what command you want"), "manage responses"))
|
|
break
|
|
}
|
|
}
|
|
|
|
private async onAdd(ctx: CommandContext): Promise<void> {
|
|
const guildId = ctx.guildID ?? null
|
|
const userId = ctx.user.id
|
|
const id = ctx.interactionID
|
|
if (!isSnowflake(id)) {
|
|
throw Error("the snowflake wasn't a snowflake")
|
|
}
|
|
const timestamp = getTimestamp(id)
|
|
const table = ctx.options['add']['table']
|
|
if (!isTable(table)) {
|
|
throw Error(`there's no table number ${table}`)
|
|
}
|
|
const text = ctx.options['add']['text']
|
|
const { timestamp: insertedTimestamp, access, inserted } = await this.db.putResponse(timestamp, table, text, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server)
|
|
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `${inserted ? 'Your new' : 'An existing'}${access === ValueAccess.Global ? " global" : ""} response`,
|
|
fields: [generateFieldFor(table, text)],
|
|
timestamp: new Date(insertedTimestamp),
|
|
}],
|
|
ephemeral: !inserted,
|
|
})
|
|
}
|
|
|
|
private async onList(ctx: CommandContext) {
|
|
if (ctx.guildID) {
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `Response list for this server`,
|
|
description: "Shows all global and server-local responses.",
|
|
url: `${this.baseUrl}/responses?server=${ctx.guildID}`,
|
|
}]
|
|
})
|
|
} else {
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `Response list for DMs`,
|
|
description: "It's not supported right now, so please just hang tight."
|
|
}]
|
|
})
|
|
}
|
|
}
|
|
|
|
private async onEdit(ctx: CommandContext): Promise<void> {
|
|
const guildId = ctx.guildID ?? null
|
|
const userId = ctx.user.id
|
|
const id = ctx.interactionID
|
|
if (!isSnowflake(id)) {
|
|
throw Error("the snowflake wasn't a snowflake")
|
|
}
|
|
const timestamp = getTimestamp(id)
|
|
const table = ctx.options['edit']['table']
|
|
if (!isTable(table)) {
|
|
throw Error(`there's no table number ${table}`)
|
|
}
|
|
const oldText = ctx.options['edit']['old_text']
|
|
const newText = ctx.options['edit']['new_text']
|
|
const result = await this.db.updateResponse(timestamp, table, oldText, newText, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server)
|
|
switch (result.result) {
|
|
case UpdateResult.Updated:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `Your updated response`,
|
|
fields: [generateFieldFor(table, oldText), generateFieldFor(table, newText)],
|
|
timestamp: new Date(timestamp).toISOString()
|
|
}],
|
|
})
|
|
break
|
|
case UpdateResult.NewConflict:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `An existing${result.access === ValueAccess.Global ? " global" : ""} response`,
|
|
fields: [generateFieldFor(table, newText)],
|
|
timestamp: new Date(result.timestamp).toISOString(),
|
|
}],
|
|
ephemeral: true,
|
|
})
|
|
break
|
|
case UpdateResult.NoOldText:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `A nonexistent response`,
|
|
fields: [generateFieldFor(table, oldText)],
|
|
}],
|
|
ephemeral: true,
|
|
})
|
|
break
|
|
case UpdateResult.OldGlobal:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: `An uneditable global response`,
|
|
fields: [generateFieldFor(table, oldText)],
|
|
}],
|
|
ephemeral: true,
|
|
})
|
|
break
|
|
}
|
|
}
|
|
|
|
private async onDelete(ctx: CommandContext): Promise<void> {
|
|
const guildId = ctx.guildID ?? null
|
|
const userId = ctx.user.id
|
|
const id = ctx.interactionID
|
|
if (!isSnowflake(id)) {
|
|
throw Error("the snowflake wasn't a snowflake")
|
|
}
|
|
const timestamp = getTimestamp(id)
|
|
const table = ctx.options['delete']['table']
|
|
if (!isTable(table)) {
|
|
throw Error(`there's no table number ${table}`)
|
|
}
|
|
const text = ctx.options['delete']['text']
|
|
const result = await this.db.deleteResponse(table, text, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server)
|
|
switch (result.result) {
|
|
case DeleteResult.Deleted:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: 'Your deleted response',
|
|
fields: [generateFieldFor(table, text)],
|
|
timestamp: new Date(result.timestamp).toISOString(),
|
|
}]
|
|
})
|
|
break
|
|
case DeleteResult.OldGlobal:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: 'An undeletable global response',
|
|
fields: [generateFieldFor(table, text)],
|
|
timestamp: new Date(result.timestamp).toISOString(),
|
|
}],
|
|
ephemeral: true
|
|
})
|
|
break
|
|
case DeleteResult.NoOldText:
|
|
await ctx.send({
|
|
embeds: [{
|
|
title: 'A nonexistent response',
|
|
fields: [generateFieldFor(table, text)],
|
|
}],
|
|
ephemeral: true
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
export class GenerateCommand extends SlashCommand {
|
|
private readonly db: DbAccess
|
|
|
|
constructor(creator: SlashCreator, db: DbAccess, forGuilds?: Snowflake|Snowflake[]) {
|
|
super(creator, {
|
|
name: "generate",
|
|
description: "Generates a new scenario to play with and sends it to the current channel.",
|
|
nsfw: false,
|
|
dmPermission: true,
|
|
guildIDs: forGuilds,
|
|
throttling: {
|
|
duration: 5,
|
|
usages: 1,
|
|
}
|
|
});
|
|
this.db = db
|
|
if (!forGuilds) {
|
|
creator.registerGlobalComponent(DONE_ID, this.onDone.bind(this))
|
|
creator.registerGlobalComponent(REROLL_ID, this.onReroll.bind(this))
|
|
creator.registerGlobalComponent(SELECT_ID, this.onSelect.bind(this))
|
|
creator.registerGlobalComponent(DELETE_ID, this.onDelete.bind(this))
|
|
}
|
|
}
|
|
|
|
async run(ctx: CommandContext): Promise<void> {
|
|
try {
|
|
const tables = calculateUnlockedValues()
|
|
const responses = await (ctx.guildID
|
|
? this.db.getResponsesInServer(ctx.guildID)
|
|
: this.db.getResponsesInDMWith(ctx.user.id))
|
|
const values = generateValuesFor(tables, responses)
|
|
const locks = populateLocksFor(values)
|
|
await ctx.send(generateMessageFor(values, locks))
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "generate a scenario for you"))
|
|
}
|
|
}
|
|
|
|
async onSelect(ctx: ComponentContext): Promise<void> {
|
|
try {
|
|
const oldEmbed = getEmbedFrom(ctx.message)
|
|
const {values, locked: oldLocks} = loadEmbed(oldEmbed)
|
|
const newLocks = selectUnlockedFrom(ctx.values, oldLocks)
|
|
await ctx.editParent(generateMessageFor(values, newLocks))
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "change the selected components"))
|
|
}
|
|
}
|
|
|
|
async onDone(ctx: ComponentContext): Promise<void> {
|
|
try {
|
|
const oldEmbed = getEmbedFrom(ctx.message)
|
|
const { values } = loadEmbed(oldEmbed)
|
|
await ctx.editParent(generateMessageFor(values, undefined))
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "finish this scenario"))
|
|
}
|
|
}
|
|
|
|
async onReroll(ctx: ComponentContext): Promise<void> {
|
|
try {
|
|
const oldEmbed = getEmbedFrom(ctx.message)
|
|
const { values: oldValues, locked: locks } = loadEmbed(oldEmbed)
|
|
const selected = calculateUnlockedValues(oldValues, locks)
|
|
const responses = await (ctx.guildID
|
|
? this.db.getResponsesInServer(ctx.guildID)
|
|
: this.db.getResponsesInDMWith(ctx.user.id))
|
|
const newValues = generateValuesFor(selected, responses, oldValues)
|
|
await ctx.editParent(generateMessageFor(newValues, locks))
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "reroll this scenario"))
|
|
throw e
|
|
}
|
|
}
|
|
|
|
async onDelete(ctx: ComponentContext): Promise<void> {
|
|
try {
|
|
await ctx.delete(ctx.messageID)
|
|
} catch (e) {
|
|
await ctx.send(generateErrorMessageFor(e, "delete this scenario"))
|
|
}
|
|
}
|
|
}
|
|
|