Discord¶
Send messages to Discord channels and DMs as an agent, with your own bot. Receiving Discord messages needs a separate Gateway bridge service that this repository does not contain; without one, Discord is send-only.
What works out of the box, and what does not¶
| Works with just a bot token | Needs the Gateway bridge | |
|---|---|---|
Send a message or file to a channel or DM (through the approval gate, as channel.discord.send) |
yes | no |
| An agent answering people who message the bot | no | yes |
| Workflow and agent triggers on "Discord message received" | no | yes |
Discord has no public webhook for ordinary messages. Reading them means holding a persistent Gateway WebSocket
open with the bot's token, and that connection is held by a standalone Node service (referred to in the code and
the console as channels/discord) that forwards each message to core. That service is not part of this
repository, its compose files or the Helm chart, so on a stock install the console's Discord tile can send but
not receive.
Set it up (sending)¶
- At discord.com/developers/applications create an application, add a Bot, turn on Message Content Intent, and copy the bot token.
- Under OAuth2 > URL Generator select the
botscope with Send Messages and Read Message History and open the generated URL to invite the bot to your server. - In the console open Channels > Discord and paste the bot token. Core asks Discord who the bot is and stores the application id and name; the token is stored encrypted.
If you run your own Gateway bridge¶
Core defines the contract; the bridge is yours to write or obtain. Both settings must be set for core to talk to it:
| Property | Env | Meaning |
|---|---|---|
app.omnichannel.discord.base-url |
DISCORD_GATEWAY_SERVICE_URL |
Where the bridge listens. A private address must also be listed in app.security.outbound.allowed-private-hosts. |
app.omnichannel.discord.shared-secret |
DISCORD_GATEWAY_SHARED_SECRET |
One secret for both directions of the link. Empty means every inbound call is refused. |
- When a tenant connects a bot, core calls
POST {base-url}/bots/{applicationId}/startwith{"botToken": "..."}(andDELETE {base-url}/bots/{applicationId}on disconnect), sendingX-Internal-Secret: <shared secret>. The call is best-effort: if it fails, the connection still saves and sending still works. - For each
MESSAGE_CREATEthe bridge hears, it mustPOST https://<your-core-host>/webhook/discordwith the sameX-Internal-Secretheader and a JSON body{"applicationId", "channelId", "authorId", "content", "messageId"}(messageIdis optional but lets core drop a replay). A missing or wrong secret is answered 403 before anything is looked up. - A conversation is keyed by the Discord channel id, the same pairing rules apply as for every channel, and replies go out through core's own Discord REST client, never through the bridge.
Security¶
The shared secret is compared in constant time and the webhook fails closed when it is not configured. Because the secret is the only authentication, run the bridge on a private network and treat the secret like a password.