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.
 
 

473 lines
15 KiB

import {
type FinalGeneratedContents,
type FinalGeneratedState,
type GeneratedContents,
type GeneratedState,
type InProgressGeneratedContents,
type InProgressGeneratedState, RolledValues, RollSelections,
type RollTable,
type RollTableAuthor, RollTableDatabase,
type RollTableDetailsNoResults,
type RollTableResult,
type RollTableResultFull
} from '../../common/rolltable.js';
import { type PreparedQueries, type QueryOutput, TypedDBWrapper } from './querytypes.js';
import {
DatabaseQueries,
} from './queries.js';
import { recordError } from '../discord/embed.js';
function processOperationResult(result: QueryOutput<(typeof DatabaseQueries)["getResultMappingsForDiscordSet"]>[number] | undefined): (RollTableResultFull<RollTableDetailsNoResults> & {
status: 'updated' | 'existing'
}) | undefined {
if (!result) {
return result;
}
return {
full: true,
mappingId: result.mappingId,
textId: result.resultId,
text: result.resultText,
table: {
full: 'details',
id: result.tableId,
identifier: result.tableIdentifier,
name: result.tableName,
title: result.tableTitle,
emoji: result.tableEmoji,
header: result.tableHeader,
ordinal: result.tableOrdinal
},
author: (result.authorId === null || result.authorName === null || result.authorRelation === null) ? null : {
id: result.authorId,
name: result.authorName,
url: result.authorUrl,
relation: result.authorRelation
},
set: {
id: result.setId,
name: result.setName,
description: result.setDescription,
global: !!(result.setGlobal)
},
updated: new Date(result.updated),
status: result.status
};
}
function processGeneratedRow(result: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>[number]): RollTableResult & { selected: boolean } {
if (result.tableId === null) {
return {
full: false,
table: {
full: false,
emoji: result.tableEmoji,
title: result.tableTitle,
header: result.tableHeader,
ordinal: result.tableOrdinal
},
text: result.resultText,
selected: false
};
} else if (result.mappingId === null) {
return {
full: false,
table: {
full: 'details',
emoji: result.tableEmoji,
header: result.tableHeader,
id: result.tableId,
identifier: result.tableIdentifier,
name: result.tableName,
ordinal: result.tableOrdinal,
title: result.tableTitle
},
text: result.resultText,
selected: result.selected
};
} else {
return {
full: true,
table: {
full: 'details',
emoji: result.tableEmoji,
header: result.tableHeader,
id: result.tableId,
identifier: result.tableIdentifier,
name: result.tableName,
ordinal: result.tableOrdinal,
title: result.tableTitle
},
author: result.authorId && result.authorName && result.authorRelation ? {
id: result.authorId,
name: result.authorName,
url: result.authorUrl,
relation: result.authorRelation
} : null,
set: {
id: result.setId,
name: result.setName,
description: result.setDescription,
global: !!(result.setGlobal)
},
mappingId: result.mappingId,
textId: result.resultId,
text: result.resultText,
updated: new Date(result.updated),
selected: result.selected
};
}
}
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: true): FinalGeneratedState
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: false): InProgressGeneratedState
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: boolean): GeneratedState
function processGeneration(results: QueryOutput<typeof DatabaseQueries['generateFromDiscord']>, final: boolean): GeneratedState {
const rolled = new Map<RollTable, RollTableResult>();
const selected = new Set<RollTable>();
for (const rawResult of results) {
const processed = processGeneratedRow(rawResult);
rolled.set(processed.table, processed);
if (!final && processed.selected) {
selected.add(processed.table);
}
}
return final ? {
final,
rolled
} : {
final,
rolled,
selected
};
}
export class Database {
private readonly db: TypedDBWrapper;
private readonly queries: PreparedQueries<typeof DatabaseQueries>;
constructor(db: D1Database) {
this.db = new TypedDBWrapper(db);
this.queries = this.db.prepareAll(DatabaseQueries);
}
async autocompleteTable(tableSoFar: string) {
return this.db.run(this.queries.autocompleteTable({
tableIdentifierSubstring: tableSoFar
}));
}
async autocompleteText(setSnowflake: string, tableIdentifier: string, partialText: string, includeGlobal: boolean) {
return this.db.run(this.queries.autocompleteTextForDiscordSet({
setSnowflake: setSnowflake,
tableIdentifierSubstring: tableIdentifier,
pattern: partialText,
includeGlobal,
}));
}
async addResponseFromDiscord(timestamp: number, table: string | number, text: string, userId: string, username: string, setId: string) {
const [, , , , results] = await this.db.batch(
this.queries.addResultForAddMapping({ tableIdentifier: table, text }),
this.queries.addDiscordAuthorForAddMapping({ userSnowflake: userId, username }),
this.queries.addDiscordSetForAddMapping({ setSnowflake: setId, userSnowflake: userId }),
this.queries.addDiscordResultMapping({
timestamp,
tableIdentifier: table,
resultText: text,
userSnowflake: userId,
setSnowflake: setId
}),
this.queries.getResultMappingsForDiscordSet({
timestamp,
tableIdentifier: table,
text,
setSnowflake: setId,
includeGlobal: false
})
);
const result = processOperationResult(results[0]);
if (!result) {
throw Error('failed adding the new response');
}
return {
...result,
status: result.status === 'updated' ? 'added' : 'existed'
};
}
async editResponseFromDiscord(timestamp: number, table: number | string, oldText: string, newText: string, userId: string, username: string, setId: string): Promise<{
status: 'nonexistent'
} | {
status: 'noneditable',
old: RollTableResultFull<RollTableDetailsNoResults>
} | {
status: 'conflict' | 'updated',
old: RollTableResultFull<RollTableDetailsNoResults>,
new: RollTableResultFull<RollTableDetailsNoResults>,
}> {
const [oldResults, , , , newResults] = await this.db.batch(
this.queries.getResultMappingsForDiscordSet({
timestamp,
tableIdentifier: table,
text: oldText,
setSnowflake: setId,
includeGlobal: true
}),
this.queries.addResultForEditMapping({
tableIdentifier: table,
oldText,
newText,
setSnowflake: setId
}),
this.queries.addDiscordAuthorForEditMapping({
userSnowflake: userId,
username,
tableIdentifier: table,
oldText,
newText,
setSnowflake: setId
}),
this.queries.editMappingForDiscord({
timestamp,
tableIdentifier: table,
oldText,
newText,
userSnowflake: userId,
setSnowflake: setId
}),
this.queries.getResultMappingsForDiscordSet({
timestamp,
tableIdentifier: table,
text: newText,
setSnowflake: setId,
includeGlobal: false
})
);
const oldResult = processOperationResult(oldResults[0]);
if (!oldResult) {
return { status: 'nonexistent' };
}
if (oldResult.set?.global) {
return { status: 'noneditable', old: oldResult };
}
const newResult = processOperationResult(newResults[0]);
if (!newResult) {
throw Error('failed to update response');
}
return {
status: newResult.status === 'updated' ? 'updated' : 'conflict',
old: oldResult,
new: newResult
};
}
async deleteResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{
status: 'nonexistent'
} | {
status: 'noneditable' | 'deleted',
old: RollTableResultFull<RollTableDetailsNoResults>
}> {
const [oldResults, deleted] = await this.db.batch(
this.queries.getResultMappingsForDiscordSet({
timestamp: null,
tableIdentifier: table,
text,
setSnowflake: setId,
includeGlobal: true
}),
this.queries.deleteDiscordResultMapping({
tableIdentifier: table,
text,
setSnowflake: setId
})
);
const oldResult = processOperationResult(oldResults[0]);
if (!oldResult) {
return {
status: 'nonexistent'
};
}
if (!deleted) {
return {
status: 'noneditable',
old: oldResult
};
}
return {
status: 'deleted',
old: oldResult
};
}
async getResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{
status: 'nonexistent'
} | ({
status: 'existent',
} & RollTableResultFull<RollTableDetailsNoResults>)> {
const results = await this.db.run(this.queries.getResultMappingsForDiscordSet({
timestamp: null,
tableIdentifier: table,
text,
setSnowflake: setId,
includeGlobal: true,
}))
const result = processOperationResult(results[0]);
if (!result) {
return {
status: 'nonexistent'
};
}
return {
...result,
status: 'existent'
};
}
private async runGenerateFromDiscord(reroll: true, setId: string|null, contents?: InProgressGeneratedContents | null, finalize?: false): Promise<InProgressGeneratedState>
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: false): Promise<InProgressGeneratedState>
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: true): Promise<FinalGeneratedState>
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: InProgressGeneratedContents): Promise<InProgressGeneratedState>
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: FinalGeneratedContents): Promise<FinalGeneratedState>
private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize?: boolean): Promise<GeneratedState>
private async runGenerateFromDiscord(reroll: boolean, setId: string|null, contents?: GeneratedContents | null, finalize?: boolean): Promise<GeneratedState> {
const results = await this.db.run(this.queries.generateFromDiscord({
reroll,
setSnowflake: setId,
original: contents ? Array.from(contents.rolled) : null,
selection: contents && !contents.final ? Array.from(contents.selected) : null
}))
return processGeneration(results, finalize ?? contents?.final ?? false);
}
async expandFromDiscordSet(setId: string, contents: FinalGeneratedContents): Promise<FinalGeneratedState>
async expandFromDiscordSet(setId: string, contents: InProgressGeneratedContents): Promise<InProgressGeneratedState>
async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise<GeneratedState>
async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise<GeneratedState> {
return this.runGenerateFromDiscord(false, setId, contents);
}
async generateFromDiscordSet(setId: string): Promise<InProgressGeneratedState> {
return this.runGenerateFromDiscord(true, setId);
}
async rerollFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<InProgressGeneratedState> {
return this.runGenerateFromDiscord(true, setId, existing);
}
async reopenFromDiscordSet(setId: string, existing: FinalGeneratedContents): Promise<InProgressGeneratedState> {
return this.runGenerateFromDiscord(false, setId, existing, false);
}
async finalizeFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise<FinalGeneratedState> {
return this.runGenerateFromDiscord(false, setId, existing, true);
}
async getDiscordAuthor(id: string): Promise<RollTableAuthor | null> {
return await this.db.run(this.queries.getDiscordAuthor({
userSnowflake: id,
}))
}
async setDiscordAuthor(id: string, username: string, name: string | null, url: string | null): Promise<RollTableAuthor|null> {
const [, result] = await this.db.batch(
this.queries.setDiscordAuthor({
userSnowflake: id,
username: username,
name: name,
url: url,
}),
this.queries.getDiscordAuthor({userSnowflake: id})
)
return result;
}
private async getWebPageDataForDiscordSet(reroll: true, setSnowflake: string|null, oldResults?: InProgressGeneratedContents | null, finalize?: false): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults?: null): Promise<RollTableDatabase>
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: false): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: true): Promise<FinalGeneratedState & {db: RollTableDatabase}>
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: InProgressGeneratedContents): Promise<InProgressGeneratedState & {db: RollTableDatabase}>
private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: FinalGeneratedContents): Promise<FinalGeneratedState & {db: RollTableDatabase}>
private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise<RollTableDatabase | (GeneratedState & {db: RollTableDatabase})>
private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise<RollTableDatabase | (GeneratedState & {db: RollTableDatabase})> {
const { tables, mappings, results, sets, authors } =
await this.db.run(this.queries.getFullDatabaseForDiscordSet({
reroll,
setSnowflake,
original: oldResults ? Array.from(oldResults.rolled) : null,
selection: oldResults && !oldResults.final ? Array.from(oldResults.selected) : null,
}))
const db = new RollTableDatabase({
tables, authors, sets, results: mappings.map(v => ({...v, updated: new Date(v.updated)}))})
if (!results) {
return db
}
const rolled = new RolledValues()
for (const result of results) {
switch (result.type) {
case 'mapping':
const mapping = db.mappings.get(result.mappingId)
if (mapping) {
rolled.add(mapping)
} else {
recordError({
error: Error(`no mapping with ID ${result.mappingId}`),
context: 'getting web page data for discord set',
})
}
break
case 'unknownText':
const table = db.tables.get(result.tableId)
if (table) {
rolled.add({
full: false,
text: result.text,
table
})
} else {
recordError({
error: Error(`no table with ID ${result.tableId}`),
context: `assembling unknown text for discord set`
})
}
break
case 'unknownTable':
rolled.add({
full: false,
table: {
full: false,
ordinal: result.ordinal,
header: result.header,
emoji: result.emoji,
title: result.title
},
text: result.text
})
break
}
}
if (finalize === true || (finalize === null && oldResults?.final)) {
return {
final: true,
db,
rolled
}
}
const selected = new RollSelections()
for (const table of tables) {
if (table.selected) {
selected.add(db.tables.get(table.id)!)
}
}
return {
final: false,
db,
rolled,
selected
}
}
async getGeneratorPageForDiscordSet(setSnowflake: string|null, oldResults?: InProgressGeneratedContents|null): Promise<InProgressGeneratedState & {db: RollTableDatabase}> {
return this.getWebPageDataForDiscordSet(true, setSnowflake, oldResults, false)
}
}