mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:00:44 +00:00
discord: persist component registries best-effort (#75584)
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
|
||||
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
|
||||
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
|
||||
- Discord: keep active buttons, selects, and forms working across Gateway restarts until they expire, so multi-step Discord interactions are less likely to break during upgrades or restarts. Thanks @amknight.
|
||||
- Messages/docs: clarify that `BodyForAgent` is the primary inbound model text while `Body` is the legacy envelope fallback, and add Signal coverage so channel hardening patches target the real prompt path. Refs #66198. Thanks @defonota3box.
|
||||
- Slack: keep track of bot-participated threads across restarts, so ongoing threaded conversations can continue auto-replying after the Gateway is restarted. Thanks @amknight.
|
||||
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import { resolveGlobalMap } from "openclaw/plugin-sdk/global-singleton";
|
||||
import type { DiscordComponentEntry, DiscordModalEntry } from "./components.js";
|
||||
import { getOptionalDiscordRuntime } from "./runtime.js";
|
||||
|
||||
const DEFAULT_COMPONENT_TTL_MS = 30 * 60 * 1000;
|
||||
const PERSISTENT_COMPONENT_NAMESPACE = "discord.components";
|
||||
const PERSISTENT_MODAL_NAMESPACE = "discord.modals";
|
||||
const PERSISTENT_COMPONENT_MAX_ENTRIES = 500;
|
||||
const PERSISTENT_MODAL_MAX_ENTRIES = 500;
|
||||
const DISCORD_COMPONENT_ENTRIES_KEY = Symbol.for("openclaw.discord.componentEntries");
|
||||
const DISCORD_MODAL_ENTRIES_KEY = Symbol.for("openclaw.discord.modalEntries");
|
||||
|
||||
type PersistedDiscordRegistryEntry<T extends { id: string }> = {
|
||||
version: 1;
|
||||
entry: T;
|
||||
};
|
||||
|
||||
type DiscordPersistentStore<T> = {
|
||||
register(key: string, value: T, opts?: { ttlMs?: number }): Promise<void>;
|
||||
lookup(key: string): Promise<T | undefined>;
|
||||
consume(key: string): Promise<T | undefined>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
};
|
||||
|
||||
type DiscordRegistryStore<T extends { id: string }> = DiscordPersistentStore<
|
||||
PersistedDiscordRegistryEntry<T>
|
||||
>;
|
||||
|
||||
let componentEntries: Map<string, DiscordComponentEntry> | undefined;
|
||||
let modalEntries: Map<string, DiscordModalEntry> | undefined;
|
||||
let persistentComponentStore: DiscordRegistryStore<DiscordComponentEntry> | undefined;
|
||||
let persistentModalStore: DiscordRegistryStore<DiscordModalEntry> | undefined;
|
||||
let persistentRegistryDisabled = false;
|
||||
|
||||
function getComponentEntries(): Map<string, DiscordComponentEntry> {
|
||||
componentEntries ??= resolveGlobalMap<string, DiscordComponentEntry>(
|
||||
@@ -20,6 +44,75 @@ function getModalEntries(): Map<string, DiscordModalEntry> {
|
||||
return modalEntries;
|
||||
}
|
||||
|
||||
function reportPersistentComponentRegistryError(error: unknown): void {
|
||||
try {
|
||||
getOptionalDiscordRuntime()
|
||||
?.logging.getChildLogger({ plugin: "discord", feature: "component-registry-state" })
|
||||
.warn("Discord persistent component registry state failed", { error: String(error) });
|
||||
} catch {
|
||||
// Best effort only: persistent state must never break Discord interactions.
|
||||
}
|
||||
}
|
||||
|
||||
function disablePersistentComponentRegistry(error: unknown): void {
|
||||
persistentRegistryDisabled = true;
|
||||
persistentComponentStore = undefined;
|
||||
persistentModalStore = undefined;
|
||||
reportPersistentComponentRegistryError(error);
|
||||
}
|
||||
|
||||
function getPersistentComponentStore(): DiscordRegistryStore<DiscordComponentEntry> | undefined {
|
||||
if (persistentRegistryDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
if (persistentComponentStore) {
|
||||
return persistentComponentStore;
|
||||
}
|
||||
const runtime = getOptionalDiscordRuntime();
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
persistentComponentStore = runtime.state.openKeyedStore<
|
||||
PersistedDiscordRegistryEntry<DiscordComponentEntry>
|
||||
>({
|
||||
namespace: PERSISTENT_COMPONENT_NAMESPACE,
|
||||
maxEntries: PERSISTENT_COMPONENT_MAX_ENTRIES,
|
||||
defaultTtlMs: DEFAULT_COMPONENT_TTL_MS,
|
||||
});
|
||||
return persistentComponentStore;
|
||||
} catch (error) {
|
||||
disablePersistentComponentRegistry(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getPersistentModalStore(): DiscordRegistryStore<DiscordModalEntry> | undefined {
|
||||
if (persistentRegistryDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
if (persistentModalStore) {
|
||||
return persistentModalStore;
|
||||
}
|
||||
const runtime = getOptionalDiscordRuntime();
|
||||
if (!runtime) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
persistentModalStore = runtime.state.openKeyedStore<
|
||||
PersistedDiscordRegistryEntry<DiscordModalEntry>
|
||||
>({
|
||||
namespace: PERSISTENT_MODAL_NAMESPACE,
|
||||
maxEntries: PERSISTENT_MODAL_MAX_ENTRIES,
|
||||
defaultTtlMs: DEFAULT_COMPONENT_TTL_MS,
|
||||
});
|
||||
return persistentModalStore;
|
||||
} catch (error) {
|
||||
disablePersistentComponentRegistry(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isExpired(entry: { expiresAt?: number }, now: number) {
|
||||
return typeof entry.expiresAt === "number" && entry.expiresAt <= now;
|
||||
}
|
||||
@@ -40,7 +133,8 @@ function registerEntries<
|
||||
entries: T[],
|
||||
store: Map<string, T>,
|
||||
params: { now: number; ttlMs: number; messageId?: string },
|
||||
): void {
|
||||
): T[] {
|
||||
const normalizedEntries: T[] = [];
|
||||
for (const entry of entries) {
|
||||
const normalized = normalizeEntryTimestamps(
|
||||
{ ...entry, messageId: params.messageId ?? entry.messageId },
|
||||
@@ -48,7 +142,9 @@ function registerEntries<
|
||||
params.ttlMs,
|
||||
);
|
||||
store.set(entry.id, normalized);
|
||||
normalizedEntries.push(normalized);
|
||||
}
|
||||
return normalizedEntries;
|
||||
}
|
||||
|
||||
function resolveEntry<T extends { expiresAt?: number }>(
|
||||
@@ -70,6 +166,81 @@ function resolveEntry<T extends { expiresAt?: number }>(
|
||||
return entry;
|
||||
}
|
||||
|
||||
function readPersistedRegistryEntry<T extends { id: string }>(
|
||||
persisted: PersistedDiscordRegistryEntry<T> | undefined,
|
||||
): T | null {
|
||||
if (persisted?.version !== 1 || typeof persisted.entry?.id !== "string") {
|
||||
return null;
|
||||
}
|
||||
return persisted.entry;
|
||||
}
|
||||
|
||||
function registerPersistentRegistryEntries<T extends { id: string }>(params: {
|
||||
entries: T[];
|
||||
ttlMs: number;
|
||||
openStore: () => DiscordRegistryStore<T> | undefined;
|
||||
}): void {
|
||||
if (params.entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
const store = params.openStore();
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
for (const entry of params.entries) {
|
||||
void store
|
||||
.register(entry.id, { version: 1, entry }, { ttlMs: params.ttlMs })
|
||||
.catch(disablePersistentComponentRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
function registerPersistentEntries(params: {
|
||||
entries: DiscordComponentEntry[];
|
||||
modals: DiscordModalEntry[];
|
||||
ttlMs: number;
|
||||
}): void {
|
||||
registerPersistentRegistryEntries({
|
||||
entries: params.entries,
|
||||
ttlMs: params.ttlMs,
|
||||
openStore: getPersistentComponentStore,
|
||||
});
|
||||
registerPersistentRegistryEntries({
|
||||
entries: params.modals,
|
||||
ttlMs: params.ttlMs,
|
||||
openStore: getPersistentModalStore,
|
||||
});
|
||||
}
|
||||
|
||||
function deletePersistentEntry<T extends { id: string }>(params: {
|
||||
id: string;
|
||||
openStore: () => DiscordRegistryStore<T> | undefined;
|
||||
}): void {
|
||||
const store = params.openStore();
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
void store.delete(params.id).catch(disablePersistentComponentRegistry);
|
||||
}
|
||||
|
||||
async function resolvePersistentRegistryEntry<T extends { id: string }>(params: {
|
||||
id: string;
|
||||
consume?: boolean;
|
||||
openStore: () => DiscordRegistryStore<T> | undefined;
|
||||
}): Promise<T | null> {
|
||||
const store = params.openStore();
|
||||
if (!store) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const value =
|
||||
params.consume === false ? await store.lookup(params.id) : await store.consume(params.id);
|
||||
return readPersistedRegistryEntry(value);
|
||||
} catch (error) {
|
||||
disablePersistentComponentRegistry(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerDiscordComponentEntries(params: {
|
||||
entries: DiscordComponentEntry[];
|
||||
modals: DiscordModalEntry[];
|
||||
@@ -78,12 +249,21 @@ export function registerDiscordComponentEntries(params: {
|
||||
}): void {
|
||||
const now = Date.now();
|
||||
const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
|
||||
registerEntries(params.entries, getComponentEntries(), {
|
||||
const normalizedEntries = registerEntries(params.entries, getComponentEntries(), {
|
||||
now,
|
||||
ttlMs,
|
||||
messageId: params.messageId,
|
||||
});
|
||||
registerEntries(params.modals, getModalEntries(), { now, ttlMs, messageId: params.messageId });
|
||||
const normalizedModals = registerEntries(params.modals, getModalEntries(), {
|
||||
now,
|
||||
ttlMs,
|
||||
messageId: params.messageId,
|
||||
});
|
||||
registerPersistentEntries({
|
||||
entries: normalizedEntries,
|
||||
modals: normalizedModals,
|
||||
ttlMs,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDiscordComponentEntry(params: {
|
||||
@@ -93,6 +273,23 @@ export function resolveDiscordComponentEntry(params: {
|
||||
return resolveEntry(getComponentEntries(), params);
|
||||
}
|
||||
|
||||
export async function resolveDiscordComponentEntryWithPersistence(params: {
|
||||
id: string;
|
||||
consume?: boolean;
|
||||
}): Promise<DiscordComponentEntry | null> {
|
||||
const inMemory = resolveDiscordComponentEntry(params);
|
||||
if (inMemory) {
|
||||
if (params.consume !== false) {
|
||||
deletePersistentEntry({ ...params, openStore: getPersistentComponentStore });
|
||||
}
|
||||
return inMemory;
|
||||
}
|
||||
return await resolvePersistentRegistryEntry({
|
||||
...params,
|
||||
openStore: getPersistentComponentStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDiscordModalEntry(params: {
|
||||
id: string;
|
||||
consume?: boolean;
|
||||
@@ -100,7 +297,27 @@ export function resolveDiscordModalEntry(params: {
|
||||
return resolveEntry(getModalEntries(), params);
|
||||
}
|
||||
|
||||
export async function resolveDiscordModalEntryWithPersistence(params: {
|
||||
id: string;
|
||||
consume?: boolean;
|
||||
}): Promise<DiscordModalEntry | null> {
|
||||
const inMemory = resolveDiscordModalEntry(params);
|
||||
if (inMemory) {
|
||||
if (params.consume !== false) {
|
||||
deletePersistentEntry({ ...params, openStore: getPersistentModalStore });
|
||||
}
|
||||
return inMemory;
|
||||
}
|
||||
return await resolvePersistentRegistryEntry({
|
||||
...params,
|
||||
openStore: getPersistentModalStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearDiscordComponentEntries(): void {
|
||||
getComponentEntries().clear();
|
||||
getModalEntries().clear();
|
||||
persistentComponentStore = undefined;
|
||||
persistentModalStore = undefined;
|
||||
persistentRegistryDisabled = false;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { MessageFlags } from "discord-api-types/v10";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let clearDiscordComponentEntries: typeof import("./components-registry.js").clearDiscordComponentEntries;
|
||||
let registerDiscordComponentEntries: typeof import("./components-registry.js").registerDiscordComponentEntries;
|
||||
let resolveDiscordComponentEntry: typeof import("./components-registry.js").resolveDiscordComponentEntry;
|
||||
let resolveDiscordComponentEntryWithPersistence: typeof import("./components-registry.js").resolveDiscordComponentEntryWithPersistence;
|
||||
let resolveDiscordModalEntry: typeof import("./components-registry.js").resolveDiscordModalEntry;
|
||||
let resolveDiscordModalEntryWithPersistence: typeof import("./components-registry.js").resolveDiscordModalEntryWithPersistence;
|
||||
let buildDiscordComponentMessage: typeof import("./components.js").buildDiscordComponentMessage;
|
||||
let buildDiscordComponentMessageFlags: typeof import("./components.js").buildDiscordComponentMessageFlags;
|
||||
let readDiscordComponentSpec: typeof import("./components.js").readDiscordComponentSpec;
|
||||
@@ -14,7 +16,9 @@ beforeAll(async () => {
|
||||
clearDiscordComponentEntries,
|
||||
registerDiscordComponentEntries,
|
||||
resolveDiscordComponentEntry,
|
||||
resolveDiscordComponentEntryWithPersistence,
|
||||
resolveDiscordModalEntry,
|
||||
resolveDiscordModalEntryWithPersistence,
|
||||
} = await import("./components-registry.js"));
|
||||
({ buildDiscordComponentMessage, buildDiscordComponentMessageFlags, readDiscordComponentSpec } =
|
||||
await import("./components.js"));
|
||||
@@ -84,6 +88,7 @@ describe("discord components", () => {
|
||||
describe("discord component registry", () => {
|
||||
beforeEach(() => {
|
||||
clearDiscordComponentEntries();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const componentsRegistryModuleUrl = new URL("./components-registry.ts", import.meta.url).href;
|
||||
@@ -136,4 +141,94 @@ describe("discord component registry", () => {
|
||||
|
||||
second.clearDiscordComponentEntries();
|
||||
});
|
||||
|
||||
it("persists component and modal entries when runtime state is available", async () => {
|
||||
const componentRegister = vi.fn().mockResolvedValue(undefined);
|
||||
const modalRegister = vi.fn().mockResolvedValue(undefined);
|
||||
const componentLookup = vi.fn().mockResolvedValue({
|
||||
version: 1,
|
||||
entry: { id: "btn_persisted", kind: "button", label: "Persisted" },
|
||||
});
|
||||
const modalLookup = vi.fn().mockResolvedValue({
|
||||
version: 1,
|
||||
entry: { id: "mdl_persisted", title: "Persisted", fields: [] },
|
||||
});
|
||||
const componentStore = {
|
||||
register: componentRegister,
|
||||
lookup: componentLookup,
|
||||
consume: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
entries: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
const modalStore = {
|
||||
register: modalRegister,
|
||||
lookup: modalLookup,
|
||||
consume: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
entries: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
const openKeyedStore = vi.fn((opts: { namespace: string }) =>
|
||||
opts.namespace === "discord.components" ? componentStore : modalStore,
|
||||
);
|
||||
const { setDiscordRuntime } = await import("./runtime.js");
|
||||
setDiscordRuntime({
|
||||
state: { openKeyedStore },
|
||||
logging: { getChildLogger: () => ({ warn: vi.fn() }) },
|
||||
} as never);
|
||||
|
||||
registerDiscordComponentEntries({
|
||||
entries: [{ id: "btn_1", kind: "button", label: "Confirm" }],
|
||||
modals: [{ id: "mdl_1", title: "Details", fields: [] }],
|
||||
ttlMs: 1000,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(componentRegister).toHaveBeenCalledTimes(1));
|
||||
expect(componentRegister).toHaveBeenCalledWith(
|
||||
"btn_1",
|
||||
{ version: 1, entry: expect.objectContaining({ id: "btn_1" }) },
|
||||
{ ttlMs: 1000 },
|
||||
);
|
||||
expect(modalRegister).toHaveBeenCalledWith(
|
||||
"mdl_1",
|
||||
{ version: 1, entry: expect.objectContaining({ id: "mdl_1" }) },
|
||||
{ ttlMs: 1000 },
|
||||
);
|
||||
|
||||
clearDiscordComponentEntries();
|
||||
await expect(
|
||||
resolveDiscordComponentEntryWithPersistence({ id: "btn_persisted", consume: false }),
|
||||
).resolves.toMatchObject({ id: "btn_persisted" });
|
||||
await expect(
|
||||
resolveDiscordModalEntryWithPersistence({ id: "mdl_persisted", consume: false }),
|
||||
).resolves.toMatchObject({ id: "mdl_persisted" });
|
||||
expect(componentLookup).toHaveBeenCalledWith("btn_persisted");
|
||||
expect(modalLookup).toHaveBeenCalledWith("mdl_persisted");
|
||||
expect(openKeyedStore).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("falls back to the in-memory registry when persistent state cannot open", async () => {
|
||||
const warn = vi.fn();
|
||||
const { setDiscordRuntime } = await import("./runtime.js");
|
||||
setDiscordRuntime({
|
||||
state: {
|
||||
openKeyedStore: vi.fn(() => {
|
||||
throw new Error("sqlite unavailable");
|
||||
}),
|
||||
},
|
||||
logging: { getChildLogger: () => ({ warn }) },
|
||||
} as never);
|
||||
|
||||
registerDiscordComponentEntries({
|
||||
entries: [{ id: "btn_fallback", kind: "button", label: "Fallback" }],
|
||||
modals: [],
|
||||
});
|
||||
|
||||
expect(resolveDiscordComponentEntry({ id: "btn_fallback", consume: false })).toMatchObject({
|
||||
id: "btn_fallback",
|
||||
label: "Fallback",
|
||||
});
|
||||
expect(warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { logError } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDiscordComponentEntry, resolveDiscordModalEntry } from "../components-registry.js";
|
||||
import {
|
||||
resolveDiscordComponentEntryWithPersistence,
|
||||
resolveDiscordModalEntryWithPersistence,
|
||||
} from "../components-registry.js";
|
||||
import type { ButtonInteraction, ComponentData } from "../internal/discord.js";
|
||||
import {
|
||||
type AgentComponentContext,
|
||||
@@ -46,7 +49,10 @@ async function handleDiscordComponentEvent(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = resolveDiscordComponentEntry({ id: parsed.componentId, consume: false });
|
||||
const entry = await resolveDiscordComponentEntryWithPersistence({
|
||||
id: parsed.componentId,
|
||||
consume: false,
|
||||
});
|
||||
if (!entry) {
|
||||
try {
|
||||
await params.interaction.reply({
|
||||
@@ -93,7 +99,7 @@ async function handleDiscordComponentEvent(params: {
|
||||
if (!componentAllowed) {
|
||||
return;
|
||||
}
|
||||
const consumed = resolveDiscordComponentEntry({
|
||||
const consumed = await resolveDiscordComponentEntryWithPersistence({
|
||||
id: parsed.componentId,
|
||||
consume: !entry.reusable,
|
||||
});
|
||||
@@ -193,7 +199,10 @@ async function handleDiscordModalTrigger(params: {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const entry = resolveDiscordComponentEntry({ id: parsed.componentId, consume: false });
|
||||
const entry = await resolveDiscordComponentEntryWithPersistence({
|
||||
id: parsed.componentId,
|
||||
consume: false,
|
||||
});
|
||||
if (!entry || entry.kind !== "modal-trigger") {
|
||||
try {
|
||||
await params.interaction.reply({
|
||||
@@ -246,7 +255,7 @@ async function handleDiscordModalTrigger(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const consumed = resolveDiscordComponentEntry({
|
||||
const consumed = await resolveDiscordComponentEntryWithPersistence({
|
||||
id: parsed.componentId,
|
||||
consume: !entry.reusable,
|
||||
});
|
||||
@@ -263,7 +272,10 @@ async function handleDiscordModalTrigger(params: {
|
||||
}
|
||||
|
||||
const resolvedModalId = consumed.modalId ?? modalId;
|
||||
const modalEntry = resolveDiscordModalEntry({ id: resolvedModalId, consume: false });
|
||||
const modalEntry = await resolveDiscordModalEntryWithPersistence({
|
||||
id: resolvedModalId,
|
||||
consume: false,
|
||||
});
|
||||
if (!modalEntry) {
|
||||
try {
|
||||
await params.interaction.reply({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logError } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { parseDiscordModalCustomIdForInteraction } from "../component-custom-id.js";
|
||||
import { resolveDiscordModalEntry } from "../components-registry.js";
|
||||
import { resolveDiscordModalEntryWithPersistence } from "../components-registry.js";
|
||||
import { Modal, type ComponentData, type ModalInteraction } from "../internal/discord.js";
|
||||
import {
|
||||
type AgentComponentContext,
|
||||
@@ -41,7 +41,10 @@ export class DiscordComponentModal extends Modal {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalEntry = resolveDiscordModalEntry({ id: modalId, consume: false });
|
||||
const modalEntry = await resolveDiscordModalEntryWithPersistence({
|
||||
id: modalId,
|
||||
consume: false,
|
||||
});
|
||||
if (!modalEntry) {
|
||||
try {
|
||||
await interaction.reply({
|
||||
@@ -94,7 +97,7 @@ export class DiscordComponentModal extends Modal {
|
||||
return;
|
||||
}
|
||||
|
||||
const consumed = resolveDiscordModalEntry({
|
||||
const consumed = await resolveDiscordModalEntryWithPersistence({
|
||||
id: modalId,
|
||||
consume: !modalEntry.reusable,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user