@kevlid/discordmenus 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ function getPermissions(interaction) {
2
+ const raw = interaction?.member?.permissions ?? interaction?.member?.permission ?? interaction?.memberPermissions ?? null;
3
+ if (raw == null) {
4
+ return "0";
5
+ }
6
+ if (typeof raw === "object" && raw.allow != null) {
7
+ return String(raw.allow);
8
+ }
9
+ if (typeof raw.valueOf === "function") {
10
+ return String(raw.valueOf());
11
+ }
12
+ return String(raw);
13
+ }
14
+
15
+ function normalizeInteraction(interaction) {
16
+ const source = typeof interaction?.toJSON === "function" ? interaction.toJSON() : interaction ?? {};
17
+
18
+ const userId =
19
+ source.member?.user?.id
20
+ ?? source.user?.id
21
+ ?? interaction?.member?.user?.id
22
+ ?? interaction?.user?.id
23
+ ?? null;
24
+
25
+ let customId =
26
+ source.data?.custom_id
27
+ ?? source.data?.customID
28
+ ?? source.custom_id
29
+ ?? source.customId
30
+ ?? interaction?.data?.custom_id
31
+ ?? interaction?.data?.customID
32
+ ?? interaction?.custom_id
33
+ ?? interaction?.customId
34
+ ?? null;
35
+
36
+ if (customId == null && typeof source.id === "string" && source.id.length > 0) {
37
+ customId = source.id;
38
+ }
39
+
40
+ let values = source.data?.values ?? source.values ?? interaction?.data?.values ?? interaction?.values ?? [];
41
+ if (!Array.isArray(values)) {
42
+ values = [];
43
+ }
44
+
45
+ let components =
46
+ source.data?.components
47
+ ?? source.components
48
+ ?? source.fields?.components
49
+ ?? interaction?.data?.components
50
+ ?? interaction?.components
51
+ ?? interaction?.fields?.components
52
+ ?? [];
53
+ if (!Array.isArray(components)) {
54
+ components = [];
55
+ }
56
+
57
+ const normalized = {
58
+ type: source.type ?? interaction?.type ?? null,
59
+ guildId:
60
+ source.guildId
61
+ ?? source.guild_id
62
+ ?? source.guildID
63
+ ?? interaction?.guildId
64
+ ?? interaction?.guild_id
65
+ ?? interaction?.guildID
66
+ ?? null,
67
+ member: null,
68
+ user: null,
69
+ data: {
70
+ custom_id: customId,
71
+ values,
72
+ components,
73
+ },
74
+ };
75
+
76
+ if (userId != null) {
77
+ normalized.user = { id: userId };
78
+ }
79
+
80
+ if (source.member != null || interaction?.member != null) {
81
+ normalized.member = {
82
+ user: { id: userId },
83
+ permissions: getPermissions(interaction),
84
+ };
85
+ }
86
+
87
+ return normalized;
88
+ }
89
+
90
+ function fromEris(interaction) {
91
+ return normalizeInteraction(interaction);
92
+ }
93
+
94
+ function fromDiscordJS(interaction) {
95
+ return normalizeInteraction(interaction);
96
+ }
97
+
98
+ module.exports = { normalizeInteraction, fromEris, fromDiscordJS };
@@ -0,0 +1,23 @@
1
+ const { MaxComponents } = require("./constants");
2
+
3
+ function validateComponents(components, max = MaxComponents) {
4
+ if (!Array.isArray(components)) {
5
+ throw new Error("components must be an array");
6
+ }
7
+ if (components.length > max) {
8
+ throw new Error(`Too many components: ${components.length} > ${max}`);
9
+ }
10
+ for (const comp of components) {
11
+ if (typeof comp !== "object" || comp == null) {
12
+ throw new Error("Component is not an object");
13
+ }
14
+ if (!("type" in comp)) {
15
+ throw new Error("Component missing type field");
16
+ }
17
+ if (comp.components) {
18
+ validateComponents(comp.components, max);
19
+ }
20
+ }
21
+ }
22
+
23
+ module.exports = { validateComponents };
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ CustomIdPrefix: "dm",
3
+ ComponentsV2MessageFlags: 32768,
4
+ EphemeralMessageFlag: 64,
5
+ MaxComponents: 40,
6
+ PageRenderOverhead: 9,
7
+ OptionRenderCost: 4,
8
+ ObjectPageOverhead: 8,
9
+ ObjectListPageOverhead: 8,
10
+ ObjectListItemCost: 5,
11
+ };
@@ -0,0 +1,67 @@
1
+ const { encodeCustomId, decodeCustomId, encodeToken, decodeToken } = require("@kevlid/customids");
2
+ const { CustomIdPrefix } = require("./constants");
3
+
4
+ const Presets = {
5
+ userId: "uid",
6
+ previousPage: "pp",
7
+ nextPage: "np",
8
+ previousCategory: "pc",
9
+ nextCategory: "nc",
10
+ nav: "nav",
11
+ view: "view",
12
+ done: "done",
13
+ add: "add",
14
+ edit: "edit",
15
+ delete: "delete",
16
+ modal: "modal",
17
+ subModal: "sub_modal",
18
+ subBoolean: "sub_boolean",
19
+ subList: "sub_list",
20
+ subListModal: "sub_list_modal",
21
+ listItemView: "iv",
22
+ listItemDone: "id",
23
+ listItemField: "if",
24
+ listItemModal: "im",
25
+ listItemBoolToggle: "it",
26
+ navSpacer: "ns",
27
+ };
28
+
29
+ function encodeId(menuKey, values = [], options = {}) {
30
+ let optionString = "";
31
+ if (Object.keys(options).length > 0) {
32
+ optionString += "|";
33
+ }
34
+ for (const [key, value] of Object.entries(options)) {
35
+ if (key.length > 9) throw new Error("Option keys must be at most 9 characters long");
36
+ optionString += `${key.length}${key}${encodeToken(value)};`;
37
+ }
38
+ let customId = CustomIdPrefix + encodeCustomId([menuKey, ...values]) + optionString;
39
+ if (customId.length > 100) {
40
+ throw new Error(`Encoded custom ID is too long (${customId.length} characters). Consider using shorter option keys or fewer values.`);
41
+ }
42
+ return customId;
43
+ }
44
+
45
+ function decodeId(id) {
46
+ const raw = id.slice(CustomIdPrefix.length);
47
+ const options = new Map();
48
+ const [parts, optionString] = raw.split("|");
49
+ const { values: decoded } = decodeCustomId(parts);
50
+ const [menu_key, ...values] = decoded;
51
+ if (optionString) {
52
+ const optionParts = optionString.split(";").filter(Boolean);
53
+ for (const part of optionParts) {
54
+ const keyLength = parseInt(part[0]);
55
+ const key = part.slice(1, 1 + keyLength);
56
+ const value = decodeToken(part.slice(1 + keyLength));
57
+ options.set(key, value);
58
+ }
59
+ }
60
+ return { menu_key, values, options };
61
+ }
62
+
63
+ module.exports = {
64
+ encodeId,
65
+ decodeId,
66
+ Presets,
67
+ };
@@ -0,0 +1 @@
1
+ function formatKey(key) {
2
  if (typeof key !== "string") throw new TypeError("Key must be a string");
1
3
  return key.toLowerCase().replace(" ", "_");
2
4
  formatKey
@@ -0,0 +1,100 @@
1
+ const { OptionTypes } = require("./types");
2
+
3
+ function arrayClone(v) {
4
+ return Array.isArray(v) ? [...v] : [];
5
+ }
6
+
7
+ function asObject(v) {
8
+ return v != null && typeof v === "object" ? v : {};
9
+ }
10
+
11
+ function objectDefaultsFromKeys(options) {
12
+ const out = {};
13
+ for (const opt of options || []) {
14
+ out[opt.key] = null;
15
+ }
16
+ return out;
17
+ }
18
+
19
+ function objectListPropertyKey(option, token) {
20
+ if (!option || option.itemType !== OptionTypes.Object) {
21
+ return token;
22
+ }
23
+ const idx = parseInt(token, 10);
24
+ const props = option.itemConfig?.properties;
25
+ if (!Number.isNaN(idx) && Array.isArray(props) && props[idx]) {
26
+ return props[idx].key;
27
+ }
28
+ return token;
29
+ }
30
+
31
+ function objectListItemFailsRequired(option, currentItem) {
32
+ const props = option?.itemConfig?.properties;
33
+ if (!Array.isArray(props)) {
34
+ return false;
35
+ }
36
+ for (const property of props) {
37
+ if (!property.required) {
38
+ continue;
39
+ }
40
+ let fieldValue;
41
+ if (currentItem != null && typeof currentItem === "object") {
42
+ fieldValue = currentItem[property.key];
43
+ } else {
44
+ fieldValue = null;
45
+ }
46
+ if (property.type === OptionTypes.String) {
47
+ if (fieldValue == null || String(fieldValue).trim() === "") {
48
+ return true;
49
+ }
50
+ } else if (property.type === OptionTypes.Number) {
51
+ if (fieldValue == null || Number.isNaN(Number(fieldValue))) {
52
+ return true;
53
+ }
54
+ } else if (property.type === OptionTypes.Boolean) {
55
+ if (fieldValue == null) {
56
+ return true;
57
+ }
58
+ } else if (fieldValue == null) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+
65
+ function listJumpSize(length) {
66
+ return Math.min(10, Math.max(1, Math.ceil(length * 0.1)));
67
+ }
68
+
69
+ function shapeObjectListItem(properties, raw) {
70
+ const s = raw != null && typeof raw === "object" ? raw : {};
71
+ const o = {};
72
+ for (const p of properties || []) {
73
+ o[p.key] = Object.prototype.hasOwnProperty.call(s, p.key) ? s[p.key] : null;
74
+ }
75
+ return o;
76
+ }
77
+
78
+ function normalizeObjectListItems(properties, items) {
79
+ if (!Array.isArray(items)) {
80
+ return [];
81
+ }
82
+ return items.map(item => shapeObjectListItem(properties, item));
83
+ }
84
+
85
+ function objectListItemsNeedReshape(properties, items) {
86
+ const norm = normalizeObjectListItems(properties, items);
87
+ return JSON.stringify(items) !== JSON.stringify(norm);
88
+ }
89
+
90
+ module.exports = {
91
+ arrayClone,
92
+ asObject,
93
+ objectDefaultsFromKeys,
94
+ objectListPropertyKey,
95
+ objectListItemFailsRequired,
96
+ listJumpSize,
97
+ shapeObjectListItem,
98
+ normalizeObjectListItems,
99
+ objectListItemsNeedReshape,
100
+ };
@@ -0,0 +1 @@
1
+ const { ComponentTypes } = require("../render/types");
2
  const results = [];
1
3
  if (!Array.isArray(components)) {
2
4
  return results;
3
5
  }
4
6
  for (const row of components) {
5
7
  let inputs = [];
6
8
  if (row.type === ComponentTypes.ActionRow) {
7
9
  if (Array.isArray(row.components)) {
8
10
  inputs = row.components;
9
11
  }
10
12
  } else if (row.type === ComponentTypes.Label) {
11
13
  if (row.component != null) {
12
14
  inputs = [row.component];
13
15
  }
14
16
  }
15
17
  for (const input of inputs) {
16
18
  if (input == null) {
17
19
  continue;
18
20
  }
19
21
  const customId = input.custom_id ?? input.customId;
20
22
  if (customId == null) {
21
23
  continue;
22
24
  }
23
25
  results.push({
24
26
  customId: String(customId),
25
27
  type: input.type,
26
28
  value: input.value ?? null,
27
29
  });
28
30
  }
29
31
  }
30
32
  return results;
@@ -0,0 +1,131 @@
1
+ class MenuStorage {
2
+ constructor({ cache = false, cacheTTL = 60000, cacheMaxSize = 500 } = {}) {
3
+ this.cacheEnabled = cache;
4
+ this.cacheTTL = cacheTTL;
5
+ this.cacheMaxSize = cacheMaxSize;
6
+ this.cache = new Map();
7
+
8
+ if (cache) {
9
+ setInterval(() => {
10
+ const now = Date.now();
11
+ for (const [k, entry] of this.cache.entries()) {
12
+ if (now - entry.ts > this.cacheTTL) {
13
+ this.cache.delete(k);
14
+ }
15
+ }
16
+ }, cacheTTL);
17
+ }
18
+ }
19
+
20
+ cacheKey(key, guildId) {
21
+ if (guildId) {
22
+ return `${guildId}:${key}`;
23
+ }
24
+ return key;
25
+ }
26
+
27
+ cacheGet(key, guildId) {
28
+ if (!this.cacheEnabled) {
29
+ return undefined;
30
+ }
31
+ const ck = this.cacheKey(key, guildId);
32
+ const entry = this.cache.get(ck);
33
+ if (!entry) {
34
+ return undefined;
35
+ }
36
+ if (Date.now() - entry.ts > this.cacheTTL) {
37
+ this.cache.delete(ck);
38
+ return undefined;
39
+ }
40
+ return entry.value;
41
+ }
42
+
43
+ cacheSet(key, value, guildId) {
44
+ if (!this.cacheEnabled) {
45
+ return;
46
+ }
47
+ if (this.cache.size >= this.cacheMaxSize) {
48
+ this.cache.delete(this.cache.keys().next().value);
49
+ }
50
+ this.cache.set(this.cacheKey(key, guildId), { value, ts: Date.now() });
51
+ }
52
+
53
+ cacheDelete(key, guildId) {
54
+ if (!this.cacheEnabled) {
55
+ return;
56
+ }
57
+ this.cache.delete(this.cacheKey(key, guildId));
58
+ }
59
+
60
+ cacheClear(guildId) {
61
+ if (!this.cacheEnabled) {
62
+ return;
63
+ }
64
+ if (guildId) {
65
+ const prefix = `${guildId}:`;
66
+ for (const k of this.cache.keys()) {
67
+ if (k.startsWith(prefix)) {
68
+ this.cache.delete(k);
69
+ }
70
+ }
71
+ } else {
72
+ this.cache.clear();
73
+ }
74
+ }
75
+
76
+ async get(key, ctx = {}) {
77
+ throw new Error("MenuStorage: get() must be implemented");
78
+ }
79
+
80
+ async save(key, value, ctx = {}) {
81
+ throw new Error("MenuStorage: save() must be implemented");
82
+ }
83
+
84
+ handler() {
85
+ return {
86
+ get: async (key, ctx) => {
87
+ const guildId = ctx?.guildId ?? null;
88
+ const cached = this.cacheGet(key, guildId);
89
+ if (cached !== undefined) {
90
+ return cached;
91
+ }
92
+ const result = await this.get(key, ctx);
93
+ this.cacheSet(key, result, guildId);
94
+ return result;
95
+ },
96
+ save: async (key, value, ctx) => {
97
+ const guildId = ctx?.guildId ?? null;
98
+ this.cacheDelete(key, guildId);
99
+ return this.save(key, value, ctx);
100
+ },
101
+ };
102
+ }
103
+ }
104
+
105
+ class MemoryMenuStorage extends MenuStorage {
106
+ constructor(opts = {}) {
107
+ super(opts);
108
+ this.data = new Map();
109
+ }
110
+
111
+ storeKey(key, guildId) {
112
+ if (guildId) {
113
+ return `${guildId}:${key}`;
114
+ }
115
+ return key;
116
+ }
117
+
118
+ async get(key, { guildId } = {}) {
119
+ const stored = this.data.get(this.storeKey(key, guildId));
120
+ if (stored === undefined) {
121
+ return null;
122
+ }
123
+ return stored;
124
+ }
125
+
126
+ async save(key, value, { guildId } = {}) {
127
+ this.data.set(this.storeKey(key, guildId), value);
128
+ }
129
+ }
130
+
131
+ module.exports = { MenuStorage, MemoryMenuStorage };
@@ -0,0 +1,34 @@
1
+ const OptionTypes = {
2
+ Menu: "menu",
3
+ Category: "category",
4
+ String: "string",
5
+ Number: "number",
6
+ Boolean: "boolean",
7
+ List: "list",
8
+ Object: "object",
9
+ StringSelect: "string_select",
10
+ UserSelect: "user_select",
11
+ RoleSelect: "role_select",
12
+ MentionableSelect: "mentionable_select",
13
+ ChannelSelect: "channel_select",
14
+ };
15
+
16
+ const InteractionTypes = {
17
+ ApplicationCommand: 2,
18
+ MessageComponent: 3,
19
+ ModalSubmit: 5,
20
+ };
21
+
22
+ const ResponseTypes = {
23
+ ChannelMessageWithSource: 4,
24
+ DeferredChannelMessageWithSource: 5,
25
+ DeferredUpdateMessage: 6,
26
+ UpdateMessage: 7,
27
+ Modal: 9,
28
+ };
29
+
30
+ module.exports = {
31
+ OptionTypes,
32
+ InteractionTypes,
33
+ ResponseTypes,
34
+ };