import {
type ApplicationCommandOptionLimitedString ,
type AutocompleteChoice ,
AutocompleteContext ,
CommandContext ,
CommandOptionType ,
ComponentContext ,
SlashCommand ,
type SlashCreator
} from 'slash-create/web' ;
import { type Database } from '../db/database' ;
import { type Snowflake } from 'discord-snowflake' ;
import {
DELETE_ID ,
DONE_ID , FAILURE_COLOR ,
generateAuthorForResult , generateEmbedForResult ,
generateErrorMessageFor ,
generateFieldForResult ,
generateFooterForResult ,
generateMessageFor ,
getEmbedFrom ,
loadEmbed , recordError ,
REROLL_ID ,
SELECT_ID , SUCCESS_COLOR , WARNING_COLOR
} from './embed' ;
import {
generatedContentsToString , generatedStateToString ,
MAX_IDENTIFIER_LENGTH ,
MAX_NAME_LENGTH ,
MAX_RESULT_LENGTH ,
MAX_URL_LENGTH ,
type RollTableAuthor
} from '../../common/rolltable' ;
import markdownEscape from 'markdown-escape' ;
const tableOption : Omit < ApplicationCommandOptionLimitedString , ' name ' | ' description ' > = {
type : CommandOptionType . STRING ,
autocomplete : true ,
max_length : MAX_IDENTIFIER_LENGTH
} ;
const resultOption : Omit < ApplicationCommandOptionLimitedString , ' name ' | ' description ' > = {
type : CommandOptionType . STRING ,
max_length : MAX_RESULT_LENGTH
} ;
export class AuthorCommand extends SlashCommand {
private readonly db : Database ;
constructor ( creator : SlashCreator , db : Database , forGuilds? : Snowflake | Snowflake [ ] ) {
super ( creator , {
name : 'author' ,
description : 'Modifies the attribution of responses you contribute to the generator.' ,
nsfw : false ,
guildIDs : forGuilds ,
dmPermission : true ,
options : [
{
type : CommandOptionType . SUB_COMMAND ,
name : 'show' ,
description : 'Shows the attribution currently associated with your contributed responses.'
} ,
{
type : CommandOptionType . SUB_COMMAND ,
name : 'set' ,
description : 'Sets your contributed responses to be associated with a name and optional URL.' ,
options : [
{
name : 'name' ,
description : 'The name to associate with the responses you create.' ,
required : true ,
type : CommandOptionType . STRING ,
max_length : MAX_NAME_LENGTH
} ,
{
name : 'url' ,
description : 'The URL to associate with your name on the responses you create.' ,
type : CommandOptionType . STRING ,
max_length : MAX_URL_LENGTH
}
]
} ,
{
type : CommandOptionType . SUB_COMMAND ,
name : 'anonymous' ,
description : 'Sets your contributed responses to be anonymous again.'
}
]
} ) ;
this . db = db ;
}
async run ( ctx : CommandContext ) : Promise < void > {
let author : RollTableAuthor | null ;
switch ( ctx . subcommands [ 0 ] ) {
case 'show' :
try {
author = await this . onShow ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'get your current authorship' } ) ) ;
return ;
}
break ;
case 'set' :
try {
author = await this . onSet ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'set your authorship' } ) ) ;
return ;
}
break ;
case 'anonymous' :
try {
author = await this . onAnonymous ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'reset your authorship to anonymous' } ) ) ;
return ;
}
break ;
default :
await ctx . send ( generateErrorMessageFor ( {
error : Error ( 'I don\'t know what command you want' ) ,
context : 'manage authorship'
} ) ) ;
return ;
}
if ( author ) {
await ctx . send ( {
embeds : [ {
title : 'Your responses are credited as...' ,
description : ` ${ markdownEscape ( author . relation ) } ${ author . url ? '[' : '' } ${ markdownEscape ( author . name ) } ${ author . url ? '](' : '' } ${ markdownEscape ( author . url ? ? '' ) } ${ author . url ? ')' : '' } ` ,
color : SUCCESS_COLOR
} ] ,
ephemeral : true
} ) ;
} else {
await ctx . send ( {
embeds : [ {
title : 'Your responses are credited as...' ,
description : 'Hey, wait, _what_ responses? I don\'t know anything about you because you haven\'t done anything yet. Come back here when you\'ve contributed a response with /response add or /response edit or used /author set or /author anonymous to tell me about yourself.' ,
color : FAILURE_COLOR
} ] ,
ephemeral : true
} ) ;
}
}
private async onShow ( ctx : CommandContext ) : Promise < RollTableAuthor | null > {
return await this . db . getDiscordAuthor ( ctx . user . id ) ;
}
private async onSet ( ctx : CommandContext ) : Promise < RollTableAuthor | null > {
return await this . db . setDiscordAuthor ( ctx . user . id , ctx . user . username , ctx . options [ 'set' ] [ 'name' ] , ctx . options [ 'set' ] [ 'url' ] ) ;
}
private async onAnonymous ( ctx : CommandContext ) : Promise < RollTableAuthor | null > {
return await this . db . setDiscordAuthor ( ctx . user . id , ctx . user . username , null , null ) ;
}
}
export class ResponseCommand extends SlashCommand {
private readonly db : Database ;
private readonly baseUrl : string ;
constructor ( creator : SlashCreator , db : Database , 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 : 'list' ,
description : 'Provides a link to the list of responses that will appear in /generate in the current context.'
} ,
{
type : CommandOptionType . SUB_COMMAND ,
name : 'show' ,
description : 'Shows details about a response that was previously created.' ,
options : [
{
. . . tableOption ,
name : 'table' ,
description : 'The table to show the response from.' ,
required : true
} ,
{
. . . resultOption ,
name : 'text' ,
description : 'The text of the response to show.' ,
autocomplete : true ,
required : true
}
]
} ,
{
type : CommandOptionType . SUB_COMMAND ,
name : 'add' ,
description : 'Adds a new response to the generator.' ,
options : [
{
. . . tableOption ,
name : 'table' ,
description : 'The table to insert the response into.' ,
required : true
} ,
{
. . . resultOption ,
name : 'text' ,
description : 'The text to use as the response.' ,
required : true
}
]
} ,
{
type : CommandOptionType . SUB_COMMAND ,
name : 'edit' ,
description : 'Modifies a response that was previously created.' ,
options : [
{
. . . tableOption ,
name : 'table' ,
description : 'The table to update the response from.' ,
required : true
} ,
{
. . . resultOption ,
name : 'old_text' ,
description : 'The text of the response to edit.' ,
autocomplete : true ,
required : true
} ,
{
. . . resultOption ,
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 : [
{
. . . tableOption ,
name : 'table' ,
description : 'The table to delete the response from.' ,
required : true
} ,
{
. . . resultOption ,
name : 'text' ,
description : 'The text of the response to delete.' ,
autocomplete : true ,
required : true
}
]
}
]
} ) ;
this . baseUrl = baseUrl ;
this . db = db ;
}
async autocompleteTable ( tableName : string ) : Promise < AutocompleteChoice [ ] > {
const results = await this . db . autocompleteTable ( tableName ) ;
return results . map ( ( { name , identifier , emoji } ) = > ( {
name : ` ${ emoji } ${ name } ` ,
value : identifier
} ) ) ;
}
async autocompleteResultText ( setSnowflake : string , tableIdentifier : string , partialText : string , includeGlobal : boolean ) : Promise < AutocompleteChoice [ ] > {
const results = await this . db . autocompleteText ( setSnowflake , tableIdentifier , partialText , includeGlobal ) ;
return results . map ( ( { text } ) = > ( {
name : text ,
value : text
} ) ) ;
}
async autocomplete ( ctx : AutocompleteContext ) : Promise < void > {
try {
const subcommand = ctx . subcommands [ 0 ] ;
switch ( subcommand ) {
case 'add' :
switch ( ctx . focused ) {
case 'table' :
await ctx . sendResults ( await this . autocompleteTable ( ctx . options [ 'add' ] [ 'table' ] ) ) ;
return ;
}
break ;
case 'edit' :
switch ( ctx . focused ) {
case 'table' :
await ctx . sendResults ( await this . autocompleteTable ( ctx . options [ 'edit' ] [ 'table' ] ) ) ;
return ;
case 'old_text' :
await ctx . sendResults ( await this . autocompleteResultText (
ctx . guildID ? ? ctx . user . id , ctx . options [ 'edit' ] [ 'table' ] , ctx . options [ 'edit' ] [ 'old_text' ] , false ) ) ;
return ;
}
break ;
case 'delete' :
switch ( ctx . focused ) {
case 'table' :
await ctx . sendResults ( await this . autocompleteTable ( ctx . options [ 'delete' ] [ 'table' ] ) ) ;
return ;
case 'text' :
await ctx . sendResults ( await this . autocompleteResultText (
ctx . guildID ? ? ctx . user . id , ctx . options [ 'delete' ] [ 'table' ] , ctx . options [ 'delete' ] [ 'text' ] , false ) ) ;
return ;
}
break ;
case 'show' :
switch ( ctx . focused ) {
case 'table' :
await ctx . sendResults ( await this . autocompleteTable ( ctx . options [ 'show' ] [ 'table' ] ) ) ;
return ;
case 'text' :
await ctx . sendResults ( await this . autocompleteResultText (
ctx . guildID ? ? ctx . user . id , ctx . options [ 'show' ] [ 'table' ] , ctx . options [ 'show' ] [ 'text' ] , true ) ) ;
return ;
}
break ;
}
await ctx . sendResults ( [ ] ) ;
} catch ( e ) {
recordError ( {
context : 'trying to autocomplete response commands' ,
error : e
} )
await ctx . sendResults ( [ ] ) ;
}
}
async run ( ctx : CommandContext ) : Promise < void > {
switch ( ctx . subcommands [ 0 ] ) {
case 'list' :
try {
await this . onList ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'get the list URL' } ) ) ;
}
break ;
case 'show' :
try {
await this . onShow ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'show that response' } ) ) ;
}
break ;
case 'add' :
try {
await this . onAdd ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'add that new response' } ) ) ;
}
break ;
case 'edit' :
try {
await this . onEdit ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'edit that response' } ) ) ;
}
break ;
case 'delete' :
try {
await this . onDelete ( ctx ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'delete that response' } ) ) ;
}
break ;
default :
await ctx . send ( generateErrorMessageFor ( {
error : Error ( 'I don\'t know what command you want' ) ,
context : 'manage responses'
} ) ) ;
break ;
}
}
private async onList ( ctx : CommandContext ) {
if ( ctx . guildID ) {
await ctx . send ( {
embeds : [ {
color : SUCCESS_COLOR ,
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 : [ {
color : FAILURE_COLOR ,
title : ` Response list for DMs ` ,
description : 'This is not supported right now, so please just hang tight.'
} ]
} ) ;
}
}
private async onShow ( ctx : CommandContext ) {
const setId = ctx . guildID ? ? ctx . user . id ;
const table = ctx . options [ 'show' ] [ 'table' ] ;
const text = ctx . options [ 'show' ] [ 'text' ] ;
const result = await this . db . getResponseFromDiscord ( table , text , setId ) ;
switch ( result . status ) {
case 'nonexistent' :
await ctx . send ( generateErrorMessageFor ( {
error : ` couldn't find a response with that text ` ,
context : ` show the response with the text ${ text } from the ${ table } table `
} ) ) ;
break ;
case 'existent' :
await ctx . send ( {
embeds : [ generateEmbedForResult ( 'Your requested response' , SUCCESS_COLOR , result ) ]
} ) ;
break ;
}
}
private async onAdd ( ctx : CommandContext ) : Promise < void > {
const userId = ctx . user . id ;
const setId = ctx . guildID ? ? userId ;
const timestamp = ctx . invokedAt ;
const table = ctx . options [ 'add' ] [ 'table' ] as string | number ;
const text = ctx . options [ 'add' ] [ 'text' ] ;
const result =
await this . db . addResponseFromDiscord ( timestamp , table , text , userId , ctx . user . username , setId ) ;
await ctx . send ( {
embeds : [ generateEmbedForResult ( ` ${ result . status === 'added' ? 'Your new' : 'An existing' } response ` , result . status === 'added' ? SUCCESS_COLOR : WARNING_COLOR , result ) ] ,
ephemeral : result.status === 'existed'
} ) ;
}
private async onEdit ( ctx : CommandContext ) : Promise < void > {
const userId = ctx . user . id ;
const setId = ctx . guildID ? ? userId ;
const timestamp = ctx . invokedAt ;
const table = ctx . options [ 'edit' ] [ 'table' ] ;
const oldText = ctx . options [ 'edit' ] [ 'old_text' ] ;
const newText = ctx . options [ 'edit' ] [ 'new_text' ] ;
const result = await this . db . editResponseFromDiscord ( timestamp , table , oldText , newText , userId , ctx . user . username , setId ) ;
switch ( result . status ) {
case 'nonexistent' :
await ctx . send ( generateErrorMessageFor ( {
error : ` couldn't find a response with that text ` ,
context : ` alter the response with the text ${ oldText } from the ${ table } table `
} ) ) ;
break ;
case 'noneditable' :
await ctx . send ( {
embeds : [ generateEmbedForResult ( 'A non-editable response (unchanged)' , FAILURE_COLOR , result . old ) ] ,
ephemeral : true
} ) ;
break ;
case 'conflict' :
await ctx . send ( {
embeds : [ generateEmbedForResult ( 'The old response (still existing)' , WARNING_COLOR , result . old ) , generateEmbedForResult ( 'A conflicting response' , FAILURE_COLOR , result . new ) ] ,
ephemeral : true
} ) ;
break ;
case 'updated' :
await ctx . send ( {
embeds : [ generateEmbedForResult ( 'The old response (now gone)' , SUCCESS_COLOR , result . old ) , generateEmbedForResult ( 'Your updated response' , SUCCESS_COLOR , result . new ) ]
} ) ;
break ;
}
}
private async onDelete ( ctx : CommandContext ) : Promise < void > {
const setId = ctx . guildID ? ? ctx . user . id ;
const table = ctx . options [ 'delete' ] [ 'table' ] ;
const text = ctx . options [ 'delete' ] [ 'text' ] ;
const result = await this . db . deleteResponseFromDiscord ( table , text , setId ) ;
switch ( result . status ) {
case 'nonexistent' :
await ctx . send ( generateErrorMessageFor ( {
error : ` couldn't find a response with that text ` ,
context : ` remove the response with the text ${ text } from the ${ table } table `
} ) ) ;
break ;
case 'noneditable' :
await ctx . send ( {
embeds : [ generateEmbedForResult (
` A non-editable response (still existing) ` ,
FAILURE_COLOR ,
result . old ) ] ,
ephemeral : true
} ) ;
break ;
case 'deleted' :
await ctx . send ( {
embeds : [ generateEmbedForResult (
` The response you deleted ` ,
SUCCESS_COLOR ,
result . old ) ]
} ) ;
break ;
}
}
}
export class GenerateCommand extends SlashCommand {
private readonly db : Database ;
constructor ( creator : SlashCreator , db : Database , 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 state = await this . db . generateFromDiscordSet ( ctx . guildID ? ? ctx . user . id ) ;
await ctx . send ( generateMessageFor ( state ) ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'generate a scenario' } ) ) ;
}
}
async onSelect ( ctx : ComponentContext ) : Promise < void > {
try {
const oldEmbed = getEmbedFrom ( ctx . message ) ;
const oldContents = loadEmbed ( oldEmbed , false ) ;
const newContents = {
. . . oldContents ,
selected : new Set ( ctx . values )
} ;
const final = await this . db . expandFromDiscordSet ( ctx . guildID ? ? ctx . user . id , newContents ) ;
await ctx . editParent ( generateMessageFor ( final ) ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'change the selected components' } ) ) ;
}
}
async onDone ( ctx : ComponentContext ) : Promise < void > {
try {
const embed = getEmbedFrom ( ctx . message ) ;
const finalContents = loadEmbed ( embed , false ) ;
const finalState = await this . db . finalizeFromDiscordSet ( ctx . guildID ? ? ctx . user . id , finalContents ) ;
await ctx . editParent ( generateMessageFor ( finalState ) ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'finish this scenario' } ) ) ;
}
}
async onReroll ( ctx : ComponentContext ) : Promise < void > {
try {
const embed = getEmbedFrom ( ctx . message ) ;
const oldContents = loadEmbed ( embed , false ) ;
const nextState = await this . db . rerollFromDiscordSet ( ctx . guildID ? ? ctx . user . id , oldContents ) ;
await ctx . editParent ( generateMessageFor ( nextState ) ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'reroll this scenario' } ) ) ;
}
}
async onDelete ( ctx : ComponentContext ) : Promise < void > {
try {
await ctx . delete ( ctx . messageID ) ;
} catch ( error ) {
await ctx . send ( generateErrorMessageFor ( { error , context : 'delete this scenario' } ) ) ;
}
}
}