parent
16c66b5460
commit
df17f6f5c3
@ -0,0 +1,13 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project version="4"> |
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> |
||||||
|
<data-source source="LOCAL" name="D1 JDBC Data Source" uuid="0d7460e5-a5b0-427c-ba12-108e8ff70b34"> |
||||||
|
<driver-ref>java.sql.Driver</driver-ref> |
||||||
|
<synchronize>true</synchronize> |
||||||
|
<configured-by-url>true</configured-by-url> |
||||||
|
<jdbc-driver>org.isaacmcfadyen.D1Driver</jdbc-driver> |
||||||
|
<jdbc-url>jdbc:d1://d09d3c74-c75f-4418-8f1b-2fe7f21637e6</jdbc-url> |
||||||
|
<working-dir>$ProjectFileDir$</working-dir> |
||||||
|
</data-source> |
||||||
|
</component> |
||||||
|
</project> |
@ -0,0 +1,55 @@ |
|||||||
|
# Vore Scenario Generator |
||||||
|
|
||||||
|
![Example scenario, reading: |
||||||
|
The action takes place at a sports event. |
||||||
|
The encounter is themed around voreception. |
||||||
|
The action begins when a threat is made. |
||||||
|
Things are more difficult because someone is not prepared |
||||||
|
Partway through, unexpectedly, the prey nearly escapes |
||||||
|
The vore scene is focused on the aftermath |
||||||
|
The word of the day is Millionaire](example.png) |
||||||
|
|
||||||
|
It does exactly what it says on the tin: it generates random vore scenarios. For you! |
||||||
|
[The list of default responses are here if you want to check it in advance.](https://scenario-generator.deliciousreya.net/responses) |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
### Installation |
||||||
|
|
||||||
|
* Visit [this Discord OAuth link](https://discord.com/api/oauth2/authorize?client_id=1192326191189864458&permissions=0&scope=applications.commands) |
||||||
|
and give permission to use application commands in your server. Share it with the admin of your server if you don't |
||||||
|
have permission yourself! |
||||||
|
* Don't worry - not only does this bot not do anything nefarious with your messages, but it actually can't read them at |
||||||
|
all. It only reads the commands you give it and its own messages! If you're concerned, though, please go through the |
||||||
|
code. Kink stuff is very personal and private! Use caution when running random horny software! |
||||||
|
|
||||||
|
### Generation |
||||||
|
|
||||||
|
* Run the `/generate` command to generate a random scenario. |
||||||
|
* Don't like what you got? Pick the components you don't like from the select box, |
||||||
|
then click "Reroll Selected" to generate fresh ones. |
||||||
|
* Think the scenario is beyond saving? Click "Trash it." to delete the message. |
||||||
|
* Satisfied with the results? Click "Looks good!" to remove the reroll commands. |
||||||
|
|
||||||
|
### Customization |
||||||
|
|
||||||
|
Note that changes to custom responses will be sent to your current channel and will be visible to the admin (that's me) |
||||||
|
and everyone in the server, as well as anyone who knows or can guess your server ID. Assume your responses are not |
||||||
|
private. |
||||||
|
|
||||||
|
* Run `/response add [table] [text]` to add a new custom response. Use the table listing in the Discord command. |
||||||
|
* Run `/response delete [table] [text]` to remove a custom response. Give the response text exactly! |
||||||
|
* Run `/response edit [table] [old text] [new text]` to modify a custom response. Give the old response text exactly! |
||||||
|
* Run `/response list` to get a link to a list of all responses that will be given in the current server, including both |
||||||
|
default and custom responses. |
||||||
|
|
||||||
|
## Credits |
||||||
|
|
||||||
|
* Icon source: [obsid1an on DeviantArt](https://www.deviantart.com/obsid1an/art/Slot-Machine-Game-Icon-341475642) |
||||||
|
* Writing for default responses by [Ssublissive](https://aryion.com/g4/gallery/Ssublissive), with additional writing by |
||||||
|
[DeliciousReya](https://aryion.com/g4/gallery/DeliciousReya), [Seina](https://aryion.com/g4/user/RediQ) and 1 other. |
||||||
|
* Development by [DeliciousReya](https://aryion.com/g4/gallery/DeliciousReya), using |
||||||
|
[slash-create](https://slash-create.js.org/#/) and |
||||||
|
[discord-snowflake](https://github.com/ianmitchell/interaction-kit/tree/main/packages/discord-snowflake). |
||||||
|
* Hosted on [CloudFlare Workers](https://developers.cloudflare.com/workers/) |
||||||
|
with [D1](https://developers.cloudflare.com/d1/). |
After Width: | Height: | Size: 53 KiB |
@ -0,0 +1,14 @@ |
|||||||
|
-- Migration number: 0000 2024-01-04T08:37:46.641Z |
||||||
|
CREATE TABLE IF NOT EXISTS responses ( |
||||||
|
id INTEGER PRIMARY KEY ASC NOT NULL, |
||||||
|
tableId INTEGER NOT NULL CHECK (tableId IN (0, 1, 2, 3, 4, 5, 6)), |
||||||
|
text TEXT NOT NULL, |
||||||
|
timestamp INTEGER NOT NULL, |
||||||
|
userSnowflake TEXT NOT NULL, |
||||||
|
serverSnowflake TEXT NULL, |
||||||
|
access INTEGER NOT NULL CHECK (access IN (0, 1, 2)) |
||||||
|
) STRICT; |
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS responses_unique_global ON responses (tableId, text) WHERE access = 0; |
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS responses_unique_by_server ON responses (serverSnowflake, tableId, text) WHERE access = 1; |
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS responses_unique_by_dm ON responses (userSnowflake, tableId, text) WHERE access = 2; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,376 @@ |
|||||||
|
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")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,180 @@ |
|||||||
|
import { type RollableTables, RollTable, RollTableOrder, ValueAccess } from './rolltable.js'; |
||||||
|
|
||||||
|
interface DbResponse { |
||||||
|
tableId: RollTable, |
||||||
|
text: string, |
||||||
|
} |
||||||
|
|
||||||
|
export enum UpdateResult { |
||||||
|
Updated = 0, |
||||||
|
NewConflict = 1, |
||||||
|
OldGlobal = 2, |
||||||
|
NoOldText = 3, |
||||||
|
} |
||||||
|
|
||||||
|
export enum DeleteResult { |
||||||
|
Deleted = 0, |
||||||
|
OldGlobal = 2, |
||||||
|
NoOldText = 3, |
||||||
|
} |
||||||
|
|
||||||
|
function buildRollableTables(responses: DbResponse[]): RollableTables { |
||||||
|
const out: {[key in RollTable]?: string[]} = {} |
||||||
|
for (const table of RollTableOrder) { |
||||||
|
out[table] = [] |
||||||
|
} |
||||||
|
for (const { tableId, text } of responses) { |
||||||
|
out[tableId]?.push(text) |
||||||
|
} |
||||||
|
return out as RollableTables |
||||||
|
} |
||||||
|
|
||||||
|
export class DbAccess { |
||||||
|
private readonly getResponsesInServerQuery: D1PreparedStatement |
||||||
|
private readonly getResponsesInDMQuery: D1PreparedStatement; |
||||||
|
private readonly putResponseQuery: D1PreparedStatement; |
||||||
|
private readonly checkResponseAlreadyExistsQuery: D1PreparedStatement; |
||||||
|
private readonly getResponsesGlobal: D1PreparedStatement; |
||||||
|
private readonly updateResponseQuery: D1PreparedStatement; |
||||||
|
private readonly deleteResponseQuery: D1PreparedStatement; |
||||||
|
|
||||||
|
constructor(db: D1Database) { |
||||||
|
this.getResponsesGlobal = db.prepare( |
||||||
|
`SELECT DISTINCT tableId, text FROM responses
|
||||||
|
WHERE access = ${ValueAccess.Global}`)
|
||||||
|
this.getResponsesInServerQuery = db.prepare( |
||||||
|
`SELECT DISTINCT tableId, text FROM responses
|
||||||
|
WHERE access = ${ValueAccess.Global} |
||||||
|
OR (access = ${ValueAccess.Server} AND serverSnowflake = ?);`)
|
||||||
|
this.getResponsesInDMQuery = db.prepare( |
||||||
|
`SELECT DISTINCT tableId, text FROM responses
|
||||||
|
WHERE access = ${ValueAccess.Global} |
||||||
|
OR (access = ${ValueAccess.CreatorDM} AND userSnowflake = ?);`)
|
||||||
|
this.putResponseQuery = db.prepare( |
||||||
|
`INSERT OR IGNORE INTO responses (id, tableId, text, timestamp, userSnowflake, serverSnowflake, access) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING timestamp, access;`) |
||||||
|
this.updateResponseQuery = db.prepare( |
||||||
|
`UPDATE responses SET text = ?3, timestamp = ?4, userSnowflake = ?5, serverSnowflake = ?6
|
||||||
|
WHERE tableId = ?1 AND text = ?2 |
||||||
|
AND ((?7 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?5) |
||||||
|
OR (?7 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?6)) |
||||||
|
RETURNING timestamp, access;`)
|
||||||
|
this.deleteResponseQuery = db.prepare( |
||||||
|
`DELETE FROM responses
|
||||||
|
WHERE tableId = ?1 AND text = ?2 |
||||||
|
AND ((?5 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?3) |
||||||
|
OR (?5 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?4)) |
||||||
|
RETURNING timestamp, access;`)
|
||||||
|
this.checkResponseAlreadyExistsQuery = db.prepare( |
||||||
|
`SELECT timestamp, access FROM responses
|
||||||
|
WHERE tableId = ?1 AND text = ?2 |
||||||
|
AND (access = ${ValueAccess.Global} |
||||||
|
OR (?3 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?4) |
||||||
|
OR (?3 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?4));`)
|
||||||
|
} |
||||||
|
|
||||||
|
async getGlobalResponses(): Promise<RollableTables> { |
||||||
|
const {results} = await this.getResponsesGlobal.all<DbResponse>() |
||||||
|
return buildRollableTables(results) |
||||||
|
} |
||||||
|
|
||||||
|
async getResponsesInServer(inServerSnowflake: string): Promise<RollableTables> { |
||||||
|
const statement = this.getResponsesInServerQuery.bind(inServerSnowflake) |
||||||
|
const {results} = await statement.all<DbResponse>() |
||||||
|
return buildRollableTables(results) |
||||||
|
} |
||||||
|
|
||||||
|
async getResponsesInDMWith(withUserSnowflake: string): Promise<RollableTables> { |
||||||
|
const statement = this.getResponsesInDMQuery.bind(withUserSnowflake) |
||||||
|
const {results} = await statement.all<DbResponse>() |
||||||
|
return buildRollableTables(results) |
||||||
|
} |
||||||
|
|
||||||
|
async putResponse(requestTimestamp: number, table: RollTable, text: string, fromUserSnowflake: string, inServerSnowflake: string|null, access?: ValueAccess): Promise<{ |
||||||
|
timestamp: number, |
||||||
|
access: ValueAccess, |
||||||
|
inserted: boolean |
||||||
|
}> { |
||||||
|
const effectiveAccess = access ?? (inServerSnowflake ? ValueAccess.Server : ValueAccess.CreatorDM) |
||||||
|
const relevantSnowflake = access === ValueAccess.Server ? inServerSnowflake : access === ValueAccess.CreatorDM ? fromUserSnowflake : null |
||||||
|
const existingResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, text, effectiveAccess, relevantSnowflake) |
||||||
|
const existingResponse = await existingResponseStatement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (existingResponse) { |
||||||
|
return { |
||||||
|
timestamp: existingResponse.timestamp, |
||||||
|
access: existingResponse.access, |
||||||
|
inserted: false, |
||||||
|
} |
||||||
|
} |
||||||
|
const statement = this.putResponseQuery.bind( |
||||||
|
requestTimestamp, table, text, requestTimestamp, fromUserSnowflake, inServerSnowflake, effectiveAccess) |
||||||
|
const result = await statement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (!result) { |
||||||
|
throw Error("no response from insert") |
||||||
|
} |
||||||
|
return { |
||||||
|
timestamp: result.timestamp, |
||||||
|
access: result.access, |
||||||
|
inserted: true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async updateResponse(timestamp: number, table: RollTable, oldText: string, newText: string, userId: string, guildId: string | null, access?: ValueAccess): Promise<{result: UpdateResult.NoOldText|UpdateResult.Updated} | {result: UpdateResult.NewConflict, timestamp: number, access: ValueAccess} | {result: UpdateResult.OldGlobal, timestamp: number, access: ValueAccess.Global}> { |
||||||
|
const effectiveAccess = access ?? (guildId ? ValueAccess.Server : ValueAccess.CreatorDM) |
||||||
|
const relevantSnowflake = access === ValueAccess.Server ? guildId : access === ValueAccess.CreatorDM ? userId : null |
||||||
|
const existingOldResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, oldText, effectiveAccess, relevantSnowflake) |
||||||
|
const existingOldResponse = await existingOldResponseStatement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (!existingOldResponse) { |
||||||
|
return { |
||||||
|
result: UpdateResult.NoOldText |
||||||
|
} |
||||||
|
} else if (existingOldResponse.access === ValueAccess.Global) { |
||||||
|
return { |
||||||
|
timestamp: existingOldResponse.timestamp, |
||||||
|
access: existingOldResponse.access, |
||||||
|
result: UpdateResult.OldGlobal, |
||||||
|
} |
||||||
|
} |
||||||
|
const existingNewResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, newText, effectiveAccess, relevantSnowflake) |
||||||
|
const existingNewResponse = await existingNewResponseStatement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (existingNewResponse) { |
||||||
|
return { |
||||||
|
result: UpdateResult.NewConflict, |
||||||
|
timestamp: existingNewResponse.timestamp, |
||||||
|
access: existingNewResponse.access, |
||||||
|
} |
||||||
|
} |
||||||
|
const statement = this.updateResponseQuery.bind( |
||||||
|
table, oldText, newText, timestamp, userId, guildId, effectiveAccess) |
||||||
|
await statement.run() |
||||||
|
return {result: UpdateResult.Updated} |
||||||
|
} |
||||||
|
|
||||||
|
async deleteResponse(table: RollTable, text: string, userId: string, guildId: string | null, access?: ValueAccess): Promise< |
||||||
|
{result: DeleteResult.Deleted, timestamp: number, access: ValueAccess} | |
||||||
|
{result: DeleteResult.NoOldText} | |
||||||
|
{result: DeleteResult.OldGlobal, timestamp: number, access: ValueAccess.Global} |
||||||
|
> { |
||||||
|
const effectiveAccess = access ?? (guildId ? ValueAccess.Server : ValueAccess.CreatorDM) |
||||||
|
const relevantSnowflake = access === ValueAccess.Server ? guildId : access === ValueAccess.CreatorDM ? userId : null |
||||||
|
const existingOldResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, text, effectiveAccess, relevantSnowflake) |
||||||
|
const existingOldResponse = await existingOldResponseStatement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (!existingOldResponse) { |
||||||
|
return { |
||||||
|
result: DeleteResult.NoOldText |
||||||
|
} |
||||||
|
} else if (existingOldResponse.access === ValueAccess.Global) { |
||||||
|
return { |
||||||
|
timestamp: existingOldResponse.timestamp, |
||||||
|
access: existingOldResponse.access, |
||||||
|
result: DeleteResult.OldGlobal, |
||||||
|
} |
||||||
|
} |
||||||
|
const statement = this.deleteResponseQuery.bind( |
||||||
|
table, text, userId, guildId, effectiveAccess) |
||||||
|
const deleted = await statement.first<{timestamp: number, access: ValueAccess}>() |
||||||
|
if (!deleted) { |
||||||
|
throw Error("no response from delete") |
||||||
|
} |
||||||
|
return {result: DeleteResult.Deleted, timestamp: deleted.timestamp, access: deleted.access} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,233 @@ |
|||||||
|
import { type RollableTables, rollOn, RollTable, RollTableOrder } from './rolltable.js'; |
||||||
|
import { |
||||||
|
ButtonStyle, |
||||||
|
type ComponentActionRow, |
||||||
|
type ComponentButton, |
||||||
|
type ComponentSelectMenu, |
||||||
|
type ComponentSelectOption, |
||||||
|
ComponentType, |
||||||
|
EmbedField, |
||||||
|
MessageEmbed, type MessageEmbedOptions, type MessageOptions |
||||||
|
} from 'slash-create/web'; |
||||||
|
|
||||||
|
export type ComponentValues = { [key in RollTable]?: string } |
||||||
|
export type ComponentLocks = { [Key in RollTable]?: boolean } |
||||||
|
|
||||||
|
export interface GeneratedMessage { |
||||||
|
values: ComponentValues |
||||||
|
locked?: ComponentLocks |
||||||
|
} |
||||||
|
|
||||||
|
export const RollTableEmoji = { |
||||||
|
[RollTable.Setting]: '\u{1f3d9}\ufe0f', |
||||||
|
[RollTable.Theme]: '\u{1f4d4}', |
||||||
|
[RollTable.Start]: '\u25b6\ufe0f', |
||||||
|
[RollTable.Challenge]: '\u{1f613}', |
||||||
|
[RollTable.Twist]: '\u{1f500}', |
||||||
|
[RollTable.Focus]: '\u{1f444}', |
||||||
|
[RollTable.Word]: '\u{2728}' |
||||||
|
} as const satisfies {readonly [key in RollTable]: string} |
||||||
|
|
||||||
|
export const RollTableEmbedTitles = { |
||||||
|
[RollTable.Setting]: 'The action takes place...', |
||||||
|
[RollTable.Theme]: 'The encounter is themed around...', |
||||||
|
[RollTable.Start]: 'The action begins when...', |
||||||
|
[RollTable.Challenge]: 'Things are more difficult because...', |
||||||
|
[RollTable.Twist]: 'Partway through, unexpectedly...', |
||||||
|
[RollTable.Focus]: 'The vore scene is focused on...', |
||||||
|
[RollTable.Word]: 'The word of the day is...' |
||||||
|
} as const satisfies {readonly [key in RollTable]: string} |
||||||
|
|
||||||
|
export const RollTableNames = { |
||||||
|
[RollTable.Setting]: 'Setting', |
||||||
|
[RollTable.Theme]: 'Theme', |
||||||
|
[RollTable.Start]: 'Inciting Incident', |
||||||
|
[RollTable.Challenge]: 'Challenge', |
||||||
|
[RollTable.Twist]: 'Twist', |
||||||
|
[RollTable.Focus]: 'Vore Scene Focus', |
||||||
|
[RollTable.Word]: 'Word of the Day' |
||||||
|
} as const satisfies {readonly [key in RollTable]: string} |
||||||
|
|
||||||
|
|
||||||
|
export const RollTableEmbedsReversed = { |
||||||
|
"\u{1f3d9}\ufe0f The action takes place...": RollTable.Setting, |
||||||
|
"\u{1f4d4} The encounter is themed around...": RollTable.Theme, |
||||||
|
"\u25b6\ufe0f The action begins when...": RollTable.Start, |
||||||
|
"\u{1f613} Things are more difficult because...": RollTable.Challenge, |
||||||
|
"\u{1f500} Partway through, unexpectedly...": RollTable.Twist, |
||||||
|
"\u{1f444} The vore scene is focused on...": RollTable.Focus, |
||||||
|
"\u{2728} The word of the day is...": RollTable.Word, |
||||||
|
} as const satisfies {readonly [key in RollTable as `${typeof RollTableEmoji[key]} ${typeof RollTableEmbedTitles[key]}`]: key} & {[other: string]: RollTable} |
||||||
|
|
||||||
|
export function calculateUnlockedValues(original?: ComponentValues|undefined, locks?: ComponentLocks|undefined): RollTable[] { |
||||||
|
if (!original && !locks) { |
||||||
|
return RollTableOrder |
||||||
|
} |
||||||
|
const existingItems = original ? RollTableOrder.filter(v => typeof original[v] !== "undefined") : RollTableOrder |
||||||
|
return locks ? existingItems.filter(v => locks[v] !== true) : existingItems |
||||||
|
} |
||||||
|
|
||||||
|
export function generateValuesFor(selected: readonly RollTable[], tables: RollableTables, original: ComponentValues = {}): ComponentValues { |
||||||
|
const result: ComponentValues = Object.assign({}, original) |
||||||
|
for (const table of selected) { |
||||||
|
result[table] = rollOn(table, tables) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export const LOCK_SUFFIX = " \u{1f512}" |
||||||
|
export const UNLOCK_SUFFIX = " \u{1f513}" |
||||||
|
|
||||||
|
export function generateFieldFor(field: RollTable, value: string, lock: boolean|null = null) { |
||||||
|
return { |
||||||
|
name: RollTableEmoji[field] + " " + RollTableEmbedTitles[field] + (lock !== null ? (lock ? LOCK_SUFFIX : UNLOCK_SUFFIX) : ""), |
||||||
|
value, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function generateEmbedFor(values: ComponentValues, locks: ComponentLocks|undefined): MessageEmbedOptions { |
||||||
|
const fields: EmbedField[] = [] |
||||||
|
const usableLocks = locks ?? {} |
||||||
|
for (const field of RollTableOrder) { |
||||||
|
const value = values[field] |
||||||
|
if (value) { |
||||||
|
fields.push(generateFieldFor(field, value, usableLocks.hasOwnProperty(field) ? usableLocks[field] : null)) |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
title: 'Your generated scenario', |
||||||
|
fields, |
||||||
|
timestamp: new Date().toISOString() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function getEmbedFrom({embeds}: {embeds?: MessageEmbed[]|undefined}): MessageEmbed { |
||||||
|
const result = embeds && embeds.length >= 1 ? embeds[0] : null |
||||||
|
if (!result) { |
||||||
|
throw Error("there were no embeds on the message to read") |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
export function loadEmbed(embed: MessageEmbed): GeneratedMessage { |
||||||
|
const result: {values: ComponentValues, locked: ComponentLocks} = { |
||||||
|
values: {}, |
||||||
|
locked: {}, |
||||||
|
} |
||||||
|
if (!embed.fields || embed.fields.length === 0) { |
||||||
|
throw Error("there were no fields on the embed to read") |
||||||
|
} |
||||||
|
for (const field of embed.fields!) { |
||||||
|
let locked: boolean|undefined, |
||||||
|
name = field.name |
||||||
|
if (name.endsWith(LOCK_SUFFIX)) { |
||||||
|
locked = true |
||||||
|
name = name.substring(0, name.length - LOCK_SUFFIX.length) |
||||||
|
} else if (name.endsWith(UNLOCK_SUFFIX)) { |
||||||
|
locked = false |
||||||
|
name = name.substring(0, name.length - UNLOCK_SUFFIX.length) |
||||||
|
} else { |
||||||
|
throw Error(`there was no lock or unlock suffix on ${name}`) |
||||||
|
} |
||||||
|
const value = field.value |
||||||
|
if (RollTableEmbedsReversed.hasOwnProperty(name)) { |
||||||
|
const table = RollTableEmbedsReversed[name as keyof typeof RollTableEmbedsReversed] |
||||||
|
if (typeof locked !== "undefined") { |
||||||
|
result.locked[table] = locked |
||||||
|
} |
||||||
|
result.values[table] = value |
||||||
|
} else { |
||||||
|
throw Error(`I don't know a field named ${name}`) |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export function populateLocksFor(values: ComponentValues, original?: ComponentLocks|undefined): ComponentLocks { |
||||||
|
const result = Object.assign({}, original) |
||||||
|
for (const table of RollTableOrder) { |
||||||
|
if (typeof values[table] !== "undefined") { |
||||||
|
result[table] = result[table] ?? true |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export function selectUnlockedFrom(values: string[], oldLocks?: ComponentLocks | undefined): ComponentLocks { |
||||||
|
const result = Object.assign({}, oldLocks ?? {}) |
||||||
|
for (const table of RollTableOrder) { |
||||||
|
if (result.hasOwnProperty(table)) { |
||||||
|
result[table] = !values.includes(`${table}`) |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export const SELECT_ID = "selected" |
||||||
|
export const REROLL_ID = "reroll" |
||||||
|
export const DONE_ID = "done" |
||||||
|
export const DELETE_ID = "delete" |
||||||
|
|
||||||
|
export function generateActionsFor(values: ComponentValues, locks: ComponentLocks|undefined): ComponentActionRow[] { |
||||||
|
if (!locks) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
const items = RollTableOrder.filter((v) => values.hasOwnProperty(v)) |
||||||
|
const lockedItems = items.filter((v) => locks[v] === true) |
||||||
|
const selectOptions: ComponentSelectOption[] = items.map((v) => ({ |
||||||
|
default: !(locks[v] ?? false), |
||||||
|
value: `${v}`, |
||||||
|
label: RollTableNames[v], |
||||||
|
emoji: {name: RollTableEmoji[v]} |
||||||
|
})) |
||||||
|
if (selectOptions.length === 0) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
const select: ComponentSelectMenu = { |
||||||
|
type: ComponentType.STRING_SELECT, |
||||||
|
custom_id: SELECT_ID, |
||||||
|
disabled: false, |
||||||
|
max_values: selectOptions.length, |
||||||
|
min_values: 0, |
||||||
|
options: selectOptions, |
||||||
|
placeholder: 'Components to reroll' |
||||||
|
} |
||||||
|
const selectRow: ComponentActionRow = { type: ComponentType.ACTION_ROW, components: [ select ] } |
||||||
|
const rerollButton: ComponentButton = { |
||||||
|
type: ComponentType.BUTTON, |
||||||
|
custom_id: REROLL_ID, |
||||||
|
disabled: lockedItems.length === items.length, |
||||||
|
emoji: {name: '\u{1f3b2}'}, |
||||||
|
label: (lockedItems.length === 0 ? "Reroll ALL" : "Reroll Selected"), |
||||||
|
style: ButtonStyle.PRIMARY |
||||||
|
} |
||||||
|
const doneButton: ComponentButton = { |
||||||
|
type: ComponentType.BUTTON, |
||||||
|
custom_id: DONE_ID, |
||||||
|
disabled: false, |
||||||
|
emoji: { name: '\u{1f44d}' }, |
||||||
|
label: 'Looks good!', |
||||||
|
style: ButtonStyle.SUCCESS, |
||||||
|
} |
||||||
|
const deleteButton: ComponentButton = { |
||||||
|
type: ComponentType.BUTTON, |
||||||
|
custom_id: DELETE_ID, |
||||||
|
disabled: false, |
||||||
|
emoji: { name: '\u{1f5d1}\ufe0f' }, |
||||||
|
label: 'Trash it.', |
||||||
|
style: ButtonStyle.DESTRUCTIVE, |
||||||
|
} |
||||||
|
const buttonRow: ComponentActionRow = { type: ComponentType.ACTION_ROW, components: [rerollButton, doneButton, deleteButton] } |
||||||
|
return [selectRow, buttonRow] |
||||||
|
} |
||||||
|
|
||||||
|
export function generateMessageFor(values: ComponentValues, locks: ComponentLocks|undefined): MessageOptions { |
||||||
|
return { embeds: [generateEmbedFor(values, locks)], components: generateActionsFor(values, locks), ephemeral: false } |
||||||
|
} |
||||||
|
|
||||||
|
export function generateErrorMessageFor(e: unknown, context?: string): MessageOptions { |
||||||
|
console.error(`Error when trying to ${context ?? "do something (unknown context)"}`, e) |
||||||
|
return { |
||||||
|
content: `I wasn't able to ${context ?? "do that"}. Thing is, ${e}...`, |
||||||
|
ephemeral: true, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
export enum RollTable { |
||||||
|
Setting = 0, |
||||||
|
Theme = 1, |
||||||
|
Start = 2, |
||||||
|
Challenge = 3, |
||||||
|
Twist = 4, |
||||||
|
Focus = 5, |
||||||
|
Word = 6, |
||||||
|
} |
||||||
|
|
||||||
|
export enum ValueAccess { |
||||||
|
Global = 0, |
||||||
|
Server = 1, |
||||||
|
CreatorDM = 2, |
||||||
|
} |
||||||
|
|
||||||
|
export const RollTableOrder = |
||||||
|
[RollTable.Setting, RollTable.Theme, RollTable.Start, RollTable.Challenge, RollTable.Twist, RollTable.Focus, RollTable.Word] as const satisfies RollTable[] |
||||||
|
|
||||||
|
export const RollTableOrdinals = |
||||||
|
{ |
||||||
|
[RollTable.Setting]: 0, |
||||||
|
[RollTable.Theme]: 1, |
||||||
|
[RollTable.Start]: 2, |
||||||
|
[RollTable.Challenge]: 3, |
||||||
|
[RollTable.Twist]: 4, |
||||||
|
[RollTable.Focus]: 5, |
||||||
|
[RollTable.Word]: 6, |
||||||
|
} as const satisfies {[key in RollTable]: number} & {[key in Extract<keyof typeof RollTableOrder, number> as typeof RollTableOrder[key]]: key} |
||||||
|
|
||||||
|
export type RollableTables = {readonly [key in RollTable]: readonly string[]} |
||||||
|
|
||||||
|
export function isTable(val: number): val is RollTable { |
||||||
|
return RollTableOrdinals.hasOwnProperty(val) |
||||||
|
} |
||||||
|
|
||||||
|
export function rollOn(table: RollTable, tables: RollableTables): string { |
||||||
|
const values = tables[table] |
||||||
|
if (values.length === 0) { |
||||||
|
throw Error(`no possible options for table ${table}`) |
||||||
|
} |
||||||
|
return values[Math.floor(values.length * Math.random())] |
||||||
|
} |
@ -1,51 +1,17 @@ |
|||||||
name = "ncc-gen" |
name = "vore-scenario-generator" |
||||||
main = "src/index.ts" |
main = "src/index.ts" |
||||||
compatibility_date = "2023-12-18" |
compatibility_date = "2023-12-18" |
||||||
|
|
||||||
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) |
[vars] |
||||||
# Note: Use secrets to store sensitive data. |
# BASE_URL |
||||||
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables |
# DISCORD_APP_ID |
||||||
# [vars] |
# DISCORD_APP_SECRET |
||||||
# MY_VARIABLE = "production_value" |
# DISCORD_PUBLIC_KEY |
||||||
|
# DISCORD_DEV_GUILD_IDS |
||||||
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. |
|
||||||
# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv |
[[d1_databases]] |
||||||
# [[kv_namespaces]] |
binding = "DB" # i.e. available in your Worker on env.DB |
||||||
# binding = "MY_KV_NAMESPACE" |
database_name = "production-ncc-gen" |
||||||
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
database_id = "d09d3c74-c75f-4418-8f1b-2fe7f21637e6" |
||||||
|
migrations_table = "d1_migrations" |
||||||
# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. |
migrations_dir = "migrations" |
||||||
# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ |
|
||||||
# [[r2_buckets]] |
|
||||||
# binding = "MY_BUCKET" |
|
||||||
# bucket_name = "my-bucket" |
|
||||||
|
|
||||||
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. |
|
||||||
# Docs: https://developers.cloudflare.com/queues/get-started |
|
||||||
# [[queues.producers]] |
|
||||||
# binding = "MY_QUEUE" |
|
||||||
# queue = "my-queue" |
|
||||||
|
|
||||||
# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. |
|
||||||
# Docs: https://developers.cloudflare.com/queues/get-started |
|
||||||
# [[queues.consumers]] |
|
||||||
# queue = "my-queue" |
|
||||||
|
|
||||||
# Bind another Worker service. Use this binding to call another Worker without network overhead. |
|
||||||
# Docs: https://developers.cloudflare.com/workers/platform/services |
|
||||||
# [[services]] |
|
||||||
# binding = "MY_SERVICE" |
|
||||||
# service = "my-service" |
|
||||||
|
|
||||||
# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. |
|
||||||
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. |
|
||||||
# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects |
|
||||||
# [[durable_objects.bindings]] |
|
||||||
# name = "MY_DURABLE_OBJECT" |
|
||||||
# class_name = "MyDurableObject" |
|
||||||
|
|
||||||
# Durable Object migrations. |
|
||||||
# Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations |
|
||||||
# [[migrations]] |
|
||||||
# tag = "v1" |
|
||||||
# new_classes = ["MyDurableObject"] |
|
||||||
|
Loading…
Reference in new issue