@openvertice/bot-sdk 0.0.0-stage → 0.1.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.
Files changed (48) hide show
  1. package/README.md +189 -2
  2. package/dist/src/audio.d.ts +32 -0
  3. package/dist/src/audio.d.ts.map +1 -0
  4. package/dist/src/audio.js +47 -0
  5. package/dist/src/audio.js.map +1 -0
  6. package/dist/src/cliente.d.ts +213 -0
  7. package/dist/src/cliente.d.ts.map +1 -0
  8. package/dist/src/cliente.js +381 -0
  9. package/dist/src/cliente.js.map +1 -0
  10. package/dist/src/gateway.d.ts +76 -0
  11. package/dist/src/gateway.d.ts.map +1 -0
  12. package/dist/src/gateway.js +299 -0
  13. package/dist/src/gateway.js.map +1 -0
  14. package/dist/src/http.d.ts +119 -0
  15. package/dist/src/http.d.ts.map +1 -0
  16. package/dist/src/http.js +189 -0
  17. package/dist/src/http.js.map +1 -0
  18. package/dist/src/index.d.ts +19 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +16 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/tipos.d.ts +243 -0
  23. package/dist/src/tipos.d.ts.map +1 -0
  24. package/dist/src/tipos.js +5 -0
  25. package/dist/src/tipos.js.map +1 -0
  26. package/dist/src/voz-wrtc.d.ts +20 -0
  27. package/dist/src/voz-wrtc.d.ts.map +1 -0
  28. package/dist/src/voz-wrtc.js +68 -0
  29. package/dist/src/voz-wrtc.js.map +1 -0
  30. package/dist/src/voz.d.ts +150 -0
  31. package/dist/src/voz.d.ts.map +1 -0
  32. package/dist/src/voz.js +311 -0
  33. package/dist/src/voz.js.map +1 -0
  34. package/dist/src/wav.d.ts +29 -0
  35. package/dist/src/wav.d.ts.map +1 -0
  36. package/dist/src/wav.js +54 -0
  37. package/dist/src/wav.js.map +1 -0
  38. package/package.json +58 -3
  39. package/src/audio.ts +88 -0
  40. package/src/cliente.ts +472 -0
  41. package/src/gateway.ts +354 -0
  42. package/src/http.ts +249 -0
  43. package/src/index.ts +41 -0
  44. package/src/tipos.ts +220 -0
  45. package/src/voz-wrtc.ts +100 -0
  46. package/src/voz.ts +407 -0
  47. package/src/wav.ts +71 -0
  48. package/src/wrtc.d.ts +4 -0
