@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.
- package/README.md +189 -2
- package/dist/src/audio.d.ts +32 -0
- package/dist/src/audio.d.ts.map +1 -0
- package/dist/src/audio.js +47 -0
- package/dist/src/audio.js.map +1 -0
- package/dist/src/cliente.d.ts +213 -0
- package/dist/src/cliente.d.ts.map +1 -0
- package/dist/src/cliente.js +381 -0
- package/dist/src/cliente.js.map +1 -0
- package/dist/src/gateway.d.ts +76 -0
- package/dist/src/gateway.d.ts.map +1 -0
- package/dist/src/gateway.js +299 -0
- package/dist/src/gateway.js.map +1 -0
- package/dist/src/http.d.ts +119 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +189 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/tipos.d.ts +243 -0
- package/dist/src/tipos.d.ts.map +1 -0
- package/dist/src/tipos.js +5 -0
- package/dist/src/tipos.js.map +1 -0
- package/dist/src/voz-wrtc.d.ts +20 -0
- package/dist/src/voz-wrtc.d.ts.map +1 -0
- package/dist/src/voz-wrtc.js +68 -0
- package/dist/src/voz-wrtc.js.map +1 -0
- package/dist/src/voz.d.ts +150 -0
- package/dist/src/voz.d.ts.map +1 -0
- package/dist/src/voz.js +311 -0
- package/dist/src/voz.js.map +1 -0
- package/dist/src/wav.d.ts +29 -0
- package/dist/src/wav.d.ts.map +1 -0
- package/dist/src/wav.js +54 -0
- package/dist/src/wav.js.map +1 -0
- package/package.json +58 -3
- package/src/audio.ts +88 -0
- package/src/cliente.ts +472 -0
- package/src/gateway.ts +354 -0
- package/src/http.ts +249 -0
- package/src/index.ts +41 -0
- package/src/tipos.ts +220 -0
- package/src/voz-wrtc.ts +100 -0
- package/src/voz.ts +407 -0
- package/src/wav.ts +71 -0
- package/src/wrtc.d.ts +4 -0
package/src/cliente.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// The bot as its author sees it: `start`, `on`, `command`, `enviar`. Everything
|
|
2
|
+
// else (token, session, heartbeat, reconnection) stays underneath.
|
|
3
|
+
import { ErroDaApi, Http } from "./http.js";
|
|
4
|
+
import { Gateway, type EstadoDoGateway } from "./gateway.js";
|
|
5
|
+
import type {
|
|
6
|
+
CanalDeTexto,
|
|
7
|
+
ComandoAnunciado,
|
|
8
|
+
Descoberta,
|
|
9
|
+
ServidorIce,
|
|
10
|
+
Eventos,
|
|
11
|
+
Intencao,
|
|
12
|
+
Mensagem,
|
|
13
|
+
Sessao,
|
|
14
|
+
} from "./tipos.js";
|
|
15
|
+
|
|
16
|
+
/** Renews the session slightly before it expires, so it never dies mid-event. */
|
|
17
|
+
const RENOVAR_ANTES_MS = 120_000;
|
|
18
|
+
|
|
19
|
+
/** Everything needed to build a {@link Bot}. */
|
|
20
|
+
export interface OpcoesDoBot {
|
|
21
|
+
/** Bot token (`vtb_…`), copied from the admin panel (Bots tab). */
|
|
22
|
+
token: string;
|
|
23
|
+
/** Server base URL. Default: `http://localhost:8080`. */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** Intents this process wants; the server refuses whatever the panel did not grant. */
|
|
26
|
+
intents?: Intencao[];
|
|
27
|
+
/** Diagnostics: one line per state change. */
|
|
28
|
+
log?: (mensagem: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a command handler receives on every invocation. */
|
|
32
|
+
export interface ContextoDoComando {
|
|
33
|
+
/** The message that triggered the command. */
|
|
34
|
+
mensagem: Mensagem;
|
|
35
|
+
/** The channel where the command was typed, with its settings. */
|
|
36
|
+
canal: CanalDeTexto;
|
|
37
|
+
/** `!tocar a.mp3` → `["a.mp3"]` (already split on whitespace). */
|
|
38
|
+
args: string[];
|
|
39
|
+
/** Everything typed after the command name, as-is. */
|
|
40
|
+
resto: string;
|
|
41
|
+
/** Replies in the same channel. */
|
|
42
|
+
responder(texto: string): Promise<Mensagem>;
|
|
43
|
+
/** Deletes the triggering message — only works when the bot wrote it. */
|
|
44
|
+
apagar(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A registered command, as kept inside the bot. */
|
|
48
|
+
interface Comando {
|
|
49
|
+
/** Command name, lowercase and without the prefix. */
|
|
50
|
+
nome: string;
|
|
51
|
+
/** Optional summary announced to the client's autocomplete. */
|
|
52
|
+
descricao?: string;
|
|
53
|
+
/** Channels where it applies; empty = every channel the bot can speak in. */
|
|
54
|
+
canais: string[];
|
|
55
|
+
/** Handler invoked when the command matches. */
|
|
56
|
+
executar: (contexto: ContextoDoComando) => void | Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Handler signature for one gateway event. */
|
|
60
|
+
type Ouvinte<K extends keyof Eventos> = (dados: Eventos[K]) => void | Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A Vértice bot.
|
|
64
|
+
*
|
|
65
|
+
* Create one, register listeners with {@link on} and commands with
|
|
66
|
+
* {@link command}, then call {@link start}. Sessions, heartbeats, `resume` and
|
|
67
|
+
* reconnection are handled internally, so the author only has to react to
|
|
68
|
+
* events.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const bot = new Bot({ token: process.env.VERTICE_BOT_TOKEN! });
|
|
73
|
+
*
|
|
74
|
+
* bot.command("ping", { descricao: "responde pong" }, async ({ responder }) => {
|
|
75
|
+
* await responder("pong");
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* await bot.start();
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export class Bot {
|
|
82
|
+
readonly #http: Http;
|
|
83
|
+
readonly #intents: Intencao[];
|
|
84
|
+
readonly #log: (mensagem: string) => void;
|
|
85
|
+
readonly #ouvintes = new Map<string, Set<(dados: never) => void>>();
|
|
86
|
+
readonly #comandos: Comando[] = [];
|
|
87
|
+
readonly #ouvintesDeEstado = new Set<(estado: EstadoDoGateway) => void>();
|
|
88
|
+
#descoberta: Descoberta | null = null;
|
|
89
|
+
#sessao: Sessao | null = null;
|
|
90
|
+
#gateway: Gateway | null = null;
|
|
91
|
+
#ultimoErro: Error | null = null;
|
|
92
|
+
|
|
93
|
+
/** @param opcoes Token, base URL and the intents this process wants. */
|
|
94
|
+
constructor(opcoes: OpcoesDoBot) {
|
|
95
|
+
this.#http = new Http({ baseUrl: opcoes.baseUrl, token: opcoes.token });
|
|
96
|
+
this.#intents = opcoes.intents ?? ["MESSAGES"];
|
|
97
|
+
this.#log = opcoes.log ?? (() => {});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Identity the server assigned, or `null` before {@link start}. */
|
|
101
|
+
get bot(): Descoberta["bot"] | null {
|
|
102
|
+
return this.#descoberta?.bot ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Channels granted to the bot, or `null` before {@link start}. */
|
|
106
|
+
get canais(): Descoberta["canais"] | null {
|
|
107
|
+
return this.#descoberta?.canais ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** ICE servers from discovery (the bot's voice uses the same ones). */
|
|
111
|
+
get iceServers(): ServidorIce[] {
|
|
112
|
+
return this.#descoberta?.iceServers ?? [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Current gateway state, or `"parado"` before {@link start} and after {@link stop}. */
|
|
116
|
+
get estado(): EstadoDoGateway | "parado" {
|
|
117
|
+
return this.#gateway?.estado ?? "parado";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Registers a listener for every gateway state change.
|
|
122
|
+
*
|
|
123
|
+
* Voice uses it to leave the room when the connection drops: `resume` brings
|
|
124
|
+
* the events back, but not the seat in the voice room.
|
|
125
|
+
*
|
|
126
|
+
* @param ouvinte Called with the new state.
|
|
127
|
+
* @returns This bot, for chaining.
|
|
128
|
+
*/
|
|
129
|
+
aoMudarEstado(ouvinte: (estado: EstadoDoGateway) => void): this {
|
|
130
|
+
this.#ouvintesDeEstado.add(ouvinte);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Last connection error, for callers that want to show it in a `/status` command. */
|
|
135
|
+
get ultimoErro(): Error | null {
|
|
136
|
+
return this.#ultimoErro;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Discovers what the bot may do, opens a session and connects the gateway.
|
|
141
|
+
*
|
|
142
|
+
* @returns This bot, once the gateway reports `ready`.
|
|
143
|
+
* @throws {ErroDaApi} When discovery or session creation fails.
|
|
144
|
+
* @throws {Error} When the server refuses the bot.
|
|
145
|
+
*/
|
|
146
|
+
async start(): Promise<this> {
|
|
147
|
+
this.#descoberta = await this.#http.descobrir();
|
|
148
|
+
this.#log(
|
|
149
|
+
`conectando como @${this.#descoberta.bot.usuario} (${this.#descoberta.canais.texto.length} canal(is) de texto)`,
|
|
150
|
+
);
|
|
151
|
+
this.#sessao = await this.#http.abrirSessao();
|
|
152
|
+
this.#gateway = new Gateway({
|
|
153
|
+
url: this.#descoberta.gateway.url,
|
|
154
|
+
heartbeatMs: this.#descoberta.gateway.heartbeatMs,
|
|
155
|
+
intents: this.#intents,
|
|
156
|
+
comandos: this.#anunciarComandos(),
|
|
157
|
+
sessao: () => this.#sessaoValida(),
|
|
158
|
+
aoEvento: (t, d) => this.#despachar(t, d),
|
|
159
|
+
aoEstado: (estado, detalhe) => {
|
|
160
|
+
if (estado === "recusado") this.#ultimoErro = new Error(detalhe ?? estado);
|
|
161
|
+
this.#log(`gateway: ${estado}${detalhe ? ` (${detalhe})` : ""}`);
|
|
162
|
+
for (const ouvinte of this.#ouvintesDeEstado) {
|
|
163
|
+
try {
|
|
164
|
+
ouvinte(estado);
|
|
165
|
+
} catch (erro) {
|
|
166
|
+
this.#log(`ouvinte de estado falhou: ${erro}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
log: this.#log,
|
|
171
|
+
});
|
|
172
|
+
await this.#gateway.conectar();
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns a session that will not expire while the bot is connected.
|
|
178
|
+
*
|
|
179
|
+
* On reconnect it asks for a new one when the current session is about to
|
|
180
|
+
* expire. Rotating the token or disabling the bot kills the connection for
|
|
181
|
+
* good (4009) — in that case `start` fails on purpose, instead of retrying
|
|
182
|
+
* forever with a dead credential.
|
|
183
|
+
*
|
|
184
|
+
* @returns A session valid for at least {@link RENOVAR_ANTES_MS} more.
|
|
185
|
+
*/
|
|
186
|
+
async #sessaoValida(): Promise<Sessao> {
|
|
187
|
+
const atual = this.#sessao;
|
|
188
|
+
if (atual && atual.expiraEm - RENOVAR_ANTES_MS > Date.now()) return atual;
|
|
189
|
+
this.#sessao = await this.#http.abrirSessao();
|
|
190
|
+
return this.#sessao;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Sends a raw gateway action (voice uses it to join, leave and signal).
|
|
195
|
+
*
|
|
196
|
+
* @param tipo Action name understood by the server.
|
|
197
|
+
* @param dados Action payload.
|
|
198
|
+
* @throws {Error} When `start()` has not been called yet.
|
|
199
|
+
*/
|
|
200
|
+
agir(tipo: string, dados: Record<string, unknown> = {}): void {
|
|
201
|
+
if (!this.#gateway) throw new Error("Chame `start()` antes de agir no gateway.");
|
|
202
|
+
this.#gateway.agir(tipo, dados);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Closes the gateway for good and forgets the session. */
|
|
206
|
+
async stop(): Promise<void> {
|
|
207
|
+
this.#gateway?.fechar();
|
|
208
|
+
this.#gateway = null;
|
|
209
|
+
this.#sessao = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Subscribes to a gateway event (`message.created`, `voice.joined`, …).
|
|
214
|
+
*
|
|
215
|
+
* @param nome Event name.
|
|
216
|
+
* @param ouvinte Handler; it may be async. Failures are logged, never thrown.
|
|
217
|
+
* @returns This bot, for chaining.
|
|
218
|
+
*/
|
|
219
|
+
on<K extends keyof Eventos>(nome: K, ouvinte: Ouvinte<K>): this {
|
|
220
|
+
if (!this.#ouvintes.has(nome)) this.#ouvintes.set(nome, new Set());
|
|
221
|
+
this.#ouvintes.get(nome)!.add(ouvinte as (dados: never) => void);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Removes a listener previously registered with {@link on}.
|
|
227
|
+
*
|
|
228
|
+
* @param nome Event name.
|
|
229
|
+
* @param ouvinte The exact handler reference that was registered.
|
|
230
|
+
* @returns This bot, for chaining.
|
|
231
|
+
*/
|
|
232
|
+
off<K extends keyof Eventos>(nome: K, ouvinte: Ouvinte<K>): this {
|
|
233
|
+
this.#ouvintes.get(nome)?.delete(ouvinte as (dados: never) => void);
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Fans an event out to its listeners.
|
|
239
|
+
*
|
|
240
|
+
* Listener failures (a sync throw or a rejected promise) are logged and
|
|
241
|
+
* swallowed, so one broken handler cannot stop the others.
|
|
242
|
+
*
|
|
243
|
+
* @param nome Event name.
|
|
244
|
+
* @param dados Event payload.
|
|
245
|
+
*/
|
|
246
|
+
#despachar<K extends keyof Eventos>(nome: K, dados: Eventos[K]): void {
|
|
247
|
+
if (nome === "message.created") this.#talvezComando(dados as Eventos["message.created"]);
|
|
248
|
+
for (const ouvinte of this.#ouvintes.get(nome) ?? []) {
|
|
249
|
+
try {
|
|
250
|
+
const resultado = (ouvinte as (d: Eventos[K]) => unknown)(dados);
|
|
251
|
+
if (resultado instanceof Promise) resultado.catch((erro) => this.#log(`ouvinte falhou: ${erro}`));
|
|
252
|
+
} catch (erro) {
|
|
253
|
+
this.#log(`ouvinte falhou: ${erro}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---- commands (the everyday `!tocar`)
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Registers a command.
|
|
262
|
+
*
|
|
263
|
+
* The channel prefix (`!`) belongs to the server: here the name is just
|
|
264
|
+
* `tocar`. Without `canais`, the command works in every channel where the bot
|
|
265
|
+
* may speak. Every registered command is announced to the client's
|
|
266
|
+
* autocomplete, including ones registered after `start()`.
|
|
267
|
+
*
|
|
268
|
+
* @param nome Command name; a leading `!` or `/` is stripped.
|
|
269
|
+
* @param opcoesOuFn Either the options object or the handler itself.
|
|
270
|
+
* @param talvezFn Handler, when the second argument carries the options.
|
|
271
|
+
* @returns This bot, for chaining.
|
|
272
|
+
* @throws {Error} When no handler is provided.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* bot.command("ping", async ({ responder }) => {
|
|
277
|
+
* await responder("pong");
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* bot.command("eco", { descricao: "repete o texto" }, async ({ resto, responder }) => {
|
|
281
|
+
* await responder(resto);
|
|
282
|
+
* });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
command(
|
|
286
|
+
nome: string,
|
|
287
|
+
opcoesOuFn:
|
|
288
|
+
| { descricao?: string; canais?: string[] }
|
|
289
|
+
| ((contexto: ContextoDoComando) => void | Promise<void>),
|
|
290
|
+
talvezFn?: (contexto: ContextoDoComando) => void | Promise<void>,
|
|
291
|
+
): this {
|
|
292
|
+
const opcoes = typeof opcoesOuFn === "function" ? {} : opcoesOuFn;
|
|
293
|
+
const executar = typeof opcoesOuFn === "function" ? opcoesOuFn : talvezFn;
|
|
294
|
+
if (!executar) throw new Error(`O comando ${nome} não tem o que executar.`);
|
|
295
|
+
this.#comandos.push({
|
|
296
|
+
nome: nome.replace(/^[!\/]/, "").toLowerCase(),
|
|
297
|
+
...(opcoes.descricao ? { descricao: opcoes.descricao } : {}),
|
|
298
|
+
canais: (opcoes.canais ?? []).map((c) => c.toUpperCase()),
|
|
299
|
+
executar,
|
|
300
|
+
});
|
|
301
|
+
// Announced here and not only in `start()`: a command registered after
|
|
302
|
+
// connecting must reach the autocomplete too.
|
|
303
|
+
this.#anunciarComandos();
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Builds the list announced on `identify`: names and descriptions only, no handlers.
|
|
309
|
+
*
|
|
310
|
+
* It also pushes the list to a live gateway, so a command registered after
|
|
311
|
+
* `start()` appears in the client's autocomplete without reconnecting.
|
|
312
|
+
*
|
|
313
|
+
* @returns The list of commands to announce.
|
|
314
|
+
*/
|
|
315
|
+
#anunciarComandos(): ComandoAnunciado[] {
|
|
316
|
+
const anunciados = this.#comandos.map(({ nome, descricao }) =>
|
|
317
|
+
descricao ? { nome, descricao } : { nome },
|
|
318
|
+
);
|
|
319
|
+
// Already connected: send the new list without reconnecting (the client's
|
|
320
|
+
// autocomplete updates immediately).
|
|
321
|
+
if (this.#gateway?.estado === "pronto")
|
|
322
|
+
this.#gateway.agir("comandos", { comandos: anunciados });
|
|
323
|
+
return anunciados;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Registered commands, in registration order (handy in a `!help` command). */
|
|
327
|
+
get comandos(): { nome: string; descricao?: string }[] {
|
|
328
|
+
return this.#comandos.map(({ nome, descricao }) =>
|
|
329
|
+
descricao ? { nome, descricao } : { nome },
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Tries to run a command for a message that just arrived.
|
|
335
|
+
*
|
|
336
|
+
* Prefix handling, channel filtering and "the bot never runs its own
|
|
337
|
+
* commands" all live here.
|
|
338
|
+
*
|
|
339
|
+
* @param mensagem The `message.created` payload that just arrived.
|
|
340
|
+
*/
|
|
341
|
+
#talvezComando(mensagem: Eventos["message.created"]): void {
|
|
342
|
+
if (!mensagem.texto || !mensagem.sala) return;
|
|
343
|
+
// The bot does not run its own commands: otherwise `!eco !eco …` would loop.
|
|
344
|
+
if (mensagem.autorBot && mensagem.autorId === this.#descoberta?.bot.id) return;
|
|
345
|
+
const canal = this.#descoberta?.canais.texto.find((c) => c.code === mensagem.sala);
|
|
346
|
+
if (!canal) return;
|
|
347
|
+
const texto = mensagem.texto.trim();
|
|
348
|
+
const prefixo = canal.prefixo;
|
|
349
|
+
// Without a channel prefix, the command is simply the first token ("tocar x").
|
|
350
|
+
const semPrefixo = prefixo && texto.startsWith(prefixo) ? texto.slice(prefixo.length) : texto;
|
|
351
|
+
if (prefixo && !texto.startsWith(prefixo)) return;
|
|
352
|
+
const [bruto = "", ...args] = semPrefixo.split(/\s+/);
|
|
353
|
+
const nome = bruto.toLowerCase();
|
|
354
|
+
const comando = this.#comandos.find(
|
|
355
|
+
(c) => c.nome === nome && (c.canais.length === 0 || c.canais.includes(canal.code)),
|
|
356
|
+
);
|
|
357
|
+
if (!comando) return;
|
|
358
|
+
const contexto: ContextoDoComando = {
|
|
359
|
+
mensagem,
|
|
360
|
+
canal,
|
|
361
|
+
args,
|
|
362
|
+
resto: semPrefixo.slice(bruto.length).trim(),
|
|
363
|
+
responder: (resposta) => this.enviar(canal.code, resposta),
|
|
364
|
+
apagar: () => this.#http.apagar(canal.code, mensagem.id, this.#sessao?.sessao),
|
|
365
|
+
};
|
|
366
|
+
Promise.resolve(comando.executar(contexto)).catch((erro: Error) =>
|
|
367
|
+
this.#log(`comando ${comando.nome} falhou: ${erro.message}`),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---- messages
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Sends a text message.
|
|
375
|
+
*
|
|
376
|
+
* It goes over HTTP on purpose: the response carries the created item (id,
|
|
377
|
+
* timestamp), and the gateway delivers `message.created` to everyone anyway.
|
|
378
|
+
*
|
|
379
|
+
* @param canal Channel code (case-insensitive; uppercased before sending).
|
|
380
|
+
* @param texto Message content.
|
|
381
|
+
* @returns The message the server created.
|
|
382
|
+
* @throws {ErroDaApi} When the bot cannot post in the channel.
|
|
383
|
+
*/
|
|
384
|
+
enviar(canal: string, texto: string): Promise<Mensagem> {
|
|
385
|
+
return this.#http.enviar(canal.toUpperCase(), texto, this.#sessao?.sessao);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Sends audio as a **voice message** — the item the client renders as a
|
|
390
|
+
* "voice message" and plays inline.
|
|
391
|
+
*
|
|
392
|
+
* @param canal Channel code.
|
|
393
|
+
* @param audio The audio bytes (WAV by default; see `wavDeTom`).
|
|
394
|
+
* @param opcoes File name and MIME type; defaults to `recado.wav` / `audio/wav`.
|
|
395
|
+
* @returns The message the server created.
|
|
396
|
+
* @throws {ErroDaApi} When the upload fails.
|
|
397
|
+
*/
|
|
398
|
+
enviarVoz(
|
|
399
|
+
canal: string,
|
|
400
|
+
audio: Uint8Array,
|
|
401
|
+
opcoes: { nome?: string; mime?: string } = {},
|
|
402
|
+
): Promise<Mensagem> {
|
|
403
|
+
return this.#http.enviarArquivo(canal.toUpperCase(), audio, {
|
|
404
|
+
nome: opcoes.nome ?? "recado.wav",
|
|
405
|
+
mime: opcoes.mime ?? "audio/wav",
|
|
406
|
+
tipo: "voz",
|
|
407
|
+
sessao: this.#sessao?.sessao,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Sends any other file (image, PDF, audio as a regular attachment).
|
|
413
|
+
*
|
|
414
|
+
* @param canal Channel code.
|
|
415
|
+
* @param conteudo File bytes.
|
|
416
|
+
* @param opcoes File name and MIME type; defaults to `arquivo` /
|
|
417
|
+
* `application/octet-stream`.
|
|
418
|
+
* @returns The message the server created.
|
|
419
|
+
* @throws {ErroDaApi} When the upload fails.
|
|
420
|
+
*/
|
|
421
|
+
enviarArquivo(
|
|
422
|
+
canal: string,
|
|
423
|
+
conteudo: Uint8Array,
|
|
424
|
+
opcoes: { nome?: string; mime?: string } = {},
|
|
425
|
+
): Promise<Mensagem> {
|
|
426
|
+
return this.#http.enviarArquivo(canal.toUpperCase(), conteudo, {
|
|
427
|
+
nome: opcoes.nome ?? "arquivo",
|
|
428
|
+
mime: opcoes.mime ?? "application/octet-stream",
|
|
429
|
+
tipo: "arquivo",
|
|
430
|
+
sessao: this.#sessao?.sessao,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Reads a page of history (100 items; `antesDe` pages backwards).
|
|
436
|
+
*
|
|
437
|
+
* @param canal Channel code.
|
|
438
|
+
* @param antesDe Timestamp cursor: returns messages older than this one.
|
|
439
|
+
* @returns The page, plus `temMais` telling whether another page exists.
|
|
440
|
+
* @throws {ErroDaApi} When the bot cannot read the channel.
|
|
441
|
+
*/
|
|
442
|
+
historico(canal: string, antesDe?: number) {
|
|
443
|
+
return this.#http.historico(canal.toUpperCase(), {
|
|
444
|
+
...(antesDe ? { antesDe } : {}),
|
|
445
|
+
sessao: this.#sessao?.sessao,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Deletes a message the bot wrote.
|
|
451
|
+
*
|
|
452
|
+
* @param canal Channel code.
|
|
453
|
+
* @param id Message id.
|
|
454
|
+
* @throws {ErroDaApi} When the message is not the bot's or is already gone.
|
|
455
|
+
*/
|
|
456
|
+
apagar(canal: string, id: string): Promise<void> {
|
|
457
|
+
return this.#http.apagar(canal.toUpperCase(), id, this.#sessao?.sessao);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Re-runs only the discovery step (e.g. after changing channels in the panel).
|
|
462
|
+
*
|
|
463
|
+
* @returns The fresh discovery document.
|
|
464
|
+
* @throws {ErroDaApi} When the token is rejected.
|
|
465
|
+
*/
|
|
466
|
+
async recarregar(): Promise<Descoberta> {
|
|
467
|
+
this.#descoberta = await this.#http.descobrir();
|
|
468
|
+
return this.#descoberta;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export { ErroDaApi };
|