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

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"))
}
}
}