package/src/gateway.ts ADDED
@@ -0,0 +1,354 @@
1
+ // The gateway: one WebSocket, the `{op, t, d, s}` envelope and nothing else. It
2
+ // owns the heartbeat, the resume and the reconnection with growing backoff.
3
+ //
4
+ // It uses Node's global WebSocket (22+), so the SDK has no runtime dependency.
5
+ // The interface is deliberately the browser's.
6
+ import type { ComandoAnunciado, Eventos, Sessao } from "./tipos.js";
7
+
8
+ /** `WebSocket.readyState` for an open socket. */
9
+ const ABERTO = 1;
10
+ /** How long without any server frame before the connection is assumed dead. */
11
+ const SEM_SINAL_MS = 90_000;
12
+
13
+ /** Codes where retrying is pointless: the server refused the bot. */
14
+ const DEFINITIVOS = new Set([4001, 4003, 4005]);
15
+ /** Another connection took over this bot: reconnecting would fight over it. */
16
+ const OUTRA_CONEXAO = 4000;
17
+ /** Session closed (rotated token, disabled bot): it does not come back on its own. */
18
+ const SESSAO_ENCERRADA = 4009;
19
+ /** Expired session: renew the token and come back. */
20
+ const SESSAO_EXPIRADA = 4002;
21
+
22
+ /**
23
+ * Connection states the bot can observe.
24
+ *
25
+ * - `conectando` — socket being opened and the bot identified.
26
+ * - `pronto` — `ready` received; events are flowing.
27
+ * - `reconectando` — waiting out the backoff before trying again.
28
+ * - `encerrado` — closed on purpose (`stop()`); no reconnect.
29
+ * - `recusado` — the server refused the bot; no reconnect.
30
+ */
31
+ export type EstadoDoGateway =
32
+ | "conectando"
33
+ | "pronto"
34
+ | "reconectando"
35
+ | "encerrado"
36
+ | "recusado";
37
+
38
+ /** Everything the gateway needs from the bot, plus its hooks. */
39
+ export interface OpcoesGateway {
40
+ /** WebSocket URL from the discovery document. */
41
+ url: string;
42
+ /** Returns a valid session; called again when the current one expires. */
43
+ sessao: () => Promise<Sessao>;
44
+ /** Intents requested on `identify`. */
45
+ intents: string[];
46
+ /** Commands for the client's autocomplete; resent on every connection. */
47
+ comandos?: ComandoAnunciado[];
48
+ /** Heartbeat interval in milliseconds, as told by the server. */
49
+ heartbeatMs: number;
50
+ /** Called for every dispatched event (op 0). */
51
+ aoEvento: <K extends keyof Eventos>(t: K, d: Eventos[K]) => void;
52
+ /** Called on every state change, with an optional human-readable detail. */
53
+ aoEstado?: (estado: EstadoDoGateway, detalhe?: string) => void;
54
+ /** Diagnostic sink: one line per interesting step. */
55
+ log?: (mensagem: string) => void;
56
+ }
57
+
58
+ /** Gateway frame: `op` is mandatory, every other field depends on the op code. */
59
+ interface Envelope {
60
+ op: number;
61
+ t?: string;
62
+ d?: unknown;
63
+ s?: number;
64
+ }
65
+
66
+ /**
67
+ * WebSocket client for the Vértice gateway.
68
+ *
69
+ * It speaks the `{op, t, d, s}` envelope: op 10 `hello`, op 2 `identify` (or
70
+ * op 6 `resume`), op 1 heartbeat, op 11 heartbeat ack, op 0 dispatch and op 4
71
+ * action. Heartbeat, session resume and reconnection with exponential backoff
72
+ * are handled here, so bot authors never have to.
73
+ *
74
+ * One instance per bot: `conectar()` resolves only after `ready` arrives.
75
+ */
76
+ export class Gateway {
77
+ #ws: WebSocket | null = null;
78
+ #sessao: Sessao | null = null;
79
+ #seq = 0;
80
+ #batida: ReturnType<typeof setInterval> | null = null;
81
+ #vigia: ReturnType<typeof setTimeout> | null = null;
82
+ #recuo: ReturnType<typeof setTimeout> | null = null;
83
+ #tentativa = 0;
84
+ #encerrado = false;
85
+ #estado: EstadoDoGateway = "conectando";
86
+
87
+ /** @param opcoes Endpoints, credentials and hooks used by the connection. */
88
+ constructor(private readonly opcoes: OpcoesGateway) {}
89
+
90
+ /** Current connection state. */
91
+ get estado(): EstadoDoGateway {
92
+ return this.#estado;
93
+ }
94
+
95
+ /** Session in use, or `null` before the first connection attempt. */
96
+ get sessaoAtual(): Sessao | null {
97
+ return this.#sessao;
98
+ }
99
+
100
+ /**
101
+ * Opens the socket and resolves once the bot is identified (`ready`).
102
+ *
103
+ * @returns A promise that settles when the gateway is truly ready.
104
+ * @throws {Error} When the server refuses the bot or the socket cannot open.
105
+ */
106
+ conectar(): Promise<void> {
107
+ this.#encerrado = false;
108
+ return this.#abrir();
109
+ }
110
+
111
+ /**
112
+ * Opens one socket attempt.
113
+ *
114
+ * Resolves on the first dispatched event — in practice `ready` — and rejects
115
+ * if the socket fails before that.
116
+ */
117
+ async #abrir(): Promise<void> {
118
+ this.#mudar("conectando");
119
+ const sessao = await this.opcoes.sessao();
120
+ this.#sessao = sessao;
121
+ return new Promise<void>((resolver, rejeitar) => {
122
+ let concluido = false;
123
+ const falhar = (erro: Error) => {
124
+ if (concluido) return;
125
+ concluido = true;
126
+ rejeitar(erro);
127
+ };
128
+ let ws: WebSocket;
129
+ try {
130
+ ws = new WebSocket(this.opcoes.url);
131
+ } catch (erro) {
132
+ falhar(erro instanceof Error ? erro : new Error(String(erro)));
133
+ return;
134
+ }
135
+ this.#ws = ws;
136
+
137
+ ws.addEventListener("error", () =>
138
+ falhar(new Error("não consegui abrir o gateway")),
139
+ );
140
+ ws.addEventListener("message", (evento) => {
141
+ const envelope = this.#ler(evento);
142
+ if (!envelope) return;
143
+ this.#armarVigia();
144
+ // hello: now it is safe to identify (or to resume, if a seq exists).
145
+ if (envelope.op === 10) {
146
+ this.#identificar(ws, sessao);
147
+ return;
148
+ }
149
+ this.#receber(envelope, () => {
150
+ if (concluido) return;
151
+ concluido = true;
152
+ resolver();
153
+ });
154
+ });
155
+ ws.addEventListener("close", (evento) => {
156
+ this.#aoFechar(evento.code, evento.reason);
157
+ if (this.#encerrado) falhar(new Error("gateway encerrado"));
158
+ });
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Sends `identify` (op 2) on a fresh connection, or `resume` (op 6) when a
164
+ * sequence number from a previous connection is available.
165
+ *
166
+ * @param ws Socket already past `hello`.
167
+ * @param sessao Session to present to the server.
168
+ */
169
+ #identificar(ws: WebSocket, sessao: Sessao): void {
170
+ const comandos = this.opcoes.comandos ?? [];
171
+ const envelope =
172
+ this.#seq > 0
173
+ ? { op: 6, d: { sessao: sessao.sessao, seq: this.#seq, comandos } }
174
+ : {
175
+ op: 2,
176
+ d: { sessao: sessao.sessao, intents: this.opcoes.intents, comandos },
177
+ };
178
+ ws.send(JSON.stringify(envelope));
179
+ }
180
+
181
+ /**
182
+ * Parses one socket frame, discarding anything that is not a valid envelope.
183
+ *
184
+ * @param evento Raw `message` event from the socket.
185
+ * @returns The envelope, or `null` when the payload is not JSON or has no `op`.
186
+ */
187
+ #ler(evento: MessageEvent): Envelope | null {
188
+ try {
189
+ const bruto = typeof evento.data === "string" ? evento.data : String(evento.data);
190
+ const envelope = JSON.parse(bruto) as Envelope;
191
+ return typeof envelope?.op === "number" ? envelope : null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Handles one gateway frame.
199
+ *
200
+ * op 0 is a dispatch, op 11 is the heartbeat ack and everything else is
201
+ * ignored here.
202
+ *
203
+ * @param envelope Frame received from the socket.
204
+ * @param aoPronto Called once, as soon as `ready` marks the bot as connected.
205
+ */
206
+ #receber(envelope: Envelope, aoPronto: () => void): void {
207
+ if (envelope.op === 11) return;
208
+ if (envelope.op !== 0 || !envelope.t) return;
209
+ if (typeof envelope.s === "number") this.#seq = envelope.s;
210
+ if (envelope.t === "ready") {
211
+ this.#tentativa = 0;
212
+ this.#mudar("pronto");
213
+ this.#bater();
214
+ aoPronto();
215
+ }
216
+ this.opcoes.aoEvento(
217
+ envelope.t as keyof Eventos,
218
+ envelope.d as Eventos[keyof Eventos],
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Sends an action (op 4) to the gateway.
224
+ *
225
+ * Voice uses this for `voz.entrar`, `voz.sair` and `voice.signal`; the same
226
+ * route carries `comandos` updates.
227
+ *
228
+ * @param tipo Action name understood by the server.
229
+ * @param dados Action payload, merged into `d`.
230
+ * @throws {Error} When the gateway is not connected.
231
+ */
232
+ agir(tipo: string, dados: Record<string, unknown> = {}): void {
233
+ const ws = this.#ws;
234
+ if (!ws || ws.readyState !== ABERTO)
235
+ throw new Error("O gateway não está conectado.");
236
+ ws.send(JSON.stringify({ op: 4, d: { tipo, ...dados } }));
237
+ }
238
+
239
+ /**
240
+ * Closes the connection on purpose: it will not reconnect.
241
+ *
242
+ * @param codigo WebSocket close code sent to the server.
243
+ * @param motivo Human-readable reason sent to the server.
244
+ */
245
+ fechar(codigo = 1000, motivo = "pedido do bot"): void {
246
+ this.#encerrado = true;
247
+ this.#limpar();
248
+ try {
249
+ this.#ws?.close(codigo, motivo);
250
+ } catch {
251
+ /* it was already closing */
252
+ }
253
+ this.#ws = null;
254
+ this.#mudar("encerrado");
255
+ }
256
+
257
+ /**
258
+ * Stores the new state, notifies the bot and logs the detail, when there is one.
259
+ *
260
+ * @param estado New connection state.
261
+ * @param detalhe Optional human-readable explanation for logs and errors.
262
+ */
263
+ #mudar(estado: EstadoDoGateway, detalhe?: string): void {
264
+ this.#estado = estado;
265
+ this.opcoes.aoEstado?.(estado, detalhe);
266
+ if (detalhe) this.opcoes.log?.(`[${estado}] ${detalhe}`);
267
+ }
268
+
269
+ /**
270
+ * (Re)starts the heartbeat timer, sending op 1 with the last sequence number.
271
+ *
272
+ * The interval is left `unref`'d so it never keeps the process alive.
273
+ */
274
+ #bater(): void {
275
+ if (this.#batida) clearInterval(this.#batida);
276
+ this.#batida = setInterval(() => {
277
+ const ws = this.#ws;
278
+ if (!ws || ws.readyState !== ABERTO) return;
279
+ ws.send(JSON.stringify({ op: 1, d: this.#seq }));
280
+ }, this.opcoes.heartbeatMs);
281
+ this.#batida.unref?.();
282
+ }
283
+
284
+ /**
285
+ * With nothing from the server for more than 90 s, the connection died
286
+ * silently (proxy, NAT). Drops it so the reconnect logic kicks in: it is the
287
+ * same deadline the server uses.
288
+ */
289
+ #armarVigia(): void {
290
+ if (this.#vigia) clearTimeout(this.#vigia);
291
+ this.#vigia = setTimeout(() => {
292
+ if (this.#estado === "encerrado") return;
293
+ this.opcoes.log?.("sem sinal do servidor; reconectando");
294
+ try {
295
+ this.#ws?.close(4000, "sem sinal");
296
+ } catch {
297
+ /* it had already dropped */
298
+ }
299
+ }, SEM_SINAL_MS);
300
+ this.#vigia.unref?.();
301
+ }
302
+
303
+ /**
304
+ * Decides what to do when the socket closes: come back or stop for good.
305
+ *
306
+ * @param codigo WebSocket close code.
307
+ * @param motivo Close reason, used in the log and in the state detail.
308
+ */
309
+ #aoFechar(codigo: number, motivo: string): void {
310
+ this.#limpar();
311
+ this.#ws = null;
312
+ if (this.#encerrado) return;
313
+ if (DEFINITIVOS.has(codigo)) {
314
+ this.#mudar("recusado", `${codigo}: ${motivo}`);
315
+ this.#encerrado = true;
316
+ return;
317
+ }
318
+ if (codigo === OUTRA_CONEXAO) {
319
+ this.#mudar("recusado", "outra conexão assumiu este bot");
320
+ this.#encerrado = true;
321
+ return;
322
+ }
323
+ if (codigo === SESSAO_ENCERRADA) {
324
+ this.#mudar("recusado", "a sessão foi encerrada (token girado ou bot desativado)");
325
+ this.#encerrado = true;
326
+ return;
327
+ }
328
+ // 4002 (expired session) and network drops: renew the session and come back.
329
+ this.#tentativa += 1;
330
+ const espera = Math.min(30_000, 500 * 2 ** Math.min(this.#tentativa, 6));
331
+ this.#mudar(
332
+ "reconectando",
333
+ `${codigo || "sem código"}: volta em ${espera} ms (tentativa ${this.#tentativa})`,
334
+ );
335
+ this.#recuo = setTimeout(() => {
336
+ if (this.#encerrado) return;
337
+ // On an expired session, the client's `sessao()` already asks for another one.
338
+ this.#abrir().catch((erro: Error) =>
339
+ this.#mudar("reconectando", `falhou: ${erro.message}`),
340
+ );
341
+ }, espera);
342
+ this.#recuo.unref?.();
343
+ }
344
+
345
+ /** Clears every timer, so a closed gateway leaves no handles behind. */
346
+ #limpar(): void {
347
+ if (this.#batida) clearInterval(this.#batida);
348
+ if (this.#vigia) clearTimeout(this.#vigia);
349
+ if (this.#recuo) clearTimeout(this.#recuo);
350
+ this.#batida = null;
351
+ this.#vigia = null;
352
+ this.#recuo = null;
353
+ }
354
+ }
package/src/http.ts ADDED
@@ -0,0 +1,249 @@
1
+ // Bot HTTP layer: discovery, session and the message routes. Dependency-free:
2
+ // `fetch` is global on Node 22+.
3
+ import type { Descoberta, Intencao, Mensagem, Sessao } from "./tipos.js";
4
+
5
+ /**
6
+ * Error thrown for every non-2xx response from the bot API.
7
+ *
8
+ * It carries the HTTP status and, when the server sends one, the machine-readable
9
+ * `codigo` (e.g. `"canal"`, `"voz"`), so callers can react to the cause instead
10
+ * of parsing the message.
11
+ */
12
+ export class ErroDaApi extends Error {
13
+ /**
14
+ * @param mensagem Human-readable message taken from the server payload.
15
+ * @param status HTTP status code of the failed response.
16
+ * @param codigo Server error code, when the payload provided one.
17
+ */
18
+ constructor(
19
+ mensagem: string,
20
+ readonly status: number,
21
+ readonly codigo: string | null = null,
22
+ ) {
23
+ super(mensagem);
24
+ this.name = "ErroDaApi";
25
+ }
26
+ }
27
+
28
+ /** Credentials and transport used by every {@link Http} call. */
29
+ export interface OpcoesHttp {
30
+ /** Server base URL, e.g. `https://vertice.example` (default: `http://localhost:8080`). */
31
+ baseUrl?: string;
32
+ /** Bot token (`vtb_…`), copied from the admin panel. */
33
+ token: string;
34
+ /** Injectable for tests; defaults to the global `fetch`. */
35
+ fetch?: typeof fetch;
36
+ }
37
+
38
+ /** Trailing slashes, removed so every path can safely start with `/`. */
39
+ const SEM_BARRA = /\/+$/;
40
+
41
+ /**
42
+ * Minimal HTTP client for the `/api/bots/*` routes.
43
+ *
44
+ * Requests authenticate as `Bot <token>` until a session exists, then as
45
+ * `Bearer <sessao>` — the form the server expects on message routes. There is
46
+ * no runtime dependency: it uses the global `fetch` from Node 22+.
47
+ */
48
+ export class Http {
49
+ readonly #base: string;
50
+ readonly #token: string;
51
+ readonly #fetch: typeof fetch;
52
+
53
+ /**
54
+ * @param options Base URL, bot token and an optional `fetch` implementation.
55
+ * @throws {Error} When the token is missing or empty.
56
+ */
57
+ constructor({ baseUrl = "http://localhost:8080", token, fetch: buscar }: OpcoesHttp) {
58
+ if (!token) throw new Error("Falta o token do bot (o `vtb_…` do painel).");
59
+ this.#base = baseUrl.replace(SEM_BARRA, "");
60
+ this.#token = token;
61
+ this.#fetch = buscar ?? globalThis.fetch;
62
+ }
63
+
64
+ /**
65
+ * Performs a JSON request and unwraps the response.
66
+ *
67
+ * @typeParam T Expected shape of the response body.
68
+ * @param caminho Path relative to the base URL (e.g. `/api/bots/gateway`).
69
+ * @param options HTTP method, session token and JSON body.
70
+ * @returns The parsed body, or `null` when the server sent no content.
71
+ * @throws {ErroDaApi} When the response status is not 2xx.
72
+ */
73
+ async #pedir<T>(
74
+ caminho: string,
75
+ {
76
+ metodo = "GET",
77
+ sessao,
78
+ corpo,
79
+ }: { metodo?: string; sessao?: string; corpo?: unknown } = {},
80
+ ): Promise<T> {
81
+ const cabecalhos: Record<string, string> = {
82
+ Accept: "application/json",
83
+ // The bot token always works; the session only once one is in hand.
84
+ Authorization: sessao ? `Bearer ${sessao}` : `Bot ${this.#token}`,
85
+ };
86
+ let envio: string | undefined;
87
+ if (corpo !== undefined) {
88
+ cabecalhos["Content-Type"] = "application/json";
89
+ envio = JSON.stringify(corpo);
90
+ }
91
+ const resposta = await this.#fetch(this.#base + caminho, {
92
+ method: metodo,
93
+ headers: cabecalhos,
94
+ body: envio,
95
+ });
96
+ const texto = await resposta.text();
97
+ let dados: unknown = null;
98
+ try {
99
+ dados = texto ? JSON.parse(texto) : null;
100
+ } catch {
101
+ dados = null;
102
+ }
103
+ if (!resposta.ok) {
104
+ const erro = (dados ?? {}) as { erro?: string; codigo?: string };
105
+ throw new ErroDaApi(
106
+ erro.erro ?? `O servidor respondeu ${resposta.status}.`,
107
+ resposta.status,
108
+ erro.codigo ?? null,
109
+ );
110
+ }
111
+ return dados as T;
112
+ }
113
+
114
+ /**
115
+ * Fetches the discovery document: what this bot may do, as configured in the panel.
116
+ *
117
+ * @returns Channels, intents, ICE servers and rate limits granted to the bot.
118
+ * @throws {ErroDaApi} When the token is rejected.
119
+ */
120
+ descobrir(): Promise<Descoberta> {
121
+ return this.#pedir<Descoberta>("/api/bots/gateway");
122
+ }
123
+
124
+ /**
125
+ * Exchanges the bot token for a short-lived session (15 minutes).
126
+ *
127
+ * The SDK renews it on its own shortly before it expires.
128
+ *
129
+ * @returns The new session and its expiry.
130
+ * @throws {ErroDaApi} When the token is invalid or the bot is disabled.
131
+ */
132
+ abrirSessao(): Promise<Sessao> {
133
+ return this.#pedir<Sessao>("/api/bots/session", { metodo: "POST" });
134
+ }
135
+
136
+ /**
137
+ * Posts a text message over HTTP (the gateway can do the same over WebSocket).
138
+ *
139
+ * @param code Channel code.
140
+ * @param texto Message content.
141
+ * @param sessao Session token; when absent, the bot token is used.
142
+ * @returns The message the server created (id, timestamp, …).
143
+ * @throws {ErroDaApi} When the channel does not exist or the bot cannot post.
144
+ */
145
+ async enviar(code: string, texto: string, sessao?: string): Promise<Mensagem> {
146
+ const resposta = await this.#pedir<{ mensagem: Mensagem }>(
147
+ `/api/bots/channels/${encodeURIComponent(code)}/messages`,
148
+ { metodo: "POST", sessao, corpo: { content: texto } },
149
+ );
150
+ return resposta.mensagem;
151
+ }
152
+
153
+ /**
154
+ * Reads one page of channel history (up to 100 messages).
155
+ *
156
+ * @param code Channel code.
157
+ * @param options `antesDe` to page backwards; `sessao` to authenticate with a session.
158
+ * @returns The page, plus `temMais` telling whether another page exists.
159
+ * @throws {ErroDaApi} When the bot cannot read the channel.
160
+ */
161
+ historico(
162
+ code: string,
163
+ { antesDe, sessao }: { antesDe?: number; sessao?: string } = {},
164
+ ): Promise<{ sala: string; mensagens: Mensagem[]; temMais: boolean }> {
165
+ const consulta = antesDe ? `?antesDe=${antesDe}` : "";
166
+ return this.#pedir(
167
+ `/api/bots/channels/${encodeURIComponent(code)}/messages${consulta}`,
168
+ { sessao },
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Uploads a file (or a voice message) to a channel.
174
+ *
175
+ * The body goes raw, with the type in the headers — the same path the chat
176
+ * upload uses, without multipart.
177
+ *
178
+ * @param code Channel code.
179
+ * @param corpo File bytes.
180
+ * @param options File name, MIME type, whether it is a voice message, and session.
181
+ * @returns The message the server created.
182
+ * @throws {ErroDaApi} When the upload fails or the response carries no message.
183
+ */
184
+ async enviarArquivo(
185
+ code: string,
186
+ corpo: Uint8Array,
187
+ {
188
+ nome = "arquivo",
189
+ mime = "application/octet-stream",
190
+ tipo = "arquivo",
191
+ sessao,
192
+ }: { nome?: string; mime?: string; tipo?: "arquivo" | "voz"; sessao?: string } = {},
193
+ ): Promise<Mensagem> {
194
+ const resposta = await this.#fetch(
195
+ `${this.#base}/api/bots/channels/${encodeURIComponent(code)}/messages`,
196
+ {
197
+ method: "POST",
198
+ headers: {
199
+ Authorization: sessao ? `Bearer ${sessao}` : `Bot ${this.#token}`,
200
+ "Content-Type": mime,
201
+ // The server undoes the percent-encoding, so accented names survive intact.
202
+ "X-Vertice-Nome": encodeURIComponent(nome),
203
+ "X-Vertice-Tipo": tipo,
204
+ },
205
+ body: corpo as unknown as BodyInit,
206
+ },
207
+ );
208
+ const texto = await resposta.text();
209
+ const dados = (() => {
210
+ try {
211
+ return JSON.parse(texto) as { mensagem?: Mensagem; erro?: string; codigo?: string };
212
+ } catch {
213
+ return {} as { mensagem?: Mensagem; erro?: string; codigo?: string };
214
+ }
215
+ })();
216
+ if (!resposta.ok || !dados.mensagem)
217
+ throw new ErroDaApi(
218
+ dados.erro ?? `O servidor respondeu ${resposta.status}.`,
219
+ resposta.status,
220
+ dados.codigo ?? null,
221
+ );
222
+ return dados.mensagem;
223
+ }
224
+
225
+ /**
226
+ * Deletes one of the bot's own messages (the server refuses anyone else's).
227
+ *
228
+ * @param code Channel code the message belongs to.
229
+ * @param id Message id.
230
+ * @param sessao Session token; when absent, the bot token is used.
231
+ * @throws {ErroDaApi} When the message is not the bot's or is already gone.
232
+ */
233
+ async apagar(code: string, id: string, sessao?: string): Promise<void> {
234
+ await this.#pedir<null>(
235
+ `/api/bots/messages/${encodeURIComponent(id)}?canal=${encodeURIComponent(code)}`,
236
+ { metodo: "DELETE", sessao },
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Lists the intents the server granted, for callers that want to check before connecting.
242
+ *
243
+ * @returns The intent names granted to this bot.
244
+ * @throws {ErroDaApi} When discovery fails.
245
+ */
246
+ async intents(): Promise<Intencao[]> {
247
+ return (await this.descobrir()).intents;
248
+ }
249
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ // Vértice bot SDK entry point (`@openvertice/bot-sdk`). The contract belongs to
2
+ // the server (server/bots/README.md); this module exports only what a bot needs
3
+ // in order to exist.
4
+ /**
5
+ * Public surface of `@openvertice/bot-sdk`.
6
+ *
7
+ * Everything a bot needs lives behind this module: the {@link Bot} client, the
8
+ * {@link Voz} mesh client, the contract types and the audio helpers.
9
+ */
10
+ export { Bot, ErroDaApi } from "./cliente.js";
11
+ export type { ContextoDoComando, OpcoesDoBot } from "./cliente.js";
12
+ export type { EstadoDoGateway, OpcoesGateway } from "./gateway.js";
13
+ export type {
14
+ AnexoDaMensagem,
15
+ CanalDeTexto,
16
+ CanalDeVoz,
17
+ Descoberta,
18
+ Eventos,
19
+ Intencao,
20
+ Limites,
21
+ Mensagem,
22
+ PeerDaVoz,
23
+ PreviaDeLink,
24
+ ServidorIce,
25
+ Sessao,
26
+ SinalDeVoz,
27
+ } from "./tipos.js";
28
+ export { CONTRATO } from "./tipos.js";
29
+ export { Voz } from "./voz.js";
30
+ export type {
31
+ ConexaoDeVoz,
32
+ EntradaNaVoz,
33
+ FonteDeAudio,
34
+ OpcoesDeVoz,
35
+ TransporteDeVoz,
36
+ } from "./voz.js";
37
+ export { criarTransporteWrtc } from "./voz-wrtc.js";
38
+ export { tocarTom } from "./audio.js";
39
+ export type { OpcoesDoTom } from "./audio.js";
40
+ export { wavDeTom } from "./wav.js";
41
+ export type { OpcoesDoWav } from "./wav.js";