[nextgen/cardstack] add cardstack cloning

This commit is contained in:
bignutty 2025-02-22 13:49:30 +01:00
parent 2e523b1dad
commit e2075b8306

View file

@ -6,7 +6,7 @@ const {editOrReply} = require("#utils/message");
const {STATIC_ASSETS} = require("#utils/statics"); const {STATIC_ASSETS} = require("#utils/statics");
const {Context} = require("detritus-client/lib/command"); 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 {Message} = require("detritus-client/lib/structures");
const {ComponentContext, Components, ComponentActionRow, ComponentButton} = require("detritus-client/lib/utils"); 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.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 {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 <index>/<total>` * @param {Function} options.pageNumberGenerator Function that renders a page number. Default style is `Page <index>/<total>`
* @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) { constructor(context, options) {
this.context = context; this.context = context;
this.options = options;
this.cards = options.cards || []; this.cards = options.cards || [];
this.buttons = options.buttons || ["previous", "next"] this.buttons = options.buttons || ["previous", "next"]
@ -56,6 +59,8 @@ class DynamicCardStack {
this.expires = options.expires || 5 * 60 * 1000; this.expires = options.expires || 5 * 60 * 1000;
this.pageNumbers = options.pageNumbers || true; this.pageNumbers = options.pageNumbers || true;
this.pageNumberGenerator = options.pageNumberGenerator || ((pg) => `Page ${pg.index + 1}/${pg.activeCardStack.length}`); 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; this.rootIndex = this.index;
@ -137,7 +142,7 @@ class DynamicCardStack {
* Creates a new cardstack in the given channel * Creates a new cardstack in the given channel
* @private * @private
*/ */
_spawn() { _spawn(createMessage = true) {
this._createDynamicCardStack(this.context.client); this._createDynamicCardStack(this.context.client);
this.activeCardStack = [...this.cards]; this.activeCardStack = [...this.cards];
@ -158,10 +163,12 @@ class DynamicCardStack {
//this.spawned = Date.now() //this.spawned = Date.now()
return this._edit({ if (createMessage) return this._edit({
...this.getCurrentCard(), ...this.getCurrentCard(),
components: this.listener 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; return card;
} catch (e) { } catch (e) {
console.error("Card rendering failed:") console.error("Card rendering failed:")
@ -246,7 +256,7 @@ class DynamicCardStack {
* Decreases the index and returns the next card from the stack. * Decreases the index and returns the next card from the stack.
* @returns {Message} Card * @returns {Message} Card
*/ */
previousPage() { previousCard() {
this.index = this.index - 1; this.index = this.index - 1;
if (this.index < 0) { if (this.index < 0) {
if (this.loopPages) this.index = this.activeCardStack.length - 1; if (this.loopPages) this.index = this.activeCardStack.length - 1;
@ -267,7 +277,7 @@ class DynamicCardStack {
let message = Object.assign({}, cardContent); let message = Object.assign({}, cardContent);
if (!components) { if (!components) {
this.listener.components = this._renderComponents(); this._renderComponents();
message.components = this.listener; message.components = this.listener;
} else { } else {
message.components = components; message.components = components;
@ -279,7 +289,9 @@ class DynamicCardStack {
return editOrReply(this.context, { return editOrReply(this.context, {
...message, ...message,
reference: true, reference: true,
allowedMentions: {parse: [], repliedUser: false} allowedMentions: {parse: [], repliedUser: false},
// TODO: allow supplying flags
flags: this.ephemeral ? MessageFlags.EPHEMERAL : 0
}) })
} catch (e) { } catch (e) {
console.error("Message editing failed:") console.error("Message editing failed:")
@ -440,7 +452,9 @@ class DynamicCardStack {
} }
if (renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.") if (renderedSlots.length > 5) console.warn("Component Overflow - Limiting to 5.")
return renderedSlots.splice(0, 5); let compListener = this.listener;
compListener.components = renderedSlots.splice(0, 5)
return compListener;
} }
/** /**
@ -500,9 +514,44 @@ class DynamicCardStack {
* @private * @private
*/ */
async _handleInteraction(ctx) { async _handleInteraction(ctx) {
if (ctx.user.id !== this.context.user.id) return ctx.respond({ if (ctx.user.id !== this.context.user.id) {
type: InteractionCallbackTypes.DEFERRED_UPDATE_MESSAGE 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(); //this.lastInteraction = Date.now();
@ -510,15 +559,9 @@ class DynamicCardStack {
if (Object.values(BuiltInButtonTypes).includes(ctx.data.customId)) { if (Object.values(BuiltInButtonTypes).includes(ctx.data.customId)) {
switch (ctx.data.customId) { switch (ctx.data.customId) {
case "next": case "next":
return ctx.respond({ return ctx.editOrRespond(this.nextCard())
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: this.nextCard()
})
case "previous": case "previous":
return ctx.respond({ return ctx.editOrRespond(this.previousCard())
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: this.previousPage()
})
default: default:
console.error("unknown button??") console.error("unknown button??")
} }
@ -537,10 +580,7 @@ class DynamicCardStack {
this.index = this.rootIndex; this.index = this.rootIndex;
this.currentSelectedSubcategory = null; this.currentSelectedSubcategory = null;
return await ctx.respond({ return await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}));
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)})
})
} else this.currentSelectedSubcategory = cid; } else this.currentSelectedSubcategory = cid;
let resolveTime = Date.now(); let resolveTime = Date.now();
@ -549,10 +589,7 @@ class DynamicCardStack {
// If we have a cached result, retrieve it // If we have a cached result, retrieve it
if (this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS) !== null) { if (this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS) !== null) {
this.activeCardStack = [...this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS)]; this.activeCardStack = [...this._getCachedValue(this.rootIndex, cid, STACK_CACHE_KEYS.RESULT_CARDS)];
await ctx.respond({ await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}));
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents(false)})
})
return; return;
} else { } else {
// Controls if we should display a loading (skeleton) embed while the // Controls if we should display a loading (skeleton) embed while the
@ -574,10 +611,7 @@ class DynamicCardStack {
if (component.renderLoadingState) if (component.renderLoadingState)
processingEmbed = page(component.renderLoadingState(this, component)); processingEmbed = page(component.renderLoadingState(this, component));
await ctx.respond({ await ctx.editOrRespond(Object.assign(processingEmbed, {components: this._renderComponents(true)}))
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(processingEmbed, {components: this._renderComponents(true)})
})
} }
// Compute the active cardstack. // Compute the active cardstack.
@ -661,10 +695,7 @@ class DynamicCardStack {
// Update the card stack with a card from the new stack. // Update the card stack with a card from the new stack.
if (component.instantResult) { if (component.instantResult) {
await ctx.respond({ await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
type: InteractionCallbackTypes.UPDATE_MESSAGE,
data: Object.assign(this.getCurrentCard(), {components: this._renderComponents()})
})
} else { } else {
// This timeout exists 1. for cosmetic reasons so people can // This timeout exists 1. for cosmetic reasons so people can
// see the skeleton state and 2. in order to avoid a really // 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. // it *should* be safe to just edit the message now.
if ((Date.now() - resolveTime) < 2000) { if ((Date.now() - resolveTime) < 2000) {
setTimeout(() => { setTimeout(() => {
return this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) return ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
}, 1500) }, 1500)
} else { } else {
await this._edit(Object.assign(this.getCurrentCard(), {components: this._renderComponents()})) await ctx.editOrRespond(Object.assign(this.getCurrentCard(), {components: this._renderComponents()}))
} }
} }
return; return;
} }