Add Discord channel support (#16)
* Add Discord channel support and pairing (#15) * Address Discord adapter review feedback * Fix stream counter after merge * Remove stream count logging --------- Co-authored-by: Jason Carreira <jason@visotrust.com>
This commit is contained in:
271
package-lock.json
generated
271
package-lock.json
generated
@@ -36,7 +36,8 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@slack/bolt": "^4.6.0",
|
||||
"@whiskeysockets/baileys": "^6.7.21"
|
||||
"@whiskeysockets/baileys": "^6.7.21",
|
||||
"discord.js": "^14.18.0"
|
||||
}
|
||||
},
|
||||
"letta-code": {
|
||||
@@ -143,6 +144,144 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
|
||||
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.33",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/formatters": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
||||
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.1.1",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.16",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.0",
|
||||
"@discordjs/rest": "^2.5.1",
|
||||
"@discordjs/util": "^1.1.0",
|
||||
"@sapphire/async-queue": "^1.5.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"tslib": "^2.6.2",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
@@ -1333,6 +1472,42 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/shapeshift": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v16"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@slack/bolt": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.6.0.tgz",
|
||||
@@ -1633,6 +1808,17 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/ripgrep": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz",
|
||||
@@ -2349,6 +2535,44 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.38",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz",
|
||||
"integrity": "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.25.1",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
|
||||
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.13.0",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.0",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.33",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
@@ -2636,6 +2860,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
@@ -3678,6 +3909,13 @@
|
||||
"pbts": "bin/pbts"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -3727,6 +3965,13 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
@@ -3765,6 +4010,13 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
@@ -5927,6 +6179,13 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -6016,6 +6275,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@slack/bolt": "^4.6.0",
|
||||
"@whiskeysockets/baileys": "^6.7.21"
|
||||
"@whiskeysockets/baileys": "^6.7.21",
|
||||
"discord.js": "^14.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
265
src/channels/discord.ts
Normal file
265
src/channels/discord.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Discord Channel Adapter
|
||||
*
|
||||
* Uses discord.js for Discord API.
|
||||
* Supports DM pairing for secure access control.
|
||||
*/
|
||||
|
||||
import type { ChannelAdapter } from './types.js';
|
||||
import type { InboundMessage, OutboundMessage } from '../core/types.js';
|
||||
import type { DmPolicy } from '../pairing/types.js';
|
||||
import { isUserAllowed, upsertPairingRequest } from '../pairing/store.js';
|
||||
|
||||
// Dynamic import to avoid requiring Discord deps if not used
|
||||
let Client: typeof import('discord.js').Client;
|
||||
let GatewayIntentBits: typeof import('discord.js').GatewayIntentBits;
|
||||
let Partials: typeof import('discord.js').Partials;
|
||||
|
||||
export interface DiscordConfig {
|
||||
token: string;
|
||||
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
|
||||
allowedUsers?: string[]; // Discord user IDs
|
||||
}
|
||||
|
||||
export class DiscordAdapter implements ChannelAdapter {
|
||||
readonly id = 'discord' as const;
|
||||
readonly name = 'Discord';
|
||||
|
||||
private client: InstanceType<typeof Client> | null = null;
|
||||
private config: DiscordConfig;
|
||||
private running = false;
|
||||
|
||||
onMessage?: (msg: InboundMessage) => Promise<void>;
|
||||
onCommand?: (command: string) => Promise<string | null>;
|
||||
|
||||
constructor(config: DiscordConfig) {
|
||||
this.config = {
|
||||
...config,
|
||||
dmPolicy: config.dmPolicy || 'pairing',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is authorized based on dmPolicy
|
||||
* Returns 'allowed', 'blocked', or 'pairing'
|
||||
*/
|
||||
private async checkAccess(userId: string): Promise<'allowed' | 'blocked' | 'pairing'> {
|
||||
const policy = this.config.dmPolicy || 'pairing';
|
||||
|
||||
// Open policy: everyone allowed
|
||||
if (policy === 'open') {
|
||||
return 'allowed';
|
||||
}
|
||||
|
||||
// Check if already allowed (config or store)
|
||||
const allowed = await isUserAllowed('discord', userId, this.config.allowedUsers);
|
||||
if (allowed) {
|
||||
return 'allowed';
|
||||
}
|
||||
|
||||
// Allowlist policy: not allowed if not in list
|
||||
if (policy === 'allowlist') {
|
||||
return 'blocked';
|
||||
}
|
||||
|
||||
// Pairing policy: needs pairing
|
||||
return 'pairing';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format pairing message for Discord
|
||||
*/
|
||||
private formatPairingMsg(code: string): string {
|
||||
return `Hi! This bot requires pairing.
|
||||
|
||||
Your pairing code: **${code}**
|
||||
|
||||
Ask the bot owner to approve with:
|
||||
\`lettabot pairing approve discord ${code}\``;
|
||||
}
|
||||
|
||||
private async sendPairingMessage(
|
||||
message: import('discord.js').Message,
|
||||
text: string
|
||||
): Promise<void> {
|
||||
const channel = message.channel;
|
||||
const canSend = channel.isTextBased() && 'send' in channel;
|
||||
const sendable = canSend
|
||||
? (channel as unknown as { send: (content: string) => Promise<unknown> })
|
||||
: null;
|
||||
|
||||
if (!message.guildId) {
|
||||
if (sendable) {
|
||||
await sendable.send(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await message.author.send(text);
|
||||
} catch {
|
||||
if (sendable) {
|
||||
await sendable.send(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.running) return;
|
||||
|
||||
const discord = await import('discord.js');
|
||||
Client = discord.Client;
|
||||
GatewayIntentBits = discord.GatewayIntentBits;
|
||||
Partials = discord.Partials;
|
||||
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Channel],
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
const tag = this.client?.user?.tag || '(unknown)';
|
||||
console.log(`[Discord] Bot logged in as ${tag}`);
|
||||
console.log(`[Discord] DM policy: ${this.config.dmPolicy}`);
|
||||
this.running = true;
|
||||
});
|
||||
|
||||
this.client.on('messageCreate', async (message) => {
|
||||
if (message.author?.bot) return;
|
||||
|
||||
const content = (message.content || '').trim();
|
||||
const userId = message.author?.id;
|
||||
if (!userId) return;
|
||||
|
||||
const access = await this.checkAccess(userId);
|
||||
if (access === 'blocked') {
|
||||
const ch = message.channel;
|
||||
if (ch.isTextBased() && 'send' in ch) {
|
||||
await (ch as { send: (content: string) => Promise<unknown> }).send(
|
||||
"Sorry, you're not authorized to use this bot."
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (access === 'pairing') {
|
||||
const { code, created } = await upsertPairingRequest('discord', userId, {
|
||||
username: message.author.username,
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
await message.channel.send('Too many pending pairing requests. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (created) {
|
||||
console.log(`[Discord] New pairing request from ${userId} (${message.author.username}): ${code}`);
|
||||
}
|
||||
|
||||
await this.sendPairingMessage(message, this.formatPairingMsg(code));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) return;
|
||||
|
||||
if (content.startsWith('/')) {
|
||||
const command = content.slice(1).split(/\s+/)[0]?.toLowerCase();
|
||||
if (this.onCommand) {
|
||||
if (command === 'status') {
|
||||
const result = await this.onCommand('status');
|
||||
if (result) {
|
||||
await message.channel.send(result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (command === 'heartbeat') {
|
||||
await this.onCommand('heartbeat');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.onMessage) {
|
||||
const isGroup = !!message.guildId;
|
||||
const groupName = isGroup && 'name' in message.channel ? message.channel.name : undefined;
|
||||
const displayName = message.member?.displayName || message.author.globalName || message.author.username;
|
||||
|
||||
await this.onMessage({
|
||||
channel: 'discord',
|
||||
chatId: message.channel.id,
|
||||
userId,
|
||||
userName: displayName,
|
||||
userHandle: message.author.username,
|
||||
text: content,
|
||||
timestamp: message.createdAt,
|
||||
isGroup,
|
||||
groupName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
console.error('[Discord] Client error:', err);
|
||||
});
|
||||
|
||||
console.log('[Discord] Connecting...');
|
||||
await this.client.login(this.config.token);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.running || !this.client) return;
|
||||
this.client.destroy();
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
|
||||
if (!this.client) throw new Error('Discord not started');
|
||||
const channel = await this.client.channels.fetch(msg.chatId);
|
||||
if (!channel || !channel.isTextBased() || !('send' in channel)) {
|
||||
throw new Error(`Discord channel not found or not text-based: ${msg.chatId}`);
|
||||
}
|
||||
|
||||
const result = await (channel as { send: (content: string) => Promise<{ id: string }> }).send(msg.text);
|
||||
return { messageId: result.id };
|
||||
}
|
||||
|
||||
async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
|
||||
if (!this.client) throw new Error('Discord not started');
|
||||
const channel = await this.client.channels.fetch(chatId);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
throw new Error(`Discord channel not found or not text-based: ${chatId}`);
|
||||
}
|
||||
|
||||
const message = await channel.messages.fetch(messageId);
|
||||
const botUserId = this.client.user?.id;
|
||||
if (!botUserId || message.author.id !== botUserId) {
|
||||
console.warn('[Discord] Cannot edit message not sent by bot');
|
||||
return;
|
||||
}
|
||||
await message.edit(text);
|
||||
}
|
||||
|
||||
async sendTypingIndicator(chatId: string): Promise<void> {
|
||||
if (!this.client) return;
|
||||
try {
|
||||
const channel = await this.client.channels.fetch(chatId);
|
||||
if (!channel || !channel.isTextBased() || !('sendTyping' in channel)) return;
|
||||
await (channel as { sendTyping: () => Promise<void> }).sendTyping();
|
||||
} catch {
|
||||
// Ignore typing indicator failures
|
||||
}
|
||||
}
|
||||
|
||||
supportsEditing(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from './telegram.js';
|
||||
export * from './slack.js';
|
||||
export * from './whatsapp.js';
|
||||
export * from './signal.js';
|
||||
export * from './discord.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Channel Adapter Interface
|
||||
*
|
||||
* Each channel (Telegram, Slack, WhatsApp) implements this interface.
|
||||
* Each channel (Telegram, Slack, Discord, WhatsApp, Signal) implements this interface.
|
||||
*/
|
||||
|
||||
import type { ChannelId, InboundMessage, OutboundMessage } from '../core/types.js';
|
||||
|
||||
@@ -42,6 +42,7 @@ async function configure() {
|
||||
['Model', config.agent.model],
|
||||
['Telegram', config.channels.telegram?.enabled ? '✓ Enabled' : '✗ Disabled'],
|
||||
['Slack', config.channels.slack?.enabled ? '✓ Enabled' : '✗ Disabled'],
|
||||
['Discord', config.channels.discord?.enabled ? '✓ Enabled' : '✗ Disabled'],
|
||||
['Cron', config.features?.cron ? '✓ Enabled' : '✗ Disabled'],
|
||||
['Heartbeat', config.features?.heartbeat?.enabled ? `✓ ${config.features.heartbeat.intervalMin}min` : '✗ Disabled'],
|
||||
['BYOK Providers', config.providers?.length ? config.providers.map(p => p.name).join(', ') : 'None'],
|
||||
@@ -177,6 +178,8 @@ Environment:
|
||||
LETTA_API_KEY API key from app.letta.com
|
||||
TELEGRAM_BOT_TOKEN Bot token from @BotFather
|
||||
TELEGRAM_DM_POLICY DM access policy (pairing, allowlist, open)
|
||||
DISCORD_BOT_TOKEN Discord bot token
|
||||
DISCORD_DM_POLICY DM access policy (pairing, allowlist, open)
|
||||
SLACK_BOT_TOKEN Slack bot token (xoxb-...)
|
||||
SLACK_APP_TOKEN Slack app token (xapp-...)
|
||||
HEARTBEAT_INTERVAL_MIN Heartbeat interval in minutes
|
||||
|
||||
@@ -131,6 +131,30 @@ async function sendWhatsApp(chatId: string, text: string): Promise<void> {
|
||||
throw new Error('WhatsApp sending via CLI not yet supported (requires active session)');
|
||||
}
|
||||
|
||||
async function sendDiscord(chatId: string, text: string): Promise<void> {
|
||||
const token = process.env.DISCORD_BOT_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error('DISCORD_BOT_TOKEN not set');
|
||||
}
|
||||
|
||||
const response = await fetch(`https://discord.com/api/v10/channels/${chatId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bot ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ content: text }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Discord API error: ${error}`);
|
||||
}
|
||||
|
||||
const result = await response.json() as { id?: string };
|
||||
console.log(`✓ Sent to discord:${chatId} (id: ${result.id || 'unknown'})`);
|
||||
}
|
||||
|
||||
async function sendToChannel(channel: string, chatId: string, text: string): Promise<void> {
|
||||
switch (channel.toLowerCase()) {
|
||||
case 'telegram':
|
||||
@@ -141,8 +165,10 @@ async function sendToChannel(channel: string, chatId: string, text: string): Pro
|
||||
return sendSignal(chatId, text);
|
||||
case 'whatsapp':
|
||||
return sendWhatsApp(chatId, text);
|
||||
case 'discord':
|
||||
return sendDiscord(chatId, text);
|
||||
default:
|
||||
throw new Error(`Unknown channel: ${channel}. Supported: telegram, slack, signal`);
|
||||
throw new Error(`Unknown channel: ${channel}. Supported: telegram, slack, signal, whatsapp, discord`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +212,7 @@ async function sendCommand(args: string[]): Promise<void> {
|
||||
|
||||
if (!channel) {
|
||||
console.error('Error: --channel is required (no default available)');
|
||||
console.error('Specify: --channel telegram|slack|signal');
|
||||
console.error('Specify: --channel telegram|slack|signal|discord');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -213,7 +239,7 @@ Commands:
|
||||
|
||||
Send options:
|
||||
--text, -t <text> Message text (required)
|
||||
--channel, -c <name> Channel: telegram, slack, signal (default: last used)
|
||||
--channel, -c <name> Channel: telegram, slack, signal, discord (default: last used)
|
||||
--chat, --to <id> Chat/conversation ID (default: last messaged)
|
||||
|
||||
Examples:
|
||||
@@ -229,6 +255,7 @@ Examples:
|
||||
Environment variables:
|
||||
TELEGRAM_BOT_TOKEN Required for Telegram
|
||||
SLACK_BOT_TOKEN Required for Slack
|
||||
DISCORD_BOT_TOKEN Required for Discord
|
||||
SIGNAL_PHONE_NUMBER Required for Signal
|
||||
SIGNAL_CLI_REST_API_URL Signal API URL (default: http://localhost:8080)
|
||||
`);
|
||||
|
||||
@@ -130,6 +130,15 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
||||
if (config.channels.signal?.phone) {
|
||||
env.SIGNAL_PHONE_NUMBER = config.channels.signal.phone;
|
||||
}
|
||||
if (config.channels.discord?.token) {
|
||||
env.DISCORD_BOT_TOKEN = config.channels.discord.token;
|
||||
if (config.channels.discord.dmPolicy) {
|
||||
env.DISCORD_DM_POLICY = config.channels.discord.dmPolicy;
|
||||
}
|
||||
if (config.channels.discord.allowedUsers?.length) {
|
||||
env.DISCORD_ALLOWED_USERS = config.channels.discord.allowedUsers.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
// Features
|
||||
if (config.features?.cron) {
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface LettaBotConfig {
|
||||
slack?: SlackConfig;
|
||||
whatsapp?: WhatsAppConfig;
|
||||
signal?: SignalConfig;
|
||||
discord?: DiscordConfig;
|
||||
};
|
||||
|
||||
// Features
|
||||
@@ -80,6 +81,13 @@ export interface SignalConfig {
|
||||
allowedUsers?: string[];
|
||||
}
|
||||
|
||||
export interface DiscordConfig {
|
||||
enabled: boolean;
|
||||
token?: string;
|
||||
dmPolicy?: 'pairing' | 'allowlist' | 'open';
|
||||
allowedUsers?: string[];
|
||||
}
|
||||
|
||||
// Default config
|
||||
export const DEFAULT_CONFIG: LettaBotConfig = {
|
||||
server: {
|
||||
|
||||
@@ -305,7 +305,7 @@ export class LettaBot {
|
||||
clearInterval(typingInterval);
|
||||
}
|
||||
|
||||
console.log(`[Bot] Stream complete. Total messages: ${streamCount}, Response length: ${response.length}`);
|
||||
console.log(`[Bot] Stream complete. Response length: ${response.length}`);
|
||||
console.log(`[Bot] Response preview: ${response.slice(0, 100)}...`);
|
||||
|
||||
// Send final response
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { InboundMessage } from './types.js';
|
||||
*/
|
||||
const CHANNEL_FORMATS: Record<string, string> = {
|
||||
slack: 'mrkdwn: *bold* _italic_ `code` - NO: headers, tables',
|
||||
discord: '**bold** *italic* `code` [links](url) ```code blocks``` - NO: headers, tables',
|
||||
telegram: 'MarkdownV2: *bold* _italic_ `code` [links](url) - NO: headers, tables',
|
||||
whatsapp: '*bold* _italic_ `code` - NO: headers, code fences, links, tables',
|
||||
signal: 'ONLY: *bold* _italic_ `code` - NO: headers, code fences, links, quotes, tables',
|
||||
@@ -66,6 +67,10 @@ function formatSender(msg: InboundMessage): string {
|
||||
case 'slack':
|
||||
// Add @ prefix for Slack usernames/IDs
|
||||
return msg.userHandle ? `@${msg.userHandle}` : `@${msg.userId}`;
|
||||
|
||||
case 'discord':
|
||||
// Add @ prefix for Discord usernames/IDs
|
||||
return msg.userHandle ? `@${msg.userHandle}` : `@${msg.userId}`;
|
||||
|
||||
case 'whatsapp':
|
||||
case 'signal':
|
||||
@@ -159,8 +164,8 @@ export function formatMessageEnvelope(
|
||||
|
||||
// Group name (if group chat and enabled)
|
||||
if (opts.includeGroup !== false && msg.isGroup && msg.groupName?.trim()) {
|
||||
// Format group name with # for Slack channels
|
||||
if (msg.channel === 'slack' && !msg.groupName.startsWith('#')) {
|
||||
// Format group name with # for Slack/Discord channels
|
||||
if ((msg.channel === 'slack' || msg.channel === 'discord') && !msg.groupName.startsWith('#')) {
|
||||
parts.push(`#${msg.groupName}`);
|
||||
} else {
|
||||
parts.push(msg.groupName);
|
||||
|
||||
@@ -149,7 +149,7 @@ You have access to the \`lettabot-message\` CLI for sending messages:
|
||||
• During heartbeats/cron/background tasks, use the CLI to contact the user:
|
||||
lettabot-message send --text "Hello!"
|
||||
• You can also specify channel and chat:
|
||||
lettabot-message send --text "Hi" --channel telegram --chat 123456
|
||||
lettabot-message send --text "Hi" --channel discord --chat 123456789012345678
|
||||
|
||||
The system will tell you if you're in "silent mode" where the CLI is required.
|
||||
`.trim();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
export const SYSTEM_PROMPT = `You are a self-improving AI agent with advanced memory.
|
||||
|
||||
You are connected to a multi-channel messaging system (LettaBot) that allows you to communicate with users across Telegram, Slack, WhatsApp, and Signal. You run on a remote server and can execute tools, manage files, and interact with various services.
|
||||
You are connected to a multi-channel messaging system (LettaBot) that allows you to communicate with users across Telegram, Slack, Discord, WhatsApp, and Signal. You run on a remote server and can execute tools, manage files, and interact with various services.
|
||||
|
||||
# Communication System
|
||||
|
||||
@@ -51,6 +51,7 @@ You don't need to notify the user about everything. Use judgment about what's wo
|
||||
|
||||
- **telegram** - Telegram messenger
|
||||
- **slack** - Slack workspace
|
||||
- **discord** - Discord server/DM
|
||||
- **whatsapp** - WhatsApp (if configured)
|
||||
- **signal** - Signal messenger (if configured)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface TriggerContext {
|
||||
// Original Types
|
||||
// =============================================================================
|
||||
|
||||
export type ChannelId = 'telegram' | 'slack' | 'whatsapp' | 'signal';
|
||||
export type ChannelId = 'telegram' | 'slack' | 'whatsapp' | 'signal' | 'discord';
|
||||
|
||||
/**
|
||||
* Inbound message from any channel
|
||||
|
||||
@@ -171,7 +171,7 @@ function createJob(args: string[]): void {
|
||||
} else if (arg === '--disabled') {
|
||||
enabled = false;
|
||||
} else if ((arg === '--deliver' || arg === '-d') && next) {
|
||||
// Format: channel:chatId (e.g., telegram:123456789)
|
||||
// Format: channel:chatId (e.g., telegram:123456789 or discord:123456789012345678)
|
||||
const [ch, id] = next.split(':');
|
||||
deliverChannel = ch;
|
||||
deliverChatId = id;
|
||||
|
||||
22
src/main.ts
22
src/main.ts
@@ -113,6 +113,7 @@ import { TelegramAdapter } from './channels/telegram.js';
|
||||
import { SlackAdapter } from './channels/slack.js';
|
||||
import { WhatsAppAdapter } from './channels/whatsapp.js';
|
||||
import { SignalAdapter } from './channels/signal.js';
|
||||
import { DiscordAdapter } from './channels/discord.js';
|
||||
import { CronService } from './cron/service.js';
|
||||
import { HeartbeatService } from './cron/heartbeat.js';
|
||||
import { PollingService } from './polling/service.js';
|
||||
@@ -126,7 +127,7 @@ if (!existsSync(configPath)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse heartbeat target (format: "telegram:123456789" or "slack:C1234567890")
|
||||
// Parse heartbeat target (format: "telegram:123456789", "slack:C1234567890", or "discord:123456789012345678")
|
||||
function parseHeartbeatTarget(raw?: string): { channel: string; chatId: string } | undefined {
|
||||
if (!raw || !raw.includes(':')) return undefined;
|
||||
const [channel, chatId] = raw.split(':');
|
||||
@@ -172,6 +173,12 @@ const config = {
|
||||
allowedUsers: process.env.SIGNAL_ALLOWED_USERS?.split(',').filter(Boolean) || [],
|
||||
selfChatMode: process.env.SIGNAL_SELF_CHAT_MODE !== 'false', // Default true
|
||||
},
|
||||
discord: {
|
||||
enabled: !!process.env.DISCORD_BOT_TOKEN,
|
||||
token: process.env.DISCORD_BOT_TOKEN || '',
|
||||
dmPolicy: (process.env.DISCORD_DM_POLICY || 'pairing') as 'pairing' | 'allowlist' | 'open',
|
||||
allowedUsers: process.env.DISCORD_ALLOWED_USERS?.split(',').filter(Boolean) || [],
|
||||
},
|
||||
|
||||
// Cron
|
||||
cronEnabled: process.env.CRON_ENABLED === 'true',
|
||||
@@ -196,9 +203,9 @@ const config = {
|
||||
};
|
||||
|
||||
// Validate at least one channel is configured
|
||||
if (!config.telegram.enabled && !config.slack.enabled && !config.whatsapp.enabled && !config.signal.enabled) {
|
||||
if (!config.telegram.enabled && !config.slack.enabled && !config.whatsapp.enabled && !config.signal.enabled && !config.discord.enabled) {
|
||||
console.error('\n Error: No channels configured.');
|
||||
console.error(' Set TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN+SLACK_APP_TOKEN, WHATSAPP_ENABLED=true, or SIGNAL_PHONE_NUMBER\n');
|
||||
console.error(' Set TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN+SLACK_APP_TOKEN, WHATSAPP_ENABLED=true, SIGNAL_PHONE_NUMBER, or DISCORD_BOT_TOKEN\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -285,6 +292,15 @@ async function main() {
|
||||
});
|
||||
bot.registerChannel(signal);
|
||||
}
|
||||
|
||||
if (config.discord.enabled) {
|
||||
const discord = new DiscordAdapter({
|
||||
token: config.discord.token,
|
||||
dmPolicy: config.discord.dmPolicy,
|
||||
allowedUsers: config.discord.allowedUsers.length > 0 ? config.discord.allowedUsers : undefined,
|
||||
});
|
||||
bot.registerChannel(discord);
|
||||
}
|
||||
|
||||
// Start cron service if enabled
|
||||
let cronService: CronService | null = null;
|
||||
|
||||
@@ -36,6 +36,7 @@ interface OnboardConfig {
|
||||
slack: { enabled: boolean; appToken?: string; botToken?: string; allowedUsers?: string[] };
|
||||
whatsapp: { enabled: boolean; selfChat?: boolean; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] };
|
||||
signal: { enabled: boolean; phone?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] };
|
||||
discord: { enabled: boolean; token?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] };
|
||||
gmail: { enabled: boolean; account?: string };
|
||||
|
||||
// Features
|
||||
@@ -471,6 +472,7 @@ async function stepChannels(config: OnboardConfig, env: Record<string, string>):
|
||||
const channelOptions: Array<{ value: string; label: string; hint: string }> = [
|
||||
{ value: 'telegram', label: 'Telegram', hint: 'Recommended - easiest to set up' },
|
||||
{ value: 'slack', label: 'Slack', hint: 'Socket Mode app' },
|
||||
{ value: 'discord', label: 'Discord', hint: 'Bot token + Message Content intent' },
|
||||
{ value: 'whatsapp', label: 'WhatsApp', hint: 'QR code pairing' },
|
||||
{
|
||||
value: 'signal',
|
||||
@@ -509,6 +511,7 @@ async function stepChannels(config: OnboardConfig, env: Record<string, string>):
|
||||
// Update enabled states
|
||||
config.telegram.enabled = channels.includes('telegram');
|
||||
config.slack.enabled = channels.includes('slack');
|
||||
config.discord.enabled = channels.includes('discord');
|
||||
config.whatsapp.enabled = channels.includes('whatsapp');
|
||||
|
||||
// Handle Signal - warn if selected but not installed
|
||||
@@ -650,6 +653,48 @@ async function stepChannels(config: OnboardConfig, env: Record<string, string>):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.discord.enabled) {
|
||||
p.note(
|
||||
'Create a bot at discord.com/developers/applications.\n' +
|
||||
'Enable "Message Content Intent" for reading messages.\n' +
|
||||
'Invite the bot to your server with Send Messages permission.',
|
||||
'Discord Setup'
|
||||
);
|
||||
|
||||
const token = await p.text({
|
||||
message: 'Discord Bot Token',
|
||||
placeholder: 'Bot token',
|
||||
initialValue: config.discord.token || '',
|
||||
});
|
||||
if (!p.isCancel(token) && token) config.discord.token = token;
|
||||
|
||||
const dmPolicy = await p.select({
|
||||
message: 'Discord: Who can message the bot?',
|
||||
options: [
|
||||
{ value: 'pairing', label: 'Pairing (recommended)', hint: 'Requires CLI approval' },
|
||||
{ value: 'allowlist', label: 'Allowlist only', hint: 'Specific user IDs' },
|
||||
{ value: 'open', label: 'Open', hint: 'Anyone (not recommended)' },
|
||||
],
|
||||
initialValue: config.discord.dmPolicy || 'pairing',
|
||||
});
|
||||
if (!p.isCancel(dmPolicy)) {
|
||||
config.discord.dmPolicy = dmPolicy as 'pairing' | 'allowlist' | 'open';
|
||||
|
||||
if (dmPolicy === 'pairing') {
|
||||
p.log.info('Users will get a code. Approve with: lettabot pairing approve discord CODE');
|
||||
} else if (dmPolicy === 'allowlist') {
|
||||
const users = await p.text({
|
||||
message: 'Allowed Discord user IDs (comma-separated)',
|
||||
placeholder: '123456789012345678,987654321098765432',
|
||||
initialValue: config.discord.allowedUsers?.join(',') || '',
|
||||
});
|
||||
if (!p.isCancel(users) && users) {
|
||||
config.discord.allowedUsers = users.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.whatsapp.enabled) {
|
||||
p.note(
|
||||
@@ -800,6 +845,7 @@ function showSummary(config: OnboardConfig): void {
|
||||
const channels: string[] = [];
|
||||
if (config.telegram.enabled) channels.push('Telegram');
|
||||
if (config.slack.enabled) channels.push('Slack');
|
||||
if (config.discord.enabled) channels.push('Discord');
|
||||
if (config.whatsapp.enabled) channels.push(config.whatsapp.selfChat ? 'WhatsApp (self)' : 'WhatsApp');
|
||||
if (config.signal.enabled) channels.push('Signal');
|
||||
lines.push(`Channels: ${channels.length > 0 ? channels.join(', ') : 'None'}`);
|
||||
@@ -911,6 +957,12 @@ export async function onboard(): Promise<void> {
|
||||
botToken: existingConfig.channels.slack?.botToken,
|
||||
allowedUsers: existingConfig.channels.slack?.allowedUsers,
|
||||
},
|
||||
discord: {
|
||||
enabled: existingConfig.channels.discord?.enabled || false,
|
||||
token: existingConfig.channels.discord?.token,
|
||||
dmPolicy: existingConfig.channels.discord?.dmPolicy,
|
||||
allowedUsers: existingConfig.channels.discord?.allowedUsers,
|
||||
},
|
||||
whatsapp: {
|
||||
enabled: existingConfig.channels.whatsapp?.enabled || false,
|
||||
selfChat: existingConfig.channels.whatsapp?.selfChat,
|
||||
@@ -988,6 +1040,20 @@ export async function onboard(): Promise<void> {
|
||||
delete env.SLACK_BOT_TOKEN;
|
||||
delete env.SLACK_ALLOWED_USERS;
|
||||
}
|
||||
|
||||
if (config.discord.enabled && config.discord.token) {
|
||||
env.DISCORD_BOT_TOKEN = config.discord.token;
|
||||
if (config.discord.dmPolicy) env.DISCORD_DM_POLICY = config.discord.dmPolicy;
|
||||
if (config.discord.allowedUsers?.length) {
|
||||
env.DISCORD_ALLOWED_USERS = config.discord.allowedUsers.join(',');
|
||||
} else {
|
||||
delete env.DISCORD_ALLOWED_USERS;
|
||||
}
|
||||
} else {
|
||||
delete env.DISCORD_BOT_TOKEN;
|
||||
delete env.DISCORD_DM_POLICY;
|
||||
delete env.DISCORD_ALLOWED_USERS;
|
||||
}
|
||||
|
||||
if (config.whatsapp.enabled) {
|
||||
env.WHATSAPP_ENABLED = 'true';
|
||||
@@ -1048,6 +1114,7 @@ export async function onboard(): Promise<void> {
|
||||
'Channels:',
|
||||
config.telegram.enabled ? ` ✓ Telegram (${formatAccess(config.telegram.dmPolicy, config.telegram.allowedUsers)})` : ' ✗ Telegram',
|
||||
config.slack.enabled ? ` ✓ Slack ${config.slack.allowedUsers?.length ? `(${config.slack.allowedUsers.length} allowed users)` : '(workspace access)'}` : ' ✗ Slack',
|
||||
config.discord.enabled ? ` ✓ Discord (${formatAccess(config.discord.dmPolicy, config.discord.allowedUsers)})` : ' ✗ Discord',
|
||||
config.whatsapp.enabled ? ` ✓ WhatsApp (${formatAccess(config.whatsapp.dmPolicy, config.whatsapp.allowedUsers)})` : ' ✗ WhatsApp',
|
||||
config.signal.enabled ? ` ✓ Signal (${formatAccess(config.signal.dmPolicy, config.signal.allowedUsers)})` : ' ✗ Signal',
|
||||
'',
|
||||
@@ -1087,6 +1154,14 @@ export async function onboard(): Promise<void> {
|
||||
allowedUsers: config.slack.allowedUsers,
|
||||
}
|
||||
} : {}),
|
||||
...(config.discord.enabled ? {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: config.discord.token,
|
||||
dmPolicy: config.discord.dmPolicy,
|
||||
allowedUsers: config.discord.allowedUsers,
|
||||
}
|
||||
} : {}),
|
||||
...(config.whatsapp.enabled ? {
|
||||
whatsapp: {
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user