diff --git a/labscore/cardstack/stack.js b/labscore/cardstack/stack.js index dd0ed42..6c61445 100644 --- a/labscore/cardstack/stack.js +++ b/labscore/cardstack/stack.js @@ -6,7 +6,7 @@ const {editOrReply} = require("#utils/message"); const {STATIC_ASSETS} = require("#utils/statics"); const {Context} = require("detritus-client/lib/command"); -const {MessageComponentTypes, InteractionCallbackTypes} = require("detritus-client/lib/constants"); +const {MessageComponentTypes, InteractionCallbackTypes, MessageFlags} = require("detritus-client/lib/constants"); const {Message} = require("detritus-client/lib/structures"); const {ComponentContext, Components, ComponentActionRow, ComponentButton} = require("detritus-client/lib/utils"); @@ -44,9 +44,12 @@ class DynamicCardStack { * @param {boolean} options.disableStackCache Allows disabling the stack result cache, meaning that every trigger will reevaluate a stack * @param {boolean} options.pageNumbers Renders Page Numbers in the footer of all embeds in cards. * @param {Function} options.pageNumberGenerator Function that renders a page number. Default style is `Page /` + * @param {boolean} options.disableCloning Disables cloning a card stack when someone other than the author interacts with it. + * @param {boolean} options.ephemeral Makes the response ephemeral (primarily used by cloning) */ constructor(context, options) { this.context = context; + this.options = options; this.cards = options.cards || []; this.buttons = options.buttons || ["previous", "next"] @@ -56,6 +59,8 @@ class DynamicCardStack { this.expires = options.expires || 5 * 60 * 1000; this.pageNumbers = options.pageNumbers || true; this.pageNumberGenerator = options.pageNumberGenerator || ((pg) => `Page ${pg.index + 1}/${pg.activeCardStack.length}`); + this.disableCloning = options.disableCloning || false; + this.ephemeral = options.ephemeral || false; this.rootIndex = this.index; @@ -137,7 +142,7 @@ class DynamicCardStack { * Creates a new cardstack in the given channel * @private */ - _spawn() { + _spawn(createMessage = true) { this._createDynamicCardStack(this.context.client); this.activeCardStack = [...this.cards]; @@ -158,10 +163,12 @@ class DynamicCardStack { //this.spawned = Date.now() - return this._edit({ + if (createMessage) return this._edit({ ...this.getCurrentCard(), components: this.listener }); + + return this; } /** @@ -217,6 +224,9 @@ class DynamicCardStack { }) } + // TODO: ensure flags don't get overwritten/allow supplying custom flags + if (this.ephemeral) card.flags = MessageFlags.EPHEMERAL; + return card; } catch (e) { console.error("Card rendering failed:") @@ -246,7 +256,7 @@ class DynamicCardStack { * Decreases the index and returns the next card from the stack. * @returns {Message} Card */ - previousPage() { + previousCard() { this.index = this.index - 1; if (this.index < 0) { if (this.loopPages) this.index = this.activeCardStack.length - 1; @@ -267,7 +277,7 @@ class DynamicCardStack { let message = Object.assign({}, cardContent); if (!components) { - this.listener.components = this._renderComponents(); + this._renderComponents(); message.components = this.listener; } else { message.components = components; @@ -279,7 +289,9 @@ class DynamicCardStack { return editOrReply(this.context, { ...message, reference: true, - allowedMentions: {parse: [], repliedUser: false} + allowedMentions: {parse: [], repliedUser: false}, + // TODO: allow supplying flags + flags: this.ephemeral ? MessageFlags.EPHEMERAL : 0 }) } catch (e) { console.error("Message editing failed:") @@ -378,7 +390,7 @@ class DynamicCardStack { // We currently support up to 5 "slots" (action rows), // although the amount you can actually use depends // on how many components are added to each slot. - let componentSlots = [[],[],[],[],[]]; + let componentSlots = [[], [], [], [], []]; // First Row always starts with built-in components for (const b of this.buttons) { @@ -419,28 +431,30 @@ class DynamicCardStack { let renderedSlots = []; // Render slots - for(const components of componentSlots) { - if(components.length === 0) continue; + for (const components of componentSlots) { + if (components.length === 0) continue; let row = new ComponentActionRow({}); // Slot all components into their respective rows. - while(components.length > 0) { + while (components.length > 0) { row.addButton(components.shift()); // Create a new row for content to overflow in. - if(row.isFull){ + if (row.isFull) { renderedSlots.push(row) row = new ComponentActionRow({}); } } // Push rendered row to stack if there are components in it. - if(!row.isEmpty) renderedSlots.push(row); + if (!row.isEmpty) renderedSlots.push(row); } - if(renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.") - return renderedSlots.splice(0, 5); + if (renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.") + let compListener = this.listener; + compListener.components = renderedSlots.splice(0, 5) + return compListener; } /** @@ -500,9 +514,44 @@ class DynamicCardStack { * @private */ async _handleInteraction(ctx) { - if (ctx.user.id !== this.context.user.id) return ctx.respond({ - type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE - }) + if (ctx.user.id !== this.context.user.id) { + if (this.disableCloning) return ctx.respond({type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE}); + + /** + * Card Stack Cloning + * + * This clones the card stack in its current state, calls + * the internal spawn function to "respawn" it under a new + * context, then executes the triggered interaction via + * the new "cloned" cardstack. + * + * This is (maybe?) kind of jank, but I can't think of any + * better ways to ensure state and content consistency + * without it affecting the parent cardstack somehow. + */ + + // New message that the new cardstack will attach to. + await ctx.respond(InteractionCallbackTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, {flags: MessageFlags.EPHEMERAL}); + + let newStack = Object.assign(Object.create(Object.getPrototypeOf(this)), this) + + // Reassign the context + newStack.context = ctx; + + // Ensure all state is properly cloned to the new stack + newStack.index = this.index; + newStack.rootIndex = this.rootIndex; + newStack.currentSelectedSubcategory = this.currentSelectedSubcategory; + + newStack.cards = structuredClone(this.cards); + newStack.activeCardStack = structuredClone(this.activeCardStack); + newStack.currentComponentsBatch = structuredClone(this.currentComponentsBatch); + + // Respawn and re-run interaction. + await newStack._spawn(false); + await newStack._handleInteraction(ctx); + return; + } //this.lastInteraction = Date.now(); @@ -510,15 +559,9 @@ class DynamicCardStack { if (Object.values(BuiltInButtonTypes).includes(ctx.data.customId)) { switch (ctx.data.customId) { case "next": - return ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: this.nextCard() - }) + return ctx.editOrRespond(this.nextCard()) case "previous": - return ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: this.previousPage() - }) + return ctx.editOrRespond(this.previousCard()) default: console.error("unknown button??") } @@ -537,10 +580,7 @@ class DynamicCardStack { this.index = this.rootIndex; this.currentSelectedSubcategory = null; - return await ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)}) - }) + return await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})); } else this.currentSelectedSubcategory = cid; let resolveTime = Date.now(); @@ -549,10 +589,7 @@ class DynamicCardStack { // If we have a cached result, retrieve it if (this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS) !== null) { this.activeCardStack = [...this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS)]; - await ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)}) - }) + await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})); return; } else { // Controls if we should display a loading (skeleton) embed while the @@ -574,10 +611,7 @@ class DynamicCardStack { if (component.renderLoadingState) processingEmbed = page(component.renderLoadingState(this, component)); - await ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: Object.assign(processingEmbed, {components: this._renderComponents(true)}) - }) + await ctx.editOrRespond(Object.assign(processingEmbed, {components: this._renderComponents(true)})) } // Compute the active cardstack. @@ -661,10 +695,7 @@ class DynamicCardStack { // Update the card stack with a card from the new stack. if (component.instantResult) { - await ctx.respond({ - type: InteractionCallbackTypes.UPDATE_MESSAGE, - data: Object.assign(this.getCurrentCard(), {components: this._renderComponents()}) - }) + await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) } else { // This timeout exists 1. for cosmetic reasons so people can // see the skeleton state and 2. in order to avoid a really @@ -675,13 +706,12 @@ class DynamicCardStack { // it *should* be safe to just edit the message now. if ((Date.now() - resolveTime) < 2000) { setTimeout(() => { - return this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) + return ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) }, 1500) } else { - await this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) + await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) } } - return; }