EXTENSION for treeos-connect
gateway-discord
Registers the Discord channel type with the gateway core, enabling trees to send and receive messages through Discord servers. Output channels post notifications to a Discord channel via webhook URL. Input channels connect a persistent Discord bot that listens for messages in a specific channel and routes them through the gateway's AI pipeline. Input-output channels do both: the bot listens for messages, processes them through the tree orchestrator, and replies directly in the Discord channel. The bot manager maintains persistent Discord.js client connections, one per unique bot token. Multiple gateway channels can share a single bot if they use the same token, with each channel mapped to a different Discord channel ID. On server startup, the manager scans the database for all enabled Discord input channels, decrypts their bot tokens, and connects their bots automatically. When a channel is disabled or deleted, the bot is removed from the channel map. When no channels remain on a bot, the client is destroyed to free resources. Bot messages from the bot's own user ID are ignored to prevent self-reply loops. Output uses the Discord webhook API, which requires no bot and no persistent connection. Messages are formatted with bold titles and truncated to Discord's 2000 character limit. For input-output channels that also need to send outbound notifications, the handler can use either a webhook URL or the bot API directly, falling through based on what is configured. Input channels require Standard or Premium tier subscriptions, enforced by the gateway core's tier check system. Config validation ensures webhook URLs point to discord.com or discordapp.com domains, and input channels provide both a bot token and a Discord channel ID.
v1.0.1 by TreeOS Site 0 downloads 6 files 809 lines 27.0 KB published 38d ago
treeos ext install gateway-discord
View changelog

Manifest

Requires

  • models: Node, User
  • extensions: gateway
SHA256: e2505df6730235e02a7334accdd1d8c8f97cb40d1cd57519f31b37cf82101eef

Dependents

1 package depend on this

PackageTypeRelationship
treeos-connect v1.0.3bundleincludes

Source Code

