@loomcycle/bridge-discord 0.2.0 → 0.4.0

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.
package/README.md CHANGED
@@ -1,17 +1,18 @@
1
1
  # @loomcycle/bridge-discord
2
2
 
3
- A Discord bot for a [loomcycle](https://github.com/denn-gubsky/loomcycle) interactive agent. People talk to the agent with slash commands. In a DM each person is their own loomcycle user, with their own runs, memory, history, budget and rate limit; a server channel is a loomcycle user of its own.
3
+ A Discord bot for a [loomcycle](https://github.com/denn-gubsky/loomcycle) interactive agent. People talk to the agent with slash commands. In a DM each person is their own loomcycle user, with their own runs, memory, history and rate limit, and a token budget of their own when loomcycle holds one for them (the gate's `new_user.budget`, below); a server channel is a loomcycle user of its own.
4
4
 
5
5
  - **Slash commands:** `/ask prompt:<text>` talks to the agent, `/new` starts over, `/stop` stops the current answer, `/help` lists them.
6
6
  - **Steering:** an `/ask` sent while the agent is working reaches it mid-turn.
7
7
  - **Questions and reviews** from the agent arrive as messages with buttons.
8
8
  - **Restarts lose nothing:** the next `/ask` continues the conversation, with the agent's full history.
9
+ - **Voice messages** (opt-in): ask with your own voice message through its **Apps → Ask by voice** menu, and hear the reply as a Discord voice message (below).
9
10
 
10
11
  It is built on `@loomcycle/bridge-core`, which holds the per-user tokens, conversations and cards every bridge shares. Design: RFC EQ (Bridges) in the loomcycle document store.
11
12
 
12
13
  ## What v1 does not do
13
14
 
14
- The bridge takes Discord **interactions over HTTP** only. It does not connect to the Gateway, so it never sees ordinary messages: people use `/ask`, in a server or in a DM. Free-text messages need the Gateway and are a later phase.
15
+ The bridge takes Discord **interactions over HTTP** only. It does not connect to the Gateway, so it never sees ordinary messages: people use `/ask`, in a server or in a DM. Free-text messages, and voice messages sent without the **Ask by voice** command, need the Gateway and are a later phase.
15
16
 
16
17
  ## How it works
17
18
 
@@ -45,7 +46,7 @@ Run loomcycle with authentication on and a persistent store. Then:
45
46
  1. In the [Developer Portal](https://discord.com/developers/applications), create an application.
46
47
  2. **General Information:** copy the **Application ID** (`DISCORD_APPLICATION_ID`) and the **Public Key** (`DISCORD_PUBLIC_KEY`).
47
48
  3. **Bot:** reset and copy the token (`DISCORD_BOT_TOKEN`). The bot needs no privileged intents.
48
- 4. **Installation:** keep **Guild Install** only. For the install link, add the scopes `bot` and `applications.commands`, and the bot permissions *Send Messages* and *View Channels*. Install it in your server.
49
+ 4. **Installation:** keep **Guild Install** only. For the install link, add the scopes `bot` and `applications.commands`, and the bot permissions *Send Messages* and *View Channels*. With voice messages on, also *Send Voice Messages* and *Attach Files* (permissions integer `70368744213504`, against `3072` without voice). Install it in your server.
49
50
  5. Start the bridge (below), then set **Interactions Endpoint URL** to `https://<bridge-host>/discord/interactions`. Discord checks the URL with a PING when you save it, so the bridge must be running and reachable; it also probes the endpoint with bad signatures from time to time.
50
51
 
51
52
  ### 3. Run the bridge
@@ -60,7 +61,7 @@ Or run the container image, `denngubsky/loomcycle-bridge-discord` (also `ghcr.io
60
61
  docker run --env-file discord-bridge.env -p 8080:8080 -v discord-bridge-data:/data denngubsky/loomcycle-bridge-discord:<version>
61
62
  ```
62
63
 
63
- With `DISCORD_BRIDGE_REGISTER_COMMANDS=1` the bridge registers its four commands when it starts:
64
+ With `DISCORD_BRIDGE_REGISTER_COMMANDS=1` the bridge registers its four commands when it starts, and with `BRIDGE_VOICE=1` the **Ask by voice** message command beside them:
64
65
 
65
66
  - **in each allowed server, as that server's commands.** They take effect at once and show only where the bot answers.
66
67
  - **globally, limited to the bot's DMs,** when DMs are on. Commands in DMs can only be global, and limiting them to DMs keeps a server from listing them twice.
@@ -73,15 +74,15 @@ The shared settings, as every bridge reads them:
73
74
 
74
75
  | Variable | Meaning | Default |
75
76
  |---|---|---|
76
- | `LOOMCYCLE_BASE_URL` | loomcycle's private URL | required |
77
- | `LOOMCYCLE_TOKEN` | The bridge's `substrate:tenant` token | required |
77
+ | `LOOMCYCLE_BASE_URL` | loomcycle's URL. From `@loomcycle/bridge-core` 0.4.0: `https://`, or `http://` to localhost; over a private network set `BRIDGE_ALLOW_INSECURE_LOOMCYCLE=1` | required |
78
+ | `LOOMCYCLE_TOKEN` | The bridge's `substrate:tenant` token | required, unless the bridge is a gate (below) |
78
79
  | `BRIDGE_AGENT` | The loomcycle agent every conversation runs | required |
79
80
  | `BRIDGE_TOKEN_TTL_SECONDS` | Lifetime of a user's token | `43200` |
80
81
  | `BRIDGE_RATE_PER_MINUTE`, `BRIDGE_RATE_BURST` | Runs a user may start per minute, and the burst | `6`, `10` |
81
82
  | `BRIDGE_USER_ACCESS_MODE` | `isolated` (a user sees only their own data) or `tenant` | `isolated` |
82
83
  | `BRIDGE_IDLE_MINUTES` | End a conversation's run after this long without a message; the next message continues it | `1440` |
83
84
  | `BRIDGE_STORE` | `memory`, or `file:<path>` to survive restarts on one instance | `memory` |
84
- | `BRIDGE_DEV` | `1` turns signature verification off, for local testing only | unset |
85
+ | `BRIDGE_DEV` | `1` turns signature verification off, for local testing only. From `@loomcycle/bridge-core` 0.4.0 it is refused on a gate, and the listener binds loopback only | unset |
85
86
  | `PORT` | Listen port | `8080` |
86
87
 
87
88
  Discord's:
@@ -97,7 +98,76 @@ Discord's:
97
98
  | `DISCORD_BRIDGE_REGISTER_COMMANDS` | `1` registers the commands at start | off |
98
99
  | `DISCORD_BRIDGE_API_BASE_URL` | Discord's REST API (`https`, or `http` to localhost for a test fake) | `https://discord.com/api/v10` |
99
100
 
100
- **The bridge is closed by default.** It refuses to start unless `DISCORD_BRIDGE_ALLOWED_GUILDS` or `DISCORD_BRIDGE_ALLOW_DMS=1` opens something: anyone on the internet can reach a Discord bot, and every message it answers runs on your model keys. `DISCORD_BRIDGE_ALLOW_DMS=1` means exactly that, so pair it with the rate limit and loomcycle's token budgets.
101
+ **The bridge is closed by default.** It refuses to start unless `DISCORD_BRIDGE_ALLOWED_GUILDS` or `DISCORD_BRIDGE_ALLOW_DMS=1` opens something: anyone on the internet can reach a Discord bot, and every message it answers runs on your model keys. `DISCORD_BRIDGE_ALLOW_DMS=1` means exactly that, so pair it with the rate limit and loomcycle's token budgets: a tenant budget, and, as a gate, `new_user.budget` (or `provision: known`, or `new_user.status: pending` to approve each person).
102
+
103
+ ## As a loomcycle gate
104
+
105
+ Since 0.3 the bridge can be a loomcycle gate instead of holding a token: it signs who is talking with its own key, and loomcycle provisions the user and issues the token. A stolen bridge can then only speak for Discord users, never act as a tenant operator. Set `BRIDGE_GATE` and the bridge runs this way; without it, it runs with `LOOMCYCLE_TOKEN` as before.
106
+
107
+ 1. Make a key pair. The private half goes to the bridge only:
108
+
109
+ ```bash
110
+ node -e 'const {privateKey,publicKey}=require("node:crypto").generateKeyPairSync("ed25519");
111
+ console.log("BRIDGE_GATE_KEY=" + JSON.stringify(privateKey.export({format:"jwk"})));
112
+ console.log("public_key: " + JSON.stringify(publicKey.export({format:"jwk"})))'
113
+ ```
114
+
115
+ 2. Register the gate in loomcycle, in the tenant the bridge used:
116
+
117
+ ```yaml
118
+ auth:
119
+ gates:
120
+ discord:
121
+ kind: external
122
+ public_key: { kty: OKP, crv: Ed25519, x: "<the public x>" }
123
+ issued_token: { ttl: 12h, agents: [chat], overrides: [interactive], rate: { per_minute: 6, burst: 10 } }
124
+ identity_map:
125
+ tenant: acme
126
+ provision: auto
127
+ legacy_subject_prefix: discord-
128
+ new_user: { budget: { soft_limit: 500000, hard_limit: 1000000 } } # tokens a month, per user
129
+ ```
130
+
131
+ `issued_token` is required, or the gate does not load (write `issued_token: unrestricted` for tokens with the user's whole member reach). `new_user.budget` becomes each new user's token limit, so people the bot admits are capped without a limit written for each; leave it out and they have none.
132
+
133
+ `legacy_subject_prefix: discord-` keeps the people and channels the bridge already served: each one's first message through the gate links the user the bridge made (`discord-<user id>`, a channel's `discord-conv-<hash>`), so their history, memory and documents stay theirs. Without it they would start again as new users `discord:<user id>`. New people are `discord:<user id>`, new channels `discord:conv:<hash>`, either way.
134
+
135
+ 3. Reconfigure the bridge: set `BRIDGE_GATE=discord`, `BRIDGE_GATE_AUDIENCE` (loomcycle's `LOOMCYCLE_PUBLIC_URL`) and `BRIDGE_GATE_KEY`, and remove `LOOMCYCLE_TOKEN` and `BRIDGE_USER_ACCESS_MODE`, which a gate bridge refuses. Then retire the old operator token. The other `BRIDGE_GATE_*` settings are in `@loomcycle/bridge-core`'s README.
136
+
137
+ Do it in this order: the gate, with its prefix, must be in loomcycle's config before the bridge switches, or the first messages make new, empty users.
138
+
139
+ As a gate the bridge also tells loomcycle about groups and leavers.
140
+
141
+ - **Groups.** A channel is in the loomcycle group `discord:dc-guild-<server id>` of its server. A person (a DM) is in `discord:dc-guild-<server id>` for each server in `DISCORD_BRIDGE_TRUSTED_GUILDS` they are a member of, and `discord:dc-role-<role id>` for each role they hold there, which an operator can bind permissions to; it takes `trusted: true` on the gate and an `identity_map.groups` block. The bridge reads a person's membership with the bot token (Get Guild Member, which needs no privileged intent; the bot must be in the server), once per person per 10 minutes. When a lookup fails, or with more than 20 trusted servers, it asserts no groups for that person, so loomcycle never drops a group on a guess.
142
+ - **Leavers.** A person who removes the app is revoked in loomcycle: disabled at once, then removed by the gate's removal policy. Discord tells the bridge through Webhook Events: in the Developer Portal, on the **Webhooks** page, set the **Endpoint** to `https://<bridge-host>/discord/events`, turn **Events** on and select `APPLICATION_DEAUTHORIZED`. Discord checks the URL with a PING when you save it, so the bridge must already run as a gate. These events are signed like interactions and always verified. The bridge answers an event only once loomcycle has taken the revoke; if it has not, Discord gets a `503` and delivers the event again.
143
+
144
+ ## Voice messages
145
+
146
+ With `BRIDGE_VOICE=1`, a person can put a question to the agent as a voice message and hear the answer as one (RFC GA). Off by default.
147
+
148
+ **Asking.** Record a voice message in the channel or DM as usual, then right-click it (long-press on mobile) and pick **Apps → Ask by voice**. The bridge answers with "thinking…", has loomcycle transcribe the recording with the conversation's own token, and hands the agent the words, marked `(voice message)`, exactly as typed text goes: a trusted speaker's into the prompt, an untrusted speaker's as an untrusted block. A message with exactly one audio file works the same way.
149
+
150
+ - **Why a command, not just a voice message.** The bridge has no Gateway connection, so it never sees messages people post; a message command arrives as an interaction, through the endpoint the bridge already serves, with the message and its attachment in it. In a server, Discord would also withhold a message's attachments from a Gateway bot without the privileged Message Content intent. The command needs neither.
151
+ - **Only your own voice message.** Ask by voice on someone else's message is refused. Their words would reach the agent as your request, with your trust, and their recording would go to transcription without their asking. It also keeps the bridge from transcribing its own voice replies.
152
+ - **The recording** is fetched from Discord's CDN (`cdn.discordapp.com`, `media.discordapp.net`, nowhere else, redirects included) at its signed URL, with no token, and held in memory for that one request. A recording longer than `BRIDGE_VOICE_MAX_SECONDS` is refused from its metadata, before anything is downloaded.
153
+ - **Settings.** `/voice on`, `/voice off` and `/voice auto` are typed through `/ask`, as in `/ask /voice on`. `/help` lists them with voice on.
154
+
155
+ **The spoken reply.** The text reply is always posted. When the reply should also be spoken (by default, when the question was a voice message), the bridge has loomcycle voice it and posts it as a Discord voice message with the bot token, as a reply to the text. A voice message can carry no text, so the text reply gets one more line, `-# AI voice` (`BRIDGE_VOICE_LABEL`), saying the voice is generated. If the text cannot be edited (its interaction is too old, or the reply already fills its 2,000 characters), the label is posted as a short message of its own and the voice replies to that. A voice message that cannot be labelled is not sent.
156
+
157
+ **What it needs.**
158
+
159
+ - **Discord:** the bot permissions *Send Voice Messages* and *Attach Files* in the channel, and the bot in the channel, since the voice message goes by the bot token. If Discord refuses it (a missing permission, a DM the person closed), spoken replies stop in that conversation until someone sends `/ask /voice on`; the text replies carry on.
160
+ - **loomcycle:** the `speech:` and `transcription.stt:` engines, and for spoken replies `speech.encode` (an ffmpeg Opus command) with `speech.format: ogg`, because Discord shows only OGG/Opus as a voice message. Without it, voice messages are still understood and the replies stay text.
161
+ - **The user tokens' surfaces.** With voice on, the bridge asks for each user token to reach loomcycle's speech routes (`surfaces: [speech, transcription]`, loomcycle 1.141 or later). As a gate, add `surfaces: [speech, transcription]` to the gate's `issued_token`; without it loomcycle grants none. A voice message then gets "I can only read text for now.", in a DM and in a server channel alike: the "Ask by voice" reply is always answered.
162
+ - **Rate.** A voice turn takes three starts from the user's rate (transcribe, run, speak). Raise `BRIDGE_RATE_PER_MINUTE` to at least `12` with voice on (and the gate's `issued_token.rate`, for a gate).
163
+
164
+ The other `BRIDGE_VOICE_*` settings (length and size caps, the reply mode, the echo, untrusted speakers) are in `@loomcycle/bridge-core`'s README.
165
+
166
+ **Not yet checked against Discord** (RFC GA P0):
167
+
168
+ - **The waveform.** The bridge sends an empty one, as community clients do, and Discord draws a flat bar. If Discord starts refusing that, loomcycle will send a real one.
169
+ - **Bot-token posting.** A voice message is posted with the bot token, not through the interaction, because whether an interaction follow-up may carry the voice flag is unconfirmed. So a spoken reply in a DM, or in a channel the bot is not in, may be refused where the text reply was not.
170
+ - **The upload limit** is the `attachment_size_limit` of the channel's last interaction, else 10 MB. A spoken reply is far smaller than either.
101
171
 
102
172
  ## Servers, channels and trust
103
173
 
@@ -125,7 +195,8 @@ Discord's:
125
195
 
126
196
  - **Inbound:** every request's `X-Signature-Ed25519` is checked over the timestamp and the raw body with the application's public key, before the body is parsed. A timestamp more than five minutes off, and an interaction id already seen, are refused. A failed check is a bare `401`.
127
197
  - **Bots are dropped,** the bot itself included, before anything reaches loomcycle.
128
- - **Least privilege:** the bridge's own token never runs agents. A user's token is limited to one agent, every per-run override except `interactive` is locked, it is rate-limited, and it expires.
198
+ - **Least privilege:** the bridge's own token never runs agents; as a loomcycle gate (above) it holds no loomcycle token at all. A user's token is limited to one agent, every per-run override except `interactive` is locked, it is rate-limited, and it expires.
199
+ - **Voice:** a recording is downloaded from Discord's CDN hosts only, with no credential, read up to the size cap and never kept; the bot token never goes to the CDN. Logs carry sizes and outcomes, never a transcript.
129
200
  - **Credentials stay in memory.** Interaction tokens are never written to the conversation store, so `BRIDGE_STORE=file:` holds no credential; after a restart, replies use the bot token until the next command. The bot token is sent only to Discord's REST API, never to an interaction webhook. Logs carry ids, never a token, message text or agent output.
130
201
  - **Output:** agent text can be steered by whoever writes in a channel, so nothing the bridge posts can ping anyone (`allowed_mentions` parses nothing, so `@everyone` and role mentions are inert), shows a link preview (`SUPPRESS_EMBEDS` on every message: Discord would fetch the link to build one, with no click, and hand its query string to whoever runs that server), or hides a link's target (a masked link `[label](url)` is rewritten as `label (<url>)`). Tool inputs and outputs are never sent.
131
202
 
package/dist/api.d.ts CHANGED
@@ -17,12 +17,22 @@ export declare class DiscordApiError extends Error {
17
17
  /** Discord's JSON error code (10062 "Unknown interaction", say), when sent. */
18
18
  code?: number | undefined);
19
19
  }
20
+ /** The parts of a guild member object the bridge reads; shape-checked at use. */
21
+ export interface GuildMember {
22
+ roles?: unknown;
23
+ user?: {
24
+ id?: string;
25
+ bot?: boolean;
26
+ };
27
+ }
20
28
  /** Discord asks bot clients to identify themselves this way. */
21
29
  export declare const USER_AGENT = "DiscordBot (https://github.com/denn-gubsky/loomcycle, 0.1.0)";
22
30
  export interface CommandSpec {
23
31
  name: string;
32
+ /** Empty for a message command: Discord refuses one with a description. */
24
33
  description: string;
25
- type: 1;
34
+ /** 1: a slash command; 3: a message command (a message's Apps menu). */
35
+ type: 1 | 3;
26
36
  contexts: number[];
27
37
  integration_types: number[];
28
38
  options?: Array<{
@@ -33,6 +43,21 @@ export interface CommandSpec {
33
43
  max_length?: number;
34
44
  }>;
35
45
  }
46
+ /** A voice message (flag IS_VOICE_MESSAGE): Discord allows no content,
47
+ * embeds or components beside its one attachment. */
48
+ export interface VoiceMessagePayload {
49
+ flags: number;
50
+ attachments: Array<{
51
+ id: number;
52
+ filename: string;
53
+ duration_secs: number;
54
+ waveform: string;
55
+ }>;
56
+ message_reference?: {
57
+ message_id: string;
58
+ fail_if_not_exists: boolean;
59
+ };
60
+ }
36
61
  export declare class DiscordApi {
37
62
  private readonly opts;
38
63
  private readonly fetch;
@@ -40,8 +65,20 @@ export declare class DiscordApi {
40
65
  constructor(opts: DiscordApiOptions);
41
66
  editOriginal(interactionToken: string, body: MessageBody): Promise<unknown>;
42
67
  followUp(interactionToken: string, body: MessageBody): Promise<unknown>;
68
+ editFollowUp(interactionToken: string, messageId: string, body: MessageBody): Promise<unknown>;
43
69
  createMessage(channelId: string, body: MessageBody): Promise<unknown>;
70
+ editMessage(channelId: string, messageId: string, body: MessageBody): Promise<unknown>;
71
+ /** createVoiceMessage posts audio as a voice message, with the bot token:
72
+ * multipart, payload_json beside the file as files[0]. */
73
+ createVoiceMessage(channelId: string, payload: VoiceMessagePayload, file: {
74
+ bytes: Uint8Array;
75
+ filename: string;
76
+ contentType: string;
77
+ }): Promise<unknown>;
44
78
  typing(channelId: string): Promise<unknown>;
79
+ /** guildMember reads one member of a server (their roles), with the bot
80
+ * token. A person not in the server is a 404. */
81
+ guildMember(guildId: string, userId: string): Promise<GuildMember | undefined>;
45
82
  /** putCommands replaces the application's commands: globally, or in one server. */
46
83
  putCommands(commands: CommandSpec[], guildId?: string): Promise<unknown>;
47
84
  private call;
package/dist/api.js CHANGED
@@ -38,12 +38,31 @@ export class DiscordApi {
38
38
  followUp(interactionToken, body) {
39
39
  return this.call("POST", `/webhooks/${this.opts.applicationId}/${interactionToken}`, "POST webhooks/followup", body, false);
40
40
  }
41
+ editFollowUp(interactionToken, messageId, body) {
42
+ return this.call("PATCH", `/webhooks/${this.opts.applicationId}/${interactionToken}/messages/${messageId}`, "PATCH webhooks/messages", body, false);
43
+ }
41
44
  createMessage(channelId, body) {
42
45
  return this.call("POST", `/channels/${channelId}/messages`, "POST channels/messages", body, true);
43
46
  }
47
+ editMessage(channelId, messageId, body) {
48
+ return this.call("PATCH", `/channels/${channelId}/messages/${messageId}`, "PATCH channels/messages", body, true);
49
+ }
50
+ /** createVoiceMessage posts audio as a voice message, with the bot token:
51
+ * multipart, payload_json beside the file as files[0]. */
52
+ createVoiceMessage(channelId, payload, file) {
53
+ const form = new FormData();
54
+ form.set("payload_json", JSON.stringify(payload));
55
+ form.set("files[0]", new Blob([new Uint8Array(file.bytes)], { type: file.contentType }), file.filename);
56
+ return this.call("POST", `/channels/${channelId}/messages`, "POST channels/messages (voice)", form, true);
57
+ }
44
58
  typing(channelId) {
45
59
  return this.call("POST", `/channels/${channelId}/typing`, "POST channels/typing", undefined, true);
46
60
  }
61
+ /** guildMember reads one member of a server (their roles), with the bot
62
+ * token. A person not in the server is a 404. */
63
+ guildMember(guildId, userId) {
64
+ return this.call("GET", `/guilds/${guildId}/members/${userId}`, "GET guilds/members", undefined, true);
65
+ }
47
66
  /** putCommands replaces the application's commands: globally, or in one server. */
48
67
  putCommands(commands, guildId) {
49
68
  const path = guildId
@@ -53,7 +72,9 @@ export class DiscordApi {
53
72
  }
54
73
  async call(method, path, route, body, bot) {
55
74
  const headers = { "user-agent": USER_AGENT };
56
- if (body !== undefined)
75
+ // A FormData body sets its own multipart content type, boundary included.
76
+ const form = body instanceof FormData;
77
+ if (body !== undefined && !form)
57
78
  headers["content-type"] = "application/json";
58
79
  if (bot)
59
80
  headers.authorization = `Bot ${this.opts.botToken}`;
@@ -61,7 +82,7 @@ export class DiscordApi {
61
82
  const res = await this.fetch(`${this.opts.baseUrl}${path}`, {
62
83
  method,
63
84
  headers,
64
- body: body === undefined ? undefined : JSON.stringify(body),
85
+ body: body === undefined ? undefined : form ? body : JSON.stringify(body),
65
86
  });
66
87
  const text = await res.text();
67
88
  const json = parseJson(text);
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/bridge.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { DiscordConfig } from "./config.js";
3
3
  import { type DiscordRef } from "./port.js";
4
4
  export declare const MAX_MESSAGE_CHARS = 2000;
5
5
  export interface DiscordBridgeOptions extends BridgeOptions<DiscordRef> {
6
- /** Override how Discord's API is called (tests, a proxy). */
6
+ /** Override how Discord's API and its attachment CDN are reached (tests, a proxy). */
7
7
  fetch?: typeof fetch;
8
8
  sleep?: (ms: number) => Promise<void>;
9
9
  /** How long a button press may take before it is acked and finished later. */
package/dist/bridge.js CHANGED
@@ -1,10 +1,13 @@
1
1
  // The Discord adapter on the shared core: interactions over HTTP, no Gateway.
2
2
  import { consoleLogger, createBridge } from "@loomcycle/bridge-core";
3
3
  import { DiscordApi } from "./api.js";
4
- import { HELP_TEXT, registerCommands } from "./commands.js";
4
+ import { helpText, registerCommands } from "./commands.js";
5
+ import { EVENTS_PATH, eventsRoute } from "./events.js";
6
+ import { GroupLookup } from "./groups.js";
5
7
  import { interactionRoute } from "./interactions.js";
6
8
  import { DiscordPort } from "./port.js";
7
9
  import { publicKeyFromHex } from "./verify.js";
10
+ import { DiscordVoice } from "./voice.js";
8
11
  export const MAX_MESSAGE_CHARS = 2000;
9
12
  export function createDiscordBridge(cfg, opts = {}) {
10
13
  const log = opts.logger ?? consoleLogger;
@@ -19,17 +22,45 @@ export function createDiscordBridge(cfg, opts = {}) {
19
22
  });
20
23
  const port = new DiscordPort(api, log, { now });
21
24
  const key = publicKeyFromHex(cfg.publicKey);
25
+ const fences = {
26
+ applicationId: cfg.applicationId,
27
+ allowedGuilds: cfg.allowedGuilds,
28
+ allowDms: cfg.allowDms,
29
+ trustedGuilds: cfg.trustedGuilds,
30
+ dev: cfg.dev,
31
+ ...(cfg.gate ? { gate: cfg.gate.name } : {}),
32
+ };
33
+ // Voice messages (RFC GA §5.3), with BRIDGE_VOICE=1 only.
34
+ const voice = cfg.voice?.enabled ? new DiscordVoice(api, port, { fetch: opts.fetch }) : undefined;
35
+ const groups = cfg.gate ? new GroupLookup({ api, gate: cfg.gate.name, trustedGuilds: cfg.trustedGuilds, now }) : undefined;
22
36
  const adapter = {
23
37
  // `/ask prompt:/help` reaches the core as "/help": answer with the same
24
38
  // list as the /help command, not the core's typed-command one.
25
- platform: { name: "discord", label: "Discord", helpText: HELP_TEXT },
39
+ // An "Ask by voice" is a deferred interaction: it must be answered even
40
+ // in a server channel, where the core is otherwise quiet about a voice
41
+ // message it does not take.
42
+ platform: { name: "discord", label: "Discord", helpText: helpText(voice !== undefined), answerEveryVoice: true },
26
43
  // Without the Gateway there is no message to edit as it grows, and each
27
44
  // turn is one message per 2,000 characters.
28
45
  capabilities: { streaming: "never", maxMessageChars: MAX_MESSAGE_CHARS },
29
46
  port,
30
47
  async start(ctx) {
48
+ const gate = ctx.gate;
31
49
  await ctx.serve({
32
- "POST /discord/interactions": interactionRoute({ cfg, key, port, api, router: ctx.router, log: ctx.log, now, inlineCardMs: opts.inlineCardMs }),
50
+ "POST /discord/interactions": interactionRoute({
51
+ cfg: fences,
52
+ key,
53
+ port,
54
+ api,
55
+ router: ctx.router,
56
+ log: ctx.log,
57
+ now,
58
+ inlineCardMs: opts.inlineCardMs,
59
+ ...(voice ? { voice: { port: voice, untrusted: cfg.voice?.untrusted ?? true } } : {}),
60
+ }),
61
+ // Webhook Events only for a gate, which is what tells loomcycle of a
62
+ // person who removed the app.
63
+ ...(gate ? { [`POST ${EVENTS_PATH}`]: eventsRoute({ key, gate, forget: (s) => groups?.forget(s), log: ctx.log, now }) } : {}),
33
64
  });
34
65
  if (cfg.registerCommands)
35
66
  await registerCommands(api, cfg, ctx.log);
@@ -37,6 +68,8 @@ export function createDiscordBridge(cfg, opts = {}) {
37
68
  async stop() {
38
69
  port.stop();
39
70
  },
71
+ ...(groups ? { groupsFor: (subject, ref) => groups.groupsFor(subject, ref) } : {}),
72
+ ...(voice ? { voice } : {}),
40
73
  };
41
74
  return createBridge(cfg, adapter, { ...opts, logger: log });
42
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,YAAY,EAAuD,MAAM,wBAAwB,CAAC;AAE1H,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAmB,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAUtC,MAAM,UAAU,mBAAmB,CAAC,GAAkB,EAAE,IAAI,GAAyB,EAAE;IACrF,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC;QACzB,OAAO,EAAE,GAAG,CAAC,UAAU;QACvB,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,GAAG;QACH,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAE5C,MAAM,OAAO,GAA8B;QACzC,wEAAwE;QACxE,+DAA+D;QAC/D,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE;QACpE,wEAAwE;QACxE,4CAA4C;QAC5C,YAAY,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE;QACxE,IAAI;QACJ,KAAK,CAAC,KAAK,CAAC,GAAG;YACb,MAAM,GAAG,CAAC,KAAK,CAAC;gBACd,4BAA4B,EAAE,gBAAgB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;aAChJ,CAAC,CAAC;YACH,IAAI,GAAG,CAAC,gBAAgB;gBAAE,MAAM,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACtE,CAAC;QACD,KAAK,CAAC,IAAI;YACR,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;KACF,CAAC;IACF,OAAO,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,28 @@
1
- import type { Logger } from "@loomcycle/bridge-core";
1
+ import { type Logger } from "@loomcycle/bridge-core";
2
2
  import type { CommandSpec, DiscordApi } from "./api.js";
3
3
  import type { DiscordConfig } from "./config.js";
4
4
  export declare const MAX_PROMPT_CHARS = 4000;
5
+ /** The message command that takes a voice message (RFC GA): right-click it,
6
+ * Apps, Ask by voice. */
7
+ export declare const ASK_BY_VOICE = "Ask by voice";
5
8
  export declare const HELP_TEXT: string;
6
- export declare function commandSpecs(contexts: number[]): CommandSpec[];
9
+ /** DISCORD_VOICE_HELP is added to the list with voice on. The core's /voice
10
+ * settings are typed messages, which on Discord go through /ask. */
11
+ export declare const DISCORD_VOICE_HELP: string;
12
+ /** helpText is the platform's /help list: the core adds its voice settings
13
+ * after it when voice is on. */
14
+ export declare function helpText(voice: boolean): string;
15
+ /** slashHelpText answers the /help command, which the bridge answers itself. */
16
+ export declare function slashHelpText(voice: boolean): string;
17
+ export declare function commandSpecs(contexts: number[], opts?: {
18
+ voice?: boolean;
19
+ }): CommandSpec[];
7
20
  /** registerCommands replaces the application's commands in every allowed
8
21
  * server, and globally (DMs only) when DMs are on. The bridge owns the
9
- * application's commands: anything else registered there is replaced. */
10
- export declare function registerCommands(api: DiscordApi, cfg: Pick<DiscordConfig, "allowedGuilds" | "allowDms">, log: Logger): Promise<void>;
22
+ * application's commands: anything else registered there is replaced. Ask
23
+ * by voice is registered only with voice on, so no menu offers it in vain. */
24
+ export declare function registerCommands(api: DiscordApi, cfg: Pick<DiscordConfig, "allowedGuilds" | "allowDms"> & {
25
+ voice?: {
26
+ enabled: boolean;
27
+ };
28
+ }, log: Logger): Promise<void>;
package/dist/commands.js CHANGED
@@ -4,9 +4,13 @@
4
4
  // take effect at once and show only where the bot answers. Commands in DMs
5
5
  // can only be global, so with DMs on they are also registered globally,
6
6
  // limited to the bot's DMs, so a server never lists them twice.
7
- import { Context } from "./interaction.js";
7
+ import { VOICE_HELP_TEXT } from "@loomcycle/bridge-core";
8
+ import { CommandType, Context } from "./interaction.js";
8
9
  const GUILD_INSTALL = 0;
9
10
  export const MAX_PROMPT_CHARS = 4000;
11
+ /** The message command that takes a voice message (RFC GA): right-click it,
12
+ * Apps, Ask by voice. */
13
+ export const ASK_BY_VOICE = "Ask by voice";
10
14
  export const HELP_TEXT = [
11
15
  "Commands:",
12
16
  "- **/ask** `prompt`: talk to the agent. When it asks you a question without options, answer with /ask too.",
@@ -16,8 +20,24 @@ export const HELP_TEXT = [
16
20
  "",
17
21
  "In a server, everyone in a channel shares one conversation with the agent. The person who started it answers the agent's questions and can /new or /stop it.",
18
22
  ].join("\n");
19
- export function commandSpecs(contexts) {
20
- const base = { type: 1, contexts, integration_types: [GUILD_INSTALL] };
23
+ /** DISCORD_VOICE_HELP is added to the list with voice on. The core's /voice
24
+ * settings are typed messages, which on Discord go through /ask. */
25
+ export const DISCORD_VOICE_HELP = [
26
+ "",
27
+ "Voice: right-click your own voice message, then **Apps** → **Ask by voice**. The voice settings go through /ask, as in `/ask /voice on`:",
28
+ ].join("\n");
29
+ /** helpText is the platform's /help list: the core adds its voice settings
30
+ * after it when voice is on. */
31
+ export function helpText(voice) {
32
+ return voice ? `${HELP_TEXT}\n${DISCORD_VOICE_HELP}` : HELP_TEXT;
33
+ }
34
+ /** slashHelpText answers the /help command, which the bridge answers itself. */
35
+ export function slashHelpText(voice) {
36
+ return voice ? `${helpText(true)}\n${VOICE_HELP_TEXT}` : HELP_TEXT;
37
+ }
38
+ export function commandSpecs(contexts, opts = {}) {
39
+ const base = { type: CommandType.CHAT_INPUT, contexts, integration_types: [GUILD_INSTALL] };
40
+ const voice = opts.voice ? [{ ...base, type: CommandType.MESSAGE, name: ASK_BY_VOICE, description: "" }] : [];
21
41
  return [
22
42
  {
23
43
  ...base,
@@ -28,18 +48,21 @@ export function commandSpecs(contexts) {
28
48
  { ...base, name: "new", description: "Start a new conversation with the agent" },
29
49
  { ...base, name: "stop", description: "Stop the agent's current answer" },
30
50
  { ...base, name: "help", description: "Show what the bot can do" },
51
+ ...voice,
31
52
  ];
32
53
  }
33
54
  /** registerCommands replaces the application's commands in every allowed
34
55
  * server, and globally (DMs only) when DMs are on. The bridge owns the
35
- * application's commands: anything else registered there is replaced. */
56
+ * application's commands: anything else registered there is replaced. Ask
57
+ * by voice is registered only with voice on, so no menu offers it in vain. */
36
58
  export async function registerCommands(api, cfg, log) {
59
+ const opts = { voice: cfg.voice?.enabled === true };
37
60
  for (const guild of cfg.allowedGuilds) {
38
- await api.putCommands(commandSpecs([Context.GUILD]), guild);
61
+ await api.putCommands(commandSpecs([Context.GUILD], opts), guild);
39
62
  log.info("registered commands", { guild });
40
63
  }
41
64
  if (cfg.allowDms) {
42
- await api.putCommands(commandSpecs([Context.BOT_DM]));
65
+ await api.putCommands(commandSpecs([Context.BOT_DM], opts));
43
66
  log.info("registered commands", { scope: "dm" });
44
67
  }
45
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,wEAAwE;AACxE,gEAAgE;AAMhE,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE3C,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,WAAW;IACX,4GAA4G;IAC5G,mEAAmE;IACnE,sCAAsC;IACtC,6BAA6B;IAC7B,EAAE;IACF,8JAA8J;CAC/J,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,UAAU,YAAY,CAAC,QAAkB;IAC7C,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,CAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC;IAChF,OAAO;QACL;YACE,GAAG,IAAI;YACP,IAAI,EAAE,KAAK;YACX,WAAW,EAAE,mBAAmB;YAChC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0BAA0B,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC;SAC9H;QACD,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,yCAAyC,EAAE;QAChF,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,iCAAiC,EAAE;QACzE,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;KACnE,CAAC;AACJ,CAAC;AAED;;0EAE0E;AAC1E,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAe,EAAE,GAAsD,EAAE,GAAW;IACzH,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;QACtC,MAAM,GAAG,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC5D,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjB,MAAM,GAAG,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACtD,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -24,7 +24,7 @@ function apiBaseUrl(reader) {
24
24
  if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
25
25
  reader.problems.push("DISCORD_BRIDGE_API_BASE_URL must be https (http only to localhost)");
26
26
  }
27
- return raw.replace(/\/+$/, "");
27
+ return withoutTrailingSlashes(raw);
28
28
  }
29
29
  function guilds(reader, name) {
30
30
  const ids = reader.list(name);
@@ -38,7 +38,9 @@ function guilds(reader, name) {
38
38
  }
39
39
  export function loadConfig(env = process.env) {
40
40
  const reader = new EnvReader(env);
41
- const shared = loadSharedConfig(reader, { port: DEFAULT_PORT });
41
+ // A gate only when BRIDGE_GATE names one: a bridge deployed with a
42
+ // loomcycle token keeps running as it was until it is reconfigured.
43
+ const shared = loadSharedConfig(reader, { port: DEFAULT_PORT, gate: reader.get("BRIDGE_GATE") !== undefined });
42
44
  const applicationId = reader.required("DISCORD_APPLICATION_ID");
43
45
  if (applicationId && !isSnowflake(applicationId))
44
46
  reader.problems.push("DISCORD_APPLICATION_ID must be the application's numeric id");
@@ -74,4 +76,13 @@ export function loadConfig(env = process.env) {
74
76
  reader.check("discord-bridge");
75
77
  return cfg;
76
78
  }
79
+ /** withoutTrailingSlashes drops a URL's trailing slashes. A loop, not
80
+ * /\/+$/, which backtracks quadratically on a long run of slashes that is
81
+ * not at the end. */
82
+ function withoutTrailingSlashes(s) {
83
+ let end = s.length;
84
+ while (end > 0 && s[end - 1] === "/")
85
+ end--;
86
+ return s.slice(0, end);
87
+ }
77
88
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { type Logger, type Route } from "@loomcycle/bridge-core";
2
+ import type { KeyObject } from "node:crypto";
3
+ export declare const EVENTS_PATH = "/discord/events";
4
+ /** The outer object's type: Discord's probe when the URL is saved, or an event. */
5
+ export declare const WebhookType: {
6
+ readonly PING: 0;
7
+ readonly EVENT: 1;
8
+ };
9
+ export declare const APPLICATION_DEAUTHORIZED = "APPLICATION_DEAUTHORIZED";
10
+ /** The parts of a webhook event the bridge reads; the body is signed by
11
+ * Discord, but its shape is still checked before use. */
12
+ export interface WebhookEvent {
13
+ version?: number;
14
+ application_id?: string;
15
+ type?: number;
16
+ event?: {
17
+ type?: string;
18
+ timestamp?: string;
19
+ data?: {
20
+ user?: {
21
+ id?: string;
22
+ };
23
+ };
24
+ };
25
+ }
26
+ export interface EventDeps {
27
+ key: KeyObject;
28
+ /** The core's gate: names a platform id's subject, and revokes it. */
29
+ gate: {
30
+ subjectOf(sub: string): string;
31
+ revoke(subject: string): Promise<void>;
32
+ };
33
+ /** Called with the revoked subject, so cached groups go with it. */
34
+ forget(subject: string): void;
35
+ log: Logger;
36
+ now: () => number;
37
+ /** How long the answer waits for loomcycle's revoke (tests). */
38
+ revokeTimeoutMs?: number;
39
+ }
40
+ export declare function eventsRoute(deps: EventDeps): Route;