1// Manages persistent Discord bot connections for gateway input channels.
2// One bot client per unique bot token. Multiple channels can share a bot.
3
4import log from "../../seed/log.js";
5
6let Client, GatewayIntentBits;
7let discordLoaded = false;
8
9async function loadDiscord() {
10  if (discordLoaded) return;
11  try {
12    const discordjs = await import("discord.js");
13    Client = discordjs.Client;
14    GatewayIntentBits = discordjs.GatewayIntentBits;
15    discordLoaded = true;
16  } catch {
17    throw new Error(
18      "discord.js package not installed. Run: npm install discord.js",
19    );
20  }
21}
22
23// tokenHash -> { client, channelMap: Map<channelId, { discordChannelId, rootId }> }
24const activeBots = new Map();
25
26function hashToken(token) {
27  // Use last 10 chars as key (enough to distinguish, avoids storing full token in memory map key)
28  return token.slice(-10);
29}
30
31export async function connectBot(
32  botToken,
33  channelId,
34  discordChannelId,
35  rootId,
36) {
37  await loadDiscord();
38
39  const key = hashToken(botToken);
40  const entry = activeBots.get(key);
41
42  if (entry) {
43    // Bot already connected, just add this channel to its map
44    entry.channelMap.set(channelId, { discordChannelId, rootId });
45    log.debug("GatewayDiscord",
46      `Bot manager: added channel ${channelId} to existing bot (${entry.channelMap.size} channels)`,
47    );
48    return;
49  }
50
51  // Create new bot client
52  const client = new Client({
53    intents: [
54      GatewayIntentBits.Guilds,
55      GatewayIntentBits.GuildMessages,
56      GatewayIntentBits.MessageContent,
57    ],
58  });
59
60  const channelMap = new Map();
61  channelMap.set(channelId, { discordChannelId, rootId });
62
63  // Message handler
64  client.on("messageCreate", async (message) => {
65    // Ignore only our own replies (prevents self-reply loops)
66    if (message.author.id === client.user?.id) return;
67
68    // Find which gateway channel(s) are listening on this Discord channel
69    for (const [gwChannelId, config] of channelMap) {
70      if (message.channel.id !== config.discordChannelId) continue;
71
72      const senderName =
73        message.author.displayName || message.author.username || "Unknown";
74      const senderPlatformId = message.author.id;
75      const messageText = message.content;
76
77      if (!messageText || !messageText.trim()) continue;
78
79      log.debug("GatewayDiscord",
80        `Bot: message on gw channel ${gwChannelId} from ${senderName}: "${messageText.slice(0, 80)}"`,
81      );
82
83      try {
84        // Process via gateway core
85        const { getExtension } = await import("../loader.js");
86        const gateway = getExtension("gateway");
87        if (!gateway?.exports?.processGatewayMessage) {
88          log.error("GatewayDiscord", "Gateway core not loaded");
89          continue;
90        }
91
92        const result = await gateway.exports.processGatewayMessage(gwChannelId, {
93          senderName,
94          senderPlatformId,
95          messageText,
96        });
97
98        // Send reply back to the same Discord channel if input-output
99        if (result.reply) {
100          let replyText = result.reply;
101          if (replyText.length > 2000) {
102            replyText = replyText.slice(0, 1997) + "...";
103          }
104          await message.channel.send(replyText);
105        }
106      } catch (err) {
107        log.error("GatewayDiscord",
108          `Bot: error processing message for channel ${gwChannelId}:`,
109          err.message,
110        );
111      }
112    }
113  });
114
115  client.on("error", (err) => {
116    log.error("GatewayDiscord", "Bot client error:", err.message);
117  });
118
119  client.on("warn", (msg) => {
120    log.warn("GatewayDiscord", "Bot warning:", msg);
121  });
122
123  // Login
124  try {
125    await client.login(botToken);
126    log.verbose("GatewayDiscord",
127      `Bot manager: bot connected as ${client.user?.tag}, ${channelMap.size} channel(s)`,
128    );
129  } catch (err) {
130    log.error("GatewayDiscord", "Bot manager: login failed:", err.message);
131    throw new Error("Discord bot login failed: " + err.message);
132  }
133
134  activeBots.set(key, { client, channelMap, botToken });
135}
136
137export async function disconnectBot(channelId) {
138  for (const [key, entry] of activeBots) {
139    if (entry.channelMap.has(channelId)) {
140      entry.channelMap.delete(channelId);
141      log.debug("GatewayDiscord",
142        `Bot manager: removed channel ${channelId} (${entry.channelMap.size} remaining)`,
143      );
144
145      // If no more channels, destroy the bot client
146      if (entry.channelMap.size === 0) {
147        try {
148          entry.client.destroy();
149        } catch (err) { log.debug("GatewayDiscord", "bot client destroy failed:", err.message); }
150        activeBots.delete(key);
151        log.verbose("GatewayDiscord",
152          "Bot manager: bot client destroyed (no channels left)",
153        );
154      }
155      return;
156    }
157  }
158}
159
160export async function disconnectAllBots() {
161  for (const [key, entry] of activeBots) {
162    try {
163      entry.client.destroy();
164    } catch (err) { log.debug("GatewayDiscord", "bot client destroy failed during shutdown:", err.message); }
165  }
166  activeBots.clear();
167  log.verbose("GatewayDiscord", "Bot manager: all bots disconnected");
168}
169
170export function getActiveBotCount() {
171  return activeBots.size;
172}
173
174/**
175 * Scan DB for enabled Discord input channels and connect their bots.
176 * Called on server startup.
177 */
178export async function startupScan() {
179  try {
180    const { getExtension } = await import("../loader.js");
181    const gatewayExt = getExtension("gateway");
182    const GatewayChannel = gatewayExt?.exports?.GatewayChannel;
183    const getChannelWithSecrets = gatewayExt?.exports?.getChannelWithSecrets;
184
185    const channels = await GatewayChannel.find({
186      type: "discord",
187      direction: { $in: ["input", "input-output"] },
188      enabled: true,
189    }).lean();
190
191    if (channels.length === 0) {
192      log.debug("GatewayDiscord", "Bot manager: no Discord input channels to connect");
193      return;
194    }
195
196    log.verbose("GatewayDiscord",
197      `Bot manager: found ${channels.length} Discord input channel(s) to connect`,
198    );
199
200    for (const channel of channels) {
201      try {
202        const full = await getChannelWithSecrets(channel._id);
203        if (!full?.config?.decryptedSecrets?.botToken) {
204          log.error("GatewayDiscord",
205            `Bot manager: no bot token for channel ${channel._id}`,
206          );
207          continue;
208        }
209
210        await connectBot(
211          full.config.decryptedSecrets.botToken,
212          channel._id,
213          channel.config.metadata.discordChannelId,
214          channel.rootId,
215        );
216      } catch (err) {
217        log.error("GatewayDiscord",
218          `Bot manager: failed to connect channel ${channel._id}:`,
219          err.message,
220        );
221      }
222    }
223  } catch (err) {
224    log.error("GatewayDiscord", "Bot manager: startup scan failed:", err.message);
225  }
226}
227
1// Discord channel type handler.
2// Registered with gateway core during init.
3
4import log from "../../seed/log.js";
5
6// ─────────────────────────────────────────────────────────────────────────
7// VALIDATION
8// ─────────────────────────────────────────────────────────────────────────
9
10function validateDiscordWebhookUrl(webhookUrl) {
11  try {
12    const parsed = new URL(webhookUrl);
13    if (
14      !parsed.hostname.endsWith("discord.com") &&
15      !parsed.hostname.endsWith("discordapp.com")
16    ) {
17      throw new Error("Discord webhook URL must be a discord.com URL");
18    }
19  } catch (err) {
20    if (err.message.includes("discord")) throw err;
21    throw new Error("Invalid Discord webhook URL");
22  }
23}
24
25function validateConfig(config, direction) {
26  const hasInput = direction === "input" || direction === "input-output";
27  const hasOutput = direction === "output" || direction === "input-output";
28
29  if (hasInput) {
30    // Discord input requires bot token + channel ID (bot connects via gateway)
31    if (!config.botToken || typeof config.botToken !== "string") {
32      throw new Error("Discord input channel requires a botToken");
33    }
34    if (
35      !config.discordChannelId ||
36      typeof config.discordChannelId !== "string"
37    ) {
38      throw new Error("Discord input channel requires a discordChannelId");
39    }
40    // Webhook URL optional for input-output (used for output side)
41    if (hasOutput && config.webhookUrl) {
42      validateDiscordWebhookUrl(config.webhookUrl);
43    }
44  } else {
45    // Output-only: webhook URL required
46    if (!config.webhookUrl || typeof config.webhookUrl !== "string") {
47      throw new Error("Discord channel requires a webhookUrl");
48    }
49    validateDiscordWebhookUrl(config.webhookUrl);
50  }
51}
52
53function buildEncryptedConfig(config, direction) {
54  const hasInput = direction === "input" || direction === "input-output";
55  let secrets;
56  let metadata = {};
57
58  if (hasInput) {
59    // Bot token for input, optionally webhook for output side
60    secrets = { botToken: config.botToken };
61    if (config.webhookUrl) secrets.webhookUrl = config.webhookUrl;
62    metadata = {
63      discordChannelId: config.discordChannelId,
64      guildId: config.guildId || null,
65    };
66  } else {
67    secrets = { webhookUrl: config.webhookUrl };
68  }
69
70  return {
71    secrets,
72    metadata,
73    displayIdentifier: config.displayIdentifier || null,
74  };
75}
76
77// ─────────────────────────────────────────────────────────────────────────
78// SENDER
79// ─────────────────────────────────────────────────────────────────────────
80
81async function send(secrets, metadata, notification) {
82  let content = `**${notification.title}**\n\n${notification.content}`;
83  if (content.length > 2000) {
84    content = content.slice(0, 1997) + "...";
85  }
86
87  if (secrets.webhookUrl) {
88    // Output via webhook
89    const res = await fetch(secrets.webhookUrl, {
90      method: "POST",
91      headers: { "Content-Type": "application/json" },
92      body: JSON.stringify({ content }),
93    });
94
95    if (!res.ok) {
96      const body = await res.text();
97      throw new Error(`Discord webhook error ${res.status}: ${body}`);
98    }
99  } else if (secrets.botToken && metadata.discordChannelId) {
100    // Input channel, send via bot API
101    const res = await fetch(`https://discord.com/api/v10/channels/${metadata.discordChannelId}/messages`, {
102      method: "POST",
103      headers: {
104        "Content-Type": "application/json",
105        "Authorization": `Bot ${secrets.botToken}`,
106      },
107      body: JSON.stringify({ content }),
108    });
109
110    if (!res.ok) {
111      const body = await res.text();
112      throw new Error(`Discord bot API error ${res.status}: ${body}`);
113    }
114  } else {
115    throw new Error("Discord channel has no webhookUrl or botToken configured");
116  }
117}
118
119// ─────────────────────────────────────────────────────────────────────────
120// INPUT LIFECYCLE
121// ─────────────────────────────────────────────────────────────────────────
122
123async function registerInput(channel, secrets) {
124  const { connectBot } = await import("./botManager.js");
125  await connectBot(
126    secrets.botToken,
127    channel._id,
128    channel.config.metadata.discordChannelId,
129    channel.rootId,
130  );
131  log.verbose("GatewayDiscord", `Discord bot connected for channel ${channel._id}`);
132}
133
134async function unregisterInput(channel, secrets) {
135  try {
136    const { disconnectBot } = await import("./botManager.js");
137    await disconnectBot(channel._id);
138    log.verbose("GatewayDiscord", `Discord bot disconnected for channel ${channel._id}`);
139  } catch (err) {
140    log.error("GatewayDiscord", `Failed to disconnect Discord bot for ${channel._id}:`, err.message);
141  }
142}
143
144// ─────────────────────────────────────────────────────────────────────────
145// EXPORT HANDLER
146// ─────────────────────────────────────────────────────────────────────────
147
148export default {
149  allowedDirections: ["input", "output", "input-output"],
150  requiredTiers: ["standard", "premium"],
151  validateConfig,
152  buildEncryptedConfig,
153  send,
154  registerInput,
155  unregisterInput,
156};
157
1import log from "../../seed/log.js";
2import handler from "./handler.js";
3import { getExtension } from "../loader.js";
4import { startupScan, disconnectAllBots } from "./botManager.js";
5
6export async function init(core) {
7  const gateway = getExtension("gateway");
8  if (!gateway?.exports?.registerChannelType) {
9    throw new Error("gateway-discord requires the gateway extension to be loaded first");
10  }
11
12  gateway.exports.registerChannelType("discord", handler);
13  log.verbose("GatewayDiscord", "Registered discord channel type");
14
15  return {
16    jobs: [
17      {
18        name: "gateway-discord-bots",
19        start: () => { startupScan(); },
20        stop: () => { disconnectAllBots(); },
21      },
22    ],
23  };
24}
25
1export default {
2  name: "gateway-discord",
3  version: "1.0.1",
4  builtFor: "treeos-connect",
5  description:
6    "Registers the Discord channel type with the gateway core, enabling trees to send and " +
7    "receive messages through Discord servers. Output channels post notifications to a " +
8    "Discord channel via webhook URL. Input channels connect a persistent Discord bot that " +
9    "listens for messages in a specific channel and routes them through the gateway's AI " +
10    "pipeline. Input-output channels do both: the bot listens for messages, processes them " +
11    "through the tree orchestrator, and replies directly in the Discord channel." +
12    "\n\n" +
13    "The bot manager maintains persistent Discord.js client connections, one per unique bot " +
14    "token. Multiple gateway channels can share a single bot if they use the same token, " +
15    "with each channel mapped to a different Discord channel ID. On server startup, the " +
16    "manager scans the database for all enabled Discord input channels, decrypts their bot " +
17    "tokens, and connects their bots automatically. When a channel is disabled or deleted, " +
18    "the bot is removed from the channel map. When no channels remain on a bot, the client " +
19    "is destroyed to free resources. Bot messages from the bot's own user ID are ignored to " +
20    "prevent self-reply loops." +
21    "\n\n" +
22    "Output uses the Discord webhook API, which requires no bot and no persistent connection. " +
23    "Messages are formatted with bold titles and truncated to Discord's 2000 character limit. " +
24    "For input-output channels that also need to send outbound notifications, the handler " +
25    "can use either a webhook URL or the bot API directly, falling through based on what is " +
26    "configured. Input channels require Standard or Premium tier subscriptions, enforced by " +
27    "the gateway core's tier check system. Config validation ensures webhook URLs point to " +
28    "discord.com or discordapp.com domains, and input channels provide both a bot token and " +
29    "a Discord channel ID.",
30
31  npm: ["discord.js@^14.25.1"],
32
33  needs: {
34    models: ["Node", "User"],
35    extensions: ["gateway"],
36  },
37
38  optional: {},
39
40  provides: {
41    models: {},
42    routes: false,
43    tools: false,
44    jobs: false,
45    orchestrator: false,
46    energyActions: {},
47    sessionTypes: {},
48    env: [],
49    cli: [],
50
51    hooks: {
52      fires: [],
53      listens: [],
54    },
55  },
56};
57
1{
2  "name": "treeos-ext-gateway-discord",
3  "version": "1.0.0",
4  "lockfileVersion": 3,
5  "requires": true,
6  "packages": {
7    "": {
8      "name": "treeos-ext-gateway-discord",
9      "version": "1.0.0",
10      "dependencies": {
11        "discord.js": "^14.25.1"
12      }
13    },
14    "node_modules/@discordjs/builders": {
15      "version": "1.14.0",
16      "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.0.tgz",
17      "integrity": "sha512-7pVKxVWkeLUtrTo9nTYkjRcJk0Hlms6lYervXAD7E7+K5lil9ms2JrEB1TalMiHvQMh7h1HJZ4fCJa0/vHpl4w==",
18      "license": "Apache-2.0",
19      "dependencies": {
20        "@discordjs/formatters": "^0.6.2",
21        "@discordjs/util": "^1.2.0",
22        "@sapphire/shapeshift": "^4.0.0",
23        "discord-api-types": "^0.38.40",
24        "fast-deep-equal": "^3.1.3",
25        "ts-mixer": "^6.0.4",
26        "tslib": "^2.6.3"
27      },
28      "engines": {
29        "node": ">=16.11.0"
30      },
31      "funding": {
32        "url": "https://github.com/discordjs/discord.js?sponsor"
33      }
34    },
35    "node_modules/@discordjs/collection": {
36      "version": "1.5.3",
37      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
38      "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
39      "license": "Apache-2.0",
40      "engines": {
41        "node": ">=16.11.0"
42      }
43    },
44    "node_modules/@discordjs/formatters": {
45      "version": "0.6.2",
46      "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
47      "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
48      "license": "Apache-2.0",
49      "dependencies": {
50        "discord-api-types": "^0.38.33"
51      },
52      "engines": {
53        "node": ">=16.11.0"
54      },
55      "funding": {
56        "url": "https://github.com/discordjs/discord.js?sponsor"
57      }
58    },
59    "node_modules/@discordjs/rest": {
60      "version": "2.6.1",
61      "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
62      "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
63      "license": "Apache-2.0",
64      "dependencies": {
65        "@discordjs/collection": "^2.1.1",
66        "@discordjs/util": "^1.2.0",
67        "@sapphire/async-queue": "^1.5.3",
68        "@sapphire/snowflake": "^3.5.5",
69        "@vladfrangu/async_event_emitter": "^2.4.6",
70        "discord-api-types": "^0.38.40",
71        "magic-bytes.js": "^1.13.0",
72        "tslib": "^2.6.3",
73        "undici": "6.24.1"
74      },
75      "engines": {
76        "node": ">=18"
77      },
78      "funding": {
79        "url": "https://github.com/discordjs/discord.js?sponsor"
80      }
81    },
82    "node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
83      "version": "2.1.1",
84      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
85      "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
86      "license": "Apache-2.0",
87      "engines": {
88        "node": ">=18"
89      },
90      "funding": {
91        "url": "https://github.com/discordjs/discord.js?sponsor"
92      }
93    },
94    "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
95      "version": "3.5.5",
96      "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
97      "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
98      "license": "MIT",
99      "engines": {
100        "node": ">=v14.0.0",
101        "npm": ">=7.0.0"
102      }
103    },
104    "node_modules/@discordjs/rest/node_modules/undici": {
105      "version": "6.24.1",
106      "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
107      "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
108      "license": "MIT",
109      "engines": {
110        "node": ">=18.17"
111      }
112    },
113    "node_modules/@discordjs/util": {
114      "version": "1.2.0",
115      "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
116      "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
117      "license": "Apache-2.0",
118      "dependencies": {
119        "discord-api-types": "^0.38.33"
120      },
121      "engines": {
122        "node": ">=18"
123      },
124      "funding": {
125        "url": "https://github.com/discordjs/discord.js?sponsor"
126      }
127    },
128    "node_modules/@discordjs/ws": {
129      "version": "1.2.3",
130      "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
131      "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
132      "license": "Apache-2.0",
133      "dependencies": {
134        "@discordjs/collection": "^2.1.0",
135        "@discordjs/rest": "^2.5.1",
136        "@discordjs/util": "^1.1.0",
137        "@sapphire/async-queue": "^1.5.2",
138        "@types/ws": "^8.5.10",
139        "@vladfrangu/async_event_emitter": "^2.2.4",
140        "discord-api-types": "^0.38.1",
141        "tslib": "^2.6.2",
142        "ws": "^8.17.0"
143      },
144      "engines": {
145        "node": ">=16.11.0"
146      },
147      "funding": {
148        "url": "https://github.com/discordjs/discord.js?sponsor"
149      }
150    },
151    "node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
152      "version": "2.1.1",
153      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
154      "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
155      "license": "Apache-2.0",
156      "engines": {
157        "node": ">=18"
158      },
159      "funding": {
160        "url": "https://github.com/discordjs/discord.js?sponsor"
161      }
162    },
163    "node_modules/@sapphire/async-queue": {
164      "version": "1.5.5",
165      "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
166      "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
167      "license": "MIT",
168      "engines": {
169        "node": ">=v14.0.0",
170        "npm": ">=7.0.0"
171      }
172    },
173    "node_modules/@sapphire/shapeshift": {
174      "version": "4.0.0",
175      "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
176      "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
177      "license": "MIT",
178      "dependencies": {
179        "fast-deep-equal": "^3.1.3",
180        "lodash": "^4.17.21"
181      },
182      "engines": {
183        "node": ">=v16"
184      }
185    },
186    "node_modules/@sapphire/snowflake": {
187      "version": "3.5.3",
188      "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
189      "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
190      "license": "MIT",
191      "engines": {
192        "node": ">=v14.0.0",
193        "npm": ">=7.0.0"
194      }
195    },
196    "node_modules/@types/node": {
197      "version": "25.5.0",
198      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
199      "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
200      "license": "MIT",
201      "dependencies": {
202        "undici-types": "~7.18.0"
203      }
204    },
205    "node_modules/@types/ws": {
206      "version": "8.18.1",
207      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
208      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
209      "license": "MIT",
210      "dependencies": {
211        "@types/node": "*"
212      }
213    },
214    "node_modules/@vladfrangu/async_event_emitter": {
215      "version": "2.4.7",
216      "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
217      "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
218      "license": "MIT",
219      "engines": {
220        "node": ">=v14.0.0",
221        "npm": ">=7.0.0"
222      }
223    },
224    "node_modules/discord-api-types": {
225      "version": "0.38.42",
226      "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz",
227      "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==",
228      "license": "MIT",
229      "workspaces": [
230        "scripts/actions/documentation"
231      ]
232    },
233    "node_modules/discord.js": {
234      "version": "14.25.1",
235      "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
236      "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
237      "license": "Apache-2.0",
238      "dependencies": {
239        "@discordjs/builders": "^1.13.0",
240        "@discordjs/collection": "1.5.3",
241        "@discordjs/formatters": "^0.6.2",
242        "@discordjs/rest": "^2.6.0",
243        "@discordjs/util": "^1.2.0",
244        "@discordjs/ws": "^1.2.3",
245        "@sapphire/snowflake": "3.5.3",
246        "discord-api-types": "^0.38.33",
247        "fast-deep-equal": "3.1.3",
248        "lodash.snakecase": "4.1.1",
249        "magic-bytes.js": "^1.10.0",
250        "tslib": "^2.6.3",
251        "undici": "6.21.3"
252      },
253      "engines": {
254        "node": ">=18"
255      },
256      "funding": {
257        "url": "https://github.com/discordjs/discord.js?sponsor"
258      }
259    },
260    "node_modules/fast-deep-equal": {
261      "version": "3.1.3",
262      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
263      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
264      "license": "MIT"
265    },
266    "node_modules/lodash": {
267      "version": "4.17.23",
268      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
269      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
270      "license": "MIT"
271    },
272    "node_modules/lodash.snakecase": {
273      "version": "4.1.1",
274      "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
275      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
276      "license": "MIT"
277    },
278    "node_modules/magic-bytes.js": {
279      "version": "1.13.0",
280      "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
281      "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
282      "license": "MIT"
283    },
284    "node_modules/ts-mixer": {
285      "version": "6.0.4",
286      "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
287      "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
288      "license": "MIT"
289    },
290    "node_modules/tslib": {
291      "version": "2.8.1",
292      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
293      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
294      "license": "0BSD"
295    },
296    "node_modules/undici": {
297      "version": "6.21.3",
298      "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
299      "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
300      "license": "MIT",
301      "engines": {
302        "node": ">=18.17"
303      }
304    },
305    "node_modules/undici-types": {
306      "version": "7.18.2",
307      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
308      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
309      "license": "MIT"
310    },
311    "node_modules/ws": {
312      "version": "8.20.0",
313      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
314      "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
315      "license": "MIT",
316      "engines": {
317        "node": ">=10.0.0"
318      },
319      "peerDependencies": {
320        "bufferutil": "^4.0.1",
321        "utf-8-validate": ">=5.0.2"
322      },
323      "peerDependenciesMeta": {
324        "bufferutil": {
325          "optional": true
326        },
327        "utf-8-validate": {
328          "optional": true
329        }
330      }
331    }
332  }
333}
334
1{
2  "type": "module",
3  "name": "treeos-ext-gateway-discord",
4  "version": "1.0.0",
5  "private": true,
6  "dependencies": {
7    "discord.js": "^14.25.1"
8  }
9}

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star gateway-discord

Comments

Loading comments...

Post comments from the CLI: treeos ext comment gateway-discord "your comment"
Max 3 comments per extension. One star and one flag per user.