fivelive.js 0.2.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 +181 -0
- package/package.json +32 -0
- package/src/client.js +289 -0
- package/src/embed.js +65 -0
- package/src/index.d.ts +250 -0
- package/src/index.js +7 -0
- package/src/rest.js +75 -0
- package/src/structures.js +257 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# fivelive.js
|
|
2
|
+
|
|
3
|
+
SDK oficial para criar bots do **Five Live**. Você recebe as mensagens em tempo real e responde, reage, manda embeds e cria comandos, do mesmo jeito que no discord.js.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { Client } from "fivelive.js";
|
|
7
|
+
|
|
8
|
+
const client = new Client({ token: process.env.FIVELIVE_TOKEN });
|
|
9
|
+
|
|
10
|
+
client.on("ready", (bot) => console.log(`Online como ${bot.displayName}`));
|
|
11
|
+
|
|
12
|
+
client.command({
|
|
13
|
+
name: "ping",
|
|
14
|
+
run: ({ reply }) => reply("🏓 pong!"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
client.login();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Começando
|
|
21
|
+
|
|
22
|
+
1. Crie o bot em **https://live.fivenetwork.dev/developers** e copie o **token** (começa com `b_`). Guarde bem: ele é a senha do bot.
|
|
23
|
+
2. Adicione o bot a um grupo pelo link **"Adicionar a um grupo"** na página dele.
|
|
24
|
+
3. Instale e rode:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install fivelive.js
|
|
28
|
+
FIVELIVE_TOKEN=b_seu_token node bot.mjs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
É preciso o Node 18 ou mais novo. No Node 22 ou mais novo não precisa de mais nada; no 18 e no 20 o pacote `ws` é instalado junto.
|
|
32
|
+
|
|
33
|
+
## Eventos
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
client.on("messageCreate", async (message) => {
|
|
37
|
+
if (message.content === "oi") await message.reply(`Oi, ${message.author}!`);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Evento | Recebe |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `ready` | o usuário do bot, depois de conectar e carregar os grupos |
|
|
44
|
+
| `messageCreate` | `Message` nova (as do próprio bot não chegam; as de outros bots só com `ignoreBots: false`) |
|
|
45
|
+
| `messageUpdate` | `Message` editada |
|
|
46
|
+
| `messageDelete` | `{ id, groupId, channelId }` |
|
|
47
|
+
| `reactionUpdate` | `{ messageId, groupId, channelId, reactions }` |
|
|
48
|
+
| `typingStart` | `{ userId, name, groupId, channelId }` |
|
|
49
|
+
| `memberAdd` | `{ user, group, groupId, via }`, quando alguém entra num grupo do bot |
|
|
50
|
+
| `memberRemove` | `{ userId, user, group, groupId, reason, by }` — reason: `left`, `kicked` ou `banned` |
|
|
51
|
+
| `groupJoin` | `Group`, quando adicionam o bot a um grupo |
|
|
52
|
+
| `groupLeave` | `{ id, reason }`, quando o bot sai, é expulso ou o grupo é apagado |
|
|
53
|
+
| `groupUpdate` | `Group`, quando o grupo muda (nome, salas, cargos) |
|
|
54
|
+
| `reconnecting` / `reconnected` / `disconnect` | a conexão caiu e voltou sozinha |
|
|
55
|
+
| `error` | `Error` |
|
|
56
|
+
| `commandError` | `(error, context)`, quando um comando lança um erro |
|
|
57
|
+
|
|
58
|
+
## Comandos
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
const client = new Client({ token, prefix: "!" });
|
|
62
|
+
|
|
63
|
+
client.command({
|
|
64
|
+
name: "dado",
|
|
65
|
+
aliases: ["d"],
|
|
66
|
+
description: "Rola um dado: !dado 20",
|
|
67
|
+
run: async ({ args, message, reply }) => {
|
|
68
|
+
const lados = Number(args[0]) || 6;
|
|
69
|
+
await message.channel.sendTyping();
|
|
70
|
+
await reply(`🎲 ${1 + Math.floor(Math.random() * lados)}`);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Mensagens
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
await message.reply("resposta citando a mensagem");
|
|
79
|
+
await message.channel.send("mensagem normal");
|
|
80
|
+
await message.react("👍");
|
|
81
|
+
await message.unreact("👍");
|
|
82
|
+
|
|
83
|
+
const minha = await message.channel.send("vou editar");
|
|
84
|
+
await minha.edit("editada");
|
|
85
|
+
await minha.delete(); // só as mensagens do próprio bot
|
|
86
|
+
|
|
87
|
+
message.content // o texto como está gravado, com as menções como <@id>
|
|
88
|
+
message.cleanContent // com as menções trocadas por @nome
|
|
89
|
+
message.author // User: id, username, displayName, avatarURL, bot
|
|
90
|
+
message.mentions // { users: [...ids], roles: [...ids], everyone }
|
|
91
|
+
message.mentionsMe // se mencionou o bot
|
|
92
|
+
message.group / message.channel
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Menções
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { userMention } from "fivelive.js";
|
|
99
|
+
|
|
100
|
+
await channel.send(`Bem-vindo, ${userMention(id)}!`); // ou `${message.author}`
|
|
101
|
+
await channel.send({ content: "Atenção, mods", roleMentions: [roleId] });
|
|
102
|
+
await channel.send({ content: "Todo mundo!", everyone: true }); // precisa da permissão
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A menção avisa a pessoa, igual a uma menção feita por alguém do grupo. As permissões do cargo do bot valem normalmente ("Mencionar membros", "Mencionar @everyone", cargo mencionável).
|
|
106
|
+
|
|
107
|
+
### Embeds
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
import { EmbedBuilder } from "fivelive.js";
|
|
111
|
+
|
|
112
|
+
const embed = new EmbedBuilder()
|
|
113
|
+
.setTitle("Placar")
|
|
114
|
+
.setDescription("Resultado da rodada")
|
|
115
|
+
.setColor("#3b6cf6")
|
|
116
|
+
.setAuthor({ name: "Five Bot", iconURL: "https://..." })
|
|
117
|
+
.addFields(
|
|
118
|
+
{ name: "Time A", value: "3", inline: true },
|
|
119
|
+
{ name: "Time B", value: "1", inline: true },
|
|
120
|
+
)
|
|
121
|
+
.setImage("https://...")
|
|
122
|
+
.setFooter({ text: "Five Live" })
|
|
123
|
+
.setTimestamp();
|
|
124
|
+
|
|
125
|
+
await message.channel.send({ content: "Fim de jogo!", embeds: [embed] });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Grupos, salas e membros
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
for (const group of client.groups.values()) {
|
|
132
|
+
console.log(group.name, [...group.channels.values()].map((c) => c.name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const geral = client.groups.get(groupId).channel("geral"); // pelo id ou pelo nome
|
|
136
|
+
await geral.send("Bom dia!");
|
|
137
|
+
|
|
138
|
+
const historico = await geral.fetchMessages({ limit: 20 });
|
|
139
|
+
const membros = await group.fetchMembers(); // com roleIds, owner e online
|
|
140
|
+
const pessoa = await client.fetchUser(userId);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Moderação
|
|
144
|
+
|
|
145
|
+
Tudo respeita as permissões do cargo do bot e a hierarquia: ele só age em quem está abaixo do cargo mais alto dele.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
if (group.can("kickMembers")) await group.kick(userId);
|
|
149
|
+
await group.ban(userId, "flood"); // banMembers
|
|
150
|
+
await group.unban(userId);
|
|
151
|
+
const banidos = await group.fetchBans();
|
|
152
|
+
await group.addRole(userId, roleId); // manageRoles
|
|
153
|
+
await group.removeRole(userId, roleId);
|
|
154
|
+
await message.pin(); // manageMessages
|
|
155
|
+
await message.delete(); // de outras pessoas: manageMessages
|
|
156
|
+
await group.leave();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Limites
|
|
160
|
+
|
|
161
|
+
- **60 mensagens por minuto** por bot. Passando disso, o SDK espera o tempo que a API pede e tenta de novo sozinho.
|
|
162
|
+
- Texto de até **2000** caracteres e até **10 embeds** por mensagem.
|
|
163
|
+
- O bot só vê os grupos em que foi adicionado, e só as salas de texto.
|
|
164
|
+
|
|
165
|
+
## Erros
|
|
166
|
+
|
|
167
|
+
Uma chamada recusada pela API lança `FiveLiveAPIError`, com `status`, `method` e `path`:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { FiveLiveAPIError } from "fivelive.js";
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await message.delete();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err instanceof FiveLiveAPIError && err.status === 403) console.log("Não posso apagar essa.");
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Sem o SDK
|
|
180
|
+
|
|
181
|
+
A API por trás é HTTP + WebSocket e pode ser usada de qualquer linguagem. A documentação está em https://live.fivenetwork.dev/developers.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fivelive.js",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDK oficial para criar bots do Five Live — eventos em tempo real, mensagens, embeds, reações e comandos.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"ws": "^8.18.0"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"fivelive",
|
|
26
|
+
"bot",
|
|
27
|
+
"sdk",
|
|
28
|
+
"chat"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://live.fivenetwork.dev/developers"
|
|
32
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// O Client: conecta no WebSocket do Five Live com o token do bot, mantém a
|
|
2
|
+
// conexão viva (e reconecta sozinho), transforma os avisos da API em eventos
|
|
3
|
+
// e guarda grupos e usuários conhecidos.
|
|
4
|
+
//
|
|
5
|
+
// Eventos:
|
|
6
|
+
// ready(user) conectou e carregou os grupos
|
|
7
|
+
// messageCreate(message) mensagem nova num grupo do bot
|
|
8
|
+
// messageUpdate(message) mensagem editada
|
|
9
|
+
// messageDelete({ id, groupId, channelId })
|
|
10
|
+
// reactionUpdate({ messageId, groupId, channelId, reactions })
|
|
11
|
+
// typingStart({ userId, name, groupId, channelId })
|
|
12
|
+
// groupJoin(group) / groupLeave({ id, reason }) / groupUpdate(group)
|
|
13
|
+
// reconnecting(attempt) / reconnected() / disconnect() / error(err)
|
|
14
|
+
// commandError(err, context)
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
import { REST } from "./rest.js";
|
|
18
|
+
import { Group, Message, User } from "./structures.js";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_API = "https://api.fivenetwork.dev";
|
|
21
|
+
const KEEPALIVE_MS = 30_000;
|
|
22
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
async function socketClass() {
|
|
25
|
+
if (typeof globalThis.WebSocket === "function") return globalThis.WebSocket;
|
|
26
|
+
try {
|
|
27
|
+
const mod = await import("ws");
|
|
28
|
+
return mod.default ?? mod.WebSocket;
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error("Este Node não tem WebSocket nativo (é do Node 22+). Rode `npm install ws` ou atualize o Node.");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class Client extends EventEmitter {
|
|
35
|
+
/**
|
|
36
|
+
* @param {{
|
|
37
|
+
* token: string,
|
|
38
|
+
* apiUrl?: string,
|
|
39
|
+
* gatewayUrl?: string,
|
|
40
|
+
* prefix?: string,
|
|
41
|
+
* ignoreBots?: boolean,
|
|
42
|
+
* }} options
|
|
43
|
+
*/
|
|
44
|
+
constructor(options) {
|
|
45
|
+
super();
|
|
46
|
+
if (!options?.token || !String(options.token).startsWith("b_")) {
|
|
47
|
+
throw new Error("Passe o token do bot (começa com b_). Pegue em https://live.fivenetwork.dev/developers.");
|
|
48
|
+
}
|
|
49
|
+
this.token = options.token;
|
|
50
|
+
this.apiUrl = (options.apiUrl ?? DEFAULT_API).replace(/\/$/, "");
|
|
51
|
+
this.gatewayUrl = options.gatewayUrl ?? `${this.apiUrl.replace(/^http/, "ws")}/ws`;
|
|
52
|
+
this.prefix = options.prefix ?? "!";
|
|
53
|
+
this.ignoreBots = options.ignoreBots ?? true;
|
|
54
|
+
this.rest = new REST({ token: this.token, apiUrl: this.apiUrl });
|
|
55
|
+
/** @type {User | null} */
|
|
56
|
+
this.user = null;
|
|
57
|
+
/** @type {Map<string, Group>} */
|
|
58
|
+
this.groups = new Map();
|
|
59
|
+
/** @type {Map<string, User>} */
|
|
60
|
+
this.users = new Map();
|
|
61
|
+
/** @type {Map<string, { name: string, description?: string, aliases?: string[], run: Function }>} */
|
|
62
|
+
this.commands = new Map();
|
|
63
|
+
this._ws = null;
|
|
64
|
+
this._ready = false;
|
|
65
|
+
this._closing = false;
|
|
66
|
+
this._attempt = 0;
|
|
67
|
+
this._keepalive = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Comandos com prefixo ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Registra um comando: `!nome arg1 arg2`.
|
|
74
|
+
* @param {{ name: string, description?: string, aliases?: string[], run: (ctx: { message: Message, args: string[], client: Client, reply: (content: any) => Promise<Message> }) => any }} command
|
|
75
|
+
*/
|
|
76
|
+
command(command) {
|
|
77
|
+
this.commands.set(command.name.toLowerCase(), command);
|
|
78
|
+
for (const alias of command.aliases ?? []) this.commands.set(alias.toLowerCase(), command);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {Message} message */
|
|
83
|
+
async _runCommand(message) {
|
|
84
|
+
if (!this.commands.size || !message.content.startsWith(this.prefix)) return;
|
|
85
|
+
const [name, ...args] = message.content.slice(this.prefix.length).trim().split(/\s+/);
|
|
86
|
+
const command = this.commands.get((name ?? "").toLowerCase());
|
|
87
|
+
if (!command) return;
|
|
88
|
+
const context = { message, args, client: this, reply: (content) => message.reply(content) };
|
|
89
|
+
try {
|
|
90
|
+
await command.run(context);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (this.listenerCount("commandError")) this.emit("commandError", err, context);
|
|
93
|
+
else this.emit("error", err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Conexão ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/** Conecta. Resolve quando o bot estiver pronto (evento "ready"). */
|
|
100
|
+
async login() {
|
|
101
|
+
const me = await this.rest.get("/bot/@me");
|
|
102
|
+
this.user = this._cacheUser({ ...me.bot, bot: true });
|
|
103
|
+
await this._loadGroups();
|
|
104
|
+
this._closing = false;
|
|
105
|
+
await this._connect();
|
|
106
|
+
return this.user;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Desconecta de vez (sem reconectar). */
|
|
110
|
+
destroy() {
|
|
111
|
+
this._closing = true;
|
|
112
|
+
clearInterval(this._keepalive);
|
|
113
|
+
try {
|
|
114
|
+
this._ws?.close();
|
|
115
|
+
} catch {
|
|
116
|
+
// já fechado
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _loadGroups() {
|
|
121
|
+
const data = await this.rest.get("/bot/groups");
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const raw of data.groups) {
|
|
124
|
+
seen.add(raw.id);
|
|
125
|
+
const held = this.groups.get(raw.id);
|
|
126
|
+
if (held) held._patch(raw);
|
|
127
|
+
else this.groups.set(raw.id, new Group(this, raw));
|
|
128
|
+
}
|
|
129
|
+
for (const id of [...this.groups.keys()]) if (!seen.has(id)) this.groups.delete(id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Um grupo, relido da API. @param {string} id */
|
|
133
|
+
async fetchGroup(id) {
|
|
134
|
+
const data = await this.rest.get(`/bot/groups/${encodeURIComponent(id)}`);
|
|
135
|
+
const held = this.groups.get(id);
|
|
136
|
+
if (held) held._patch(data.group);
|
|
137
|
+
else this.groups.set(id, new Group(this, data.group));
|
|
138
|
+
return this.groups.get(id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Uma pessoa pelo id. @param {string} id */
|
|
142
|
+
async fetchUser(id) {
|
|
143
|
+
const data = await this.rest.get(`/bot/users/${encodeURIComponent(id)}`);
|
|
144
|
+
return this._cacheUser(data.user);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {Record<string, any>} data */
|
|
148
|
+
_cacheUser(data) {
|
|
149
|
+
const user = new User(this, data);
|
|
150
|
+
this.users.set(user.id, user);
|
|
151
|
+
return user;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async _connect() {
|
|
155
|
+
const Socket = await socketClass();
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const ws = new Socket(this.gatewayUrl);
|
|
158
|
+
this._ws = ws;
|
|
159
|
+
let settled = false;
|
|
160
|
+
const send = (payload) => {
|
|
161
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(payload));
|
|
162
|
+
};
|
|
163
|
+
ws.onopen = () => {
|
|
164
|
+
send({ type: "register", name: this.user.displayName || this.user.username, token: this.token });
|
|
165
|
+
clearInterval(this._keepalive);
|
|
166
|
+
// O proxy da hospedagem derruba conexão parada: uma mensagem leve de tempos em tempos.
|
|
167
|
+
this._keepalive = setInterval(() => send({ type: "time-sync", t0: Date.now() }), KEEPALIVE_MS);
|
|
168
|
+
};
|
|
169
|
+
ws.onmessage = (event) => {
|
|
170
|
+
let msg;
|
|
171
|
+
try {
|
|
172
|
+
msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString());
|
|
173
|
+
} catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (msg.type === "registered") {
|
|
177
|
+
const first = !this._ready;
|
|
178
|
+
this._ready = true;
|
|
179
|
+
this._attempt = 0;
|
|
180
|
+
if (!settled) {
|
|
181
|
+
settled = true;
|
|
182
|
+
resolve();
|
|
183
|
+
}
|
|
184
|
+
if (first) this.emit("ready", this.user);
|
|
185
|
+
else this.emit("reconnected");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (msg.type === "register-error") {
|
|
189
|
+
this._closing = true;
|
|
190
|
+
const err = new Error(msg.message || "O Five Live recusou o token do bot.");
|
|
191
|
+
if (!settled) {
|
|
192
|
+
settled = true;
|
|
193
|
+
reject(err);
|
|
194
|
+
} else this.emit("error", err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
this._dispatch(msg).catch((err) => this.emit("error", err));
|
|
198
|
+
};
|
|
199
|
+
ws.onerror = (event) => {
|
|
200
|
+
if (!settled && this._attempt === 0 && !this._ready) {
|
|
201
|
+
settled = true;
|
|
202
|
+
reject(new Error(`Não foi possível conectar em ${this.gatewayUrl}: ${event?.message ?? "erro de rede"}`));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
ws.onclose = () => {
|
|
206
|
+
clearInterval(this._keepalive);
|
|
207
|
+
this.emit("disconnect");
|
|
208
|
+
if (this._closing || !this._ready) return;
|
|
209
|
+
this._reconnect();
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_reconnect() {
|
|
215
|
+
this._attempt += 1;
|
|
216
|
+
const delay = Math.min(MAX_BACKOFF_MS, 1000 * 2 ** Math.min(this._attempt - 1, 5)) + Math.random() * 500;
|
|
217
|
+
this.emit("reconnecting", this._attempt);
|
|
218
|
+
setTimeout(async () => {
|
|
219
|
+
if (this._closing) return;
|
|
220
|
+
try {
|
|
221
|
+
// O que mudou enquanto estava fora (grupos que entrou/saiu).
|
|
222
|
+
await this._loadGroups();
|
|
223
|
+
await this._connect();
|
|
224
|
+
} catch {
|
|
225
|
+
this._reconnect();
|
|
226
|
+
}
|
|
227
|
+
}, delay);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {Record<string, any>} msg */
|
|
231
|
+
async _dispatch(msg) {
|
|
232
|
+
switch (msg.type) {
|
|
233
|
+
case "group-message": {
|
|
234
|
+
if (!msg.message) return;
|
|
235
|
+
const message = new Message(this, msg.message, msg.author ? { ...msg.author } : undefined);
|
|
236
|
+
if (message.author.id === this.user?.id) return;
|
|
237
|
+
if (this.ignoreBots && message.author.bot) return;
|
|
238
|
+
this.emit("messageCreate", message);
|
|
239
|
+
await this._runCommand(message);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
case "group-message-updated":
|
|
243
|
+
if (msg.message) this.emit("messageUpdate", new Message(this, msg.message));
|
|
244
|
+
return;
|
|
245
|
+
case "group-message-deleted":
|
|
246
|
+
this.emit("messageDelete", { id: msg.messageId, groupId: msg.groupId, channelId: msg.channelId });
|
|
247
|
+
return;
|
|
248
|
+
case "group-message-reactions":
|
|
249
|
+
this.emit("reactionUpdate", { messageId: msg.messageId, groupId: msg.groupId, channelId: msg.channelId, reactions: msg.reactions ?? [] });
|
|
250
|
+
return;
|
|
251
|
+
case "group-typing":
|
|
252
|
+
if (msg.userId !== this.user?.id && msg.typing !== false) {
|
|
253
|
+
this.emit("typingStart", { userId: msg.userId, name: msg.name, groupId: msg.groupId, channelId: msg.channelId });
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
case "group-joined": {
|
|
257
|
+
const group = await this.fetchGroup(msg.groupId);
|
|
258
|
+
this.emit("groupJoin", group);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case "group-updated": {
|
|
262
|
+
if (!this.groups.has(msg.groupId)) return;
|
|
263
|
+
const group = await this.fetchGroup(msg.groupId).catch(() => null);
|
|
264
|
+
if (group) this.emit("groupUpdate", group);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "group-member-add": {
|
|
268
|
+
if (!msg.member) return;
|
|
269
|
+
const user = this._cacheUser(msg.member);
|
|
270
|
+
const group = this.groups.get(msg.groupId) ?? (await this.fetchGroup(msg.groupId).catch(() => null));
|
|
271
|
+
if (group) group.memberCount += 1;
|
|
272
|
+
this.emit("memberAdd", { user, group, groupId: msg.groupId, via: msg.via ?? null });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
case "group-member-remove": {
|
|
276
|
+
const group = this.groups.get(msg.groupId) ?? null;
|
|
277
|
+
if (group) group.memberCount = Math.max(0, group.memberCount - 1);
|
|
278
|
+
this.emit("memberRemove", { userId: msg.userId, user: this.users.get(msg.userId) ?? null, group, groupId: msg.groupId, reason: msg.reason ?? null, by: msg.by ?? null });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case "group-removed":
|
|
282
|
+
this.groups.delete(msg.groupId);
|
|
283
|
+
this.emit("groupLeave", { id: msg.groupId, reason: msg.reason ?? null });
|
|
284
|
+
return;
|
|
285
|
+
default:
|
|
286
|
+
this.emit("raw", msg);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/embed.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Construtor de embeds no mesmo formato do discord.js (e do webhook do Five
|
|
2
|
+
// Live): título, descrição, cor, autor, campos, imagem, miniatura, rodapé.
|
|
3
|
+
|
|
4
|
+
/** @param {string | number} color */
|
|
5
|
+
function toColor(color) {
|
|
6
|
+
if (typeof color === "number") return color;
|
|
7
|
+
const hex = String(color).replace(/^#/, "");
|
|
8
|
+
return /^[0-9a-f]{6}$/i.test(hex) ? parseInt(hex, 16) : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class EmbedBuilder {
|
|
12
|
+
/** @param {Record<string, unknown>} [data] */
|
|
13
|
+
constructor(data = {}) {
|
|
14
|
+
/** @type {Record<string, any>} */
|
|
15
|
+
this.data = { ...data };
|
|
16
|
+
}
|
|
17
|
+
setTitle(title) {
|
|
18
|
+
this.data.title = title;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
setDescription(description) {
|
|
22
|
+
this.data.description = description;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
setURL(url) {
|
|
26
|
+
this.data.url = url;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
/** @param {string | number} color "#3b6cf6" ou 0x3b6cf6 */
|
|
30
|
+
setColor(color) {
|
|
31
|
+
this.data.color = toColor(color);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** @param {{ name: string, url?: string, iconURL?: string }} author */
|
|
35
|
+
setAuthor(author) {
|
|
36
|
+
this.data.author = { name: author.name, url: author.url, icon_url: author.iconURL };
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
/** @param {{ text: string, iconURL?: string }} footer */
|
|
40
|
+
setFooter(footer) {
|
|
41
|
+
this.data.footer = { text: footer.text, icon_url: footer.iconURL };
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
setImage(url) {
|
|
45
|
+
this.data.image = { url };
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
setThumbnail(url) {
|
|
49
|
+
this.data.thumbnail = { url };
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
/** @param {Date | number | string} [timestamp] */
|
|
53
|
+
setTimestamp(timestamp = new Date()) {
|
|
54
|
+
this.data.timestamp = new Date(timestamp).toISOString();
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/** @param {...{ name: string, value: string, inline?: boolean }} fields */
|
|
58
|
+
addFields(...fields) {
|
|
59
|
+
this.data.fields = [...(this.data.fields ?? []), ...fields.flat()];
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
toJSON() {
|
|
63
|
+
return this.data;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
/** O token do bot (começa com b_), do portal do desenvolvedor. */
|
|
5
|
+
token: string;
|
|
6
|
+
/** Padrão: https://api.fivenetwork.dev */
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
/** Padrão: o apiUrl com ws(s):// e /ws. */
|
|
9
|
+
gatewayUrl?: string;
|
|
10
|
+
/** Prefixo dos comandos registrados com client.command(). Padrão: "!" */
|
|
11
|
+
prefix?: string;
|
|
12
|
+
/** Ignorar mensagens de outros bots. Padrão: true */
|
|
13
|
+
ignoreBots?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EmbedData {
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
color?: number;
|
|
21
|
+
author?: { name: string; url?: string; icon_url?: string };
|
|
22
|
+
footer?: { text: string; icon_url?: string };
|
|
23
|
+
image?: { url: string };
|
|
24
|
+
thumbnail?: { url: string };
|
|
25
|
+
timestamp?: string;
|
|
26
|
+
fields?: { name: string; value: string; inline?: boolean }[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class EmbedBuilder {
|
|
30
|
+
constructor(data?: EmbedData);
|
|
31
|
+
data: EmbedData;
|
|
32
|
+
setTitle(title: string): this;
|
|
33
|
+
setDescription(description: string): this;
|
|
34
|
+
setURL(url: string): this;
|
|
35
|
+
/** "#3b6cf6" ou 0x3b6cf6 */
|
|
36
|
+
setColor(color: string | number): this;
|
|
37
|
+
setAuthor(author: { name: string; url?: string; iconURL?: string }): this;
|
|
38
|
+
setFooter(footer: { text: string; iconURL?: string }): this;
|
|
39
|
+
setImage(url: string): this;
|
|
40
|
+
setThumbnail(url: string): this;
|
|
41
|
+
setTimestamp(timestamp?: Date | number | string): this;
|
|
42
|
+
addFields(...fields: ({ name: string; value: string; inline?: boolean } | { name: string; value: string; inline?: boolean }[])[]): this;
|
|
43
|
+
toJSON(): EmbedData;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MessageOptions {
|
|
47
|
+
content?: string;
|
|
48
|
+
embeds?: (EmbedBuilder | EmbedData)[];
|
|
49
|
+
/** ids de pessoas a mencionar (além dos <@id> no texto). */
|
|
50
|
+
mentions?: string[];
|
|
51
|
+
/** ids de cargos a mencionar (o cargo precisa ser mencionável, ou o bot ter "Mencionar @everyone"). */
|
|
52
|
+
roleMentions?: string[];
|
|
53
|
+
/** @everyone (precisa da permissão). */
|
|
54
|
+
everyone?: boolean;
|
|
55
|
+
/** Responder a uma mensagem (id ou a mensagem). */
|
|
56
|
+
reply?: string | Message;
|
|
57
|
+
}
|
|
58
|
+
export type MessageContent = string | MessageOptions;
|
|
59
|
+
|
|
60
|
+
export class User {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly username: string | null;
|
|
63
|
+
readonly displayName: string;
|
|
64
|
+
readonly avatarURL: string | null;
|
|
65
|
+
readonly bot: boolean;
|
|
66
|
+
readonly flags: string[];
|
|
67
|
+
/** "<@id>" */
|
|
68
|
+
toString(): string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Channel {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly kind: "text" | "voice";
|
|
75
|
+
readonly categoryId: string | null;
|
|
76
|
+
readonly topic: string;
|
|
77
|
+
readonly group: Group;
|
|
78
|
+
readonly isText: boolean;
|
|
79
|
+
send(content: MessageContent): Promise<Message>;
|
|
80
|
+
sendTyping(): Promise<void>;
|
|
81
|
+
fetchMessages(options?: { limit?: number; after?: string }): Promise<Message[]>;
|
|
82
|
+
fetchMessage(id: string): Promise<Message>;
|
|
83
|
+
toString(): string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface GroupRole {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
color: string | null;
|
|
90
|
+
position: number;
|
|
91
|
+
mentionable: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface GroupMember {
|
|
95
|
+
id: string;
|
|
96
|
+
username: string;
|
|
97
|
+
displayName: string;
|
|
98
|
+
avatarUrl: string | null;
|
|
99
|
+
bot: boolean;
|
|
100
|
+
roleIds: string[];
|
|
101
|
+
owner: boolean;
|
|
102
|
+
online: boolean;
|
|
103
|
+
user: User;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type BotPermission =
|
|
107
|
+
| "administrator"
|
|
108
|
+
| "manageGroup"
|
|
109
|
+
| "manageChannels"
|
|
110
|
+
| "manageRoles"
|
|
111
|
+
| "kickMembers"
|
|
112
|
+
| "banMembers"
|
|
113
|
+
| "manageMessages"
|
|
114
|
+
| "manageReactions"
|
|
115
|
+
| "createInvites"
|
|
116
|
+
| "manageWebhooks";
|
|
117
|
+
|
|
118
|
+
export class Group {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly iconURL: string | null;
|
|
122
|
+
readonly ownerId: string | null;
|
|
123
|
+
readonly memberCount: number;
|
|
124
|
+
readonly roles: GroupRole[];
|
|
125
|
+
readonly channels: Map<string, Channel>;
|
|
126
|
+
/** O que o bot pode fazer neste grupo. */
|
|
127
|
+
readonly permissions: Partial<Record<BotPermission, boolean>>;
|
|
128
|
+
can(permission: BotPermission): boolean;
|
|
129
|
+
kick(userId: string): Promise<void>;
|
|
130
|
+
ban(userId: string, reason?: string): Promise<void>;
|
|
131
|
+
unban(userId: string): Promise<void>;
|
|
132
|
+
fetchBans(): Promise<{ userId: string; name: string; bannedAt: number; by: string; reason?: string }[]>;
|
|
133
|
+
addRole(userId: string, roleId: string): Promise<string[]>;
|
|
134
|
+
removeRole(userId: string, roleId: string): Promise<string[]>;
|
|
135
|
+
leave(): Promise<void>;
|
|
136
|
+
/** A sala pelo id ou pelo nome. */
|
|
137
|
+
channel(idOrName: string): Channel | null;
|
|
138
|
+
role(idOrName: string): GroupRole | null;
|
|
139
|
+
fetchMembers(): Promise<GroupMember[]>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface Reaction {
|
|
143
|
+
emoji: string;
|
|
144
|
+
users: string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class Message {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
readonly groupId: string;
|
|
150
|
+
readonly channelId: string;
|
|
151
|
+
/** O texto como está gravado: menções como <@id>. */
|
|
152
|
+
readonly content: string;
|
|
153
|
+
/** O texto com <@id> trocado por @nome. */
|
|
154
|
+
readonly cleanContent: string;
|
|
155
|
+
readonly embeds: EmbedData[];
|
|
156
|
+
readonly attachments: { url: string; name: string; size: number; type: string; kind: string }[];
|
|
157
|
+
readonly images: string[];
|
|
158
|
+
readonly reactions: Reaction[];
|
|
159
|
+
readonly replyTo: { id: string; from: string; fromName: string; text: string } | null;
|
|
160
|
+
readonly createdAt: Date;
|
|
161
|
+
readonly editedAt: Date | null;
|
|
162
|
+
readonly pinned: boolean;
|
|
163
|
+
readonly webhook: boolean;
|
|
164
|
+
readonly author: User;
|
|
165
|
+
readonly mentions: { users: string[]; roles: string[]; everyone: boolean; raw: string[] };
|
|
166
|
+
/** A mensagem menciona o bot (pelo id ou @everyone). */
|
|
167
|
+
readonly mentionsMe: boolean;
|
|
168
|
+
readonly group: Group | null;
|
|
169
|
+
readonly channel: Channel | null;
|
|
170
|
+
reply(content: MessageContent): Promise<Message>;
|
|
171
|
+
edit(content: MessageContent): Promise<Message>;
|
|
172
|
+
delete(): Promise<void>;
|
|
173
|
+
react(emoji: string): Promise<void>;
|
|
174
|
+
unreact(emoji: string): Promise<void>;
|
|
175
|
+
pin(): Promise<void>;
|
|
176
|
+
unpin(): Promise<void>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface CommandContext {
|
|
180
|
+
message: Message;
|
|
181
|
+
args: string[];
|
|
182
|
+
client: Client;
|
|
183
|
+
reply(content: MessageContent): Promise<Message>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface Command {
|
|
187
|
+
name: string;
|
|
188
|
+
description?: string;
|
|
189
|
+
aliases?: string[];
|
|
190
|
+
run(context: CommandContext): unknown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ClientEvents {
|
|
194
|
+
ready: [user: User];
|
|
195
|
+
messageCreate: [message: Message];
|
|
196
|
+
messageUpdate: [message: Message];
|
|
197
|
+
messageDelete: [info: { id: string; groupId: string; channelId: string }];
|
|
198
|
+
reactionUpdate: [info: { messageId: string; groupId: string; channelId: string; reactions: Reaction[] }];
|
|
199
|
+
typingStart: [info: { userId: string; name: string; groupId: string; channelId: string }];
|
|
200
|
+
groupJoin: [group: Group];
|
|
201
|
+
memberAdd: [info: { user: User; group: Group | null; groupId: string; via: "join" | "invite" | "profile" | null }];
|
|
202
|
+
memberRemove: [info: { userId: string; user: User | null; group: Group | null; groupId: string; reason: "left" | "kicked" | "banned" | null; by: string | null }];
|
|
203
|
+
groupLeave: [info: { id: string; reason: string | null }];
|
|
204
|
+
groupUpdate: [group: Group];
|
|
205
|
+
reconnecting: [attempt: number];
|
|
206
|
+
reconnected: [];
|
|
207
|
+
disconnect: [];
|
|
208
|
+
error: [error: Error];
|
|
209
|
+
commandError: [error: unknown, context: CommandContext];
|
|
210
|
+
raw: [payload: Record<string, unknown>];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export class Client extends EventEmitter {
|
|
214
|
+
constructor(options: ClientOptions);
|
|
215
|
+
readonly user: User | null;
|
|
216
|
+
readonly groups: Map<string, Group>;
|
|
217
|
+
readonly users: Map<string, User>;
|
|
218
|
+
readonly commands: Map<string, Command>;
|
|
219
|
+
readonly prefix: string;
|
|
220
|
+
readonly rest: REST;
|
|
221
|
+
login(): Promise<User>;
|
|
222
|
+
destroy(): void;
|
|
223
|
+
command(command: Command): this;
|
|
224
|
+
fetchGroup(id: string): Promise<Group>;
|
|
225
|
+
fetchUser(id: string): Promise<User>;
|
|
226
|
+
on<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
|
|
227
|
+
once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
|
|
228
|
+
off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => void): this;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export class REST {
|
|
232
|
+
constructor(options: { token: string; apiUrl: string; retries?: number });
|
|
233
|
+
request<T = any>(method: string, path: string, body?: unknown): Promise<T>;
|
|
234
|
+
get<T = any>(path: string): Promise<T>;
|
|
235
|
+
post<T = any>(path: string, body?: unknown): Promise<T>;
|
|
236
|
+
patch<T = any>(path: string, body?: unknown): Promise<T>;
|
|
237
|
+
put<T = any>(path: string, body?: unknown): Promise<T>;
|
|
238
|
+
delete<T = any>(path: string): Promise<T>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class FiveLiveAPIError extends Error {
|
|
242
|
+
readonly status: number;
|
|
243
|
+
readonly method: string;
|
|
244
|
+
readonly path: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** "<@id>" */
|
|
248
|
+
export function userMention(id: string): string;
|
|
249
|
+
/** "<#id>" */
|
|
250
|
+
export function channelMention(id: string): string;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// fivelive.js — SDK para bots do Five Live.
|
|
2
|
+
// Documentação: README.md e https://live.fivenetwork.dev/developers
|
|
3
|
+
|
|
4
|
+
export { Client } from "./client.js";
|
|
5
|
+
export { EmbedBuilder } from "./embed.js";
|
|
6
|
+
export { REST, FiveLiveAPIError } from "./rest.js";
|
|
7
|
+
export { Channel, Group, Message, User, channelMention, userMention } from "./structures.js";
|
package/src/rest.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// As rotas HTTP da API de bots (api/botApi.mjs), com o token no cabeçalho
|
|
2
|
+
// `Authorization: Bot <token>`. Trata o limite de envio (429) esperando o
|
|
3
|
+
// tempo que a API pede e tentando de novo.
|
|
4
|
+
|
|
5
|
+
export class FiveLiveAPIError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {number} status
|
|
9
|
+
* @param {string} method
|
|
10
|
+
* @param {string} path
|
|
11
|
+
*/
|
|
12
|
+
constructor(message, status, method, path) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "FiveLiveAPIError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.method = method;
|
|
17
|
+
this.path = path;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
|
|
23
|
+
export class REST {
|
|
24
|
+
/** @param {{ token: string, apiUrl: string, retries?: number }} options */
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.token = options.token;
|
|
27
|
+
this.apiUrl = options.apiUrl.replace(/\/$/, "");
|
|
28
|
+
this.retries = options.retries ?? 3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} method
|
|
33
|
+
* @param {string} path
|
|
34
|
+
* @param {unknown} [body]
|
|
35
|
+
*/
|
|
36
|
+
async request(method, path, body) {
|
|
37
|
+
for (let attempt = 0; ; attempt++) {
|
|
38
|
+
const res = await fetch(this.apiUrl + path, {
|
|
39
|
+
method,
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bot ${this.token}`,
|
|
42
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
43
|
+
"User-Agent": "fivelive.js (https://live.fivenetwork.dev/developers, 0.1.0)",
|
|
44
|
+
},
|
|
45
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
const data = await res.json().catch(() => ({}));
|
|
48
|
+
if (res.status === 429 && attempt < this.retries) {
|
|
49
|
+
const seconds = Number(data.retryAfter ?? res.headers.get("retry-after") ?? 5);
|
|
50
|
+
await sleep(Math.max(1, seconds) * 1000);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) throw new FiveLiveAPIError(data.error ?? `HTTP ${res.status}`, res.status, method, path);
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(path) {
|
|
59
|
+
return this.request("GET", path);
|
|
60
|
+
}
|
|
61
|
+
post(path, body) {
|
|
62
|
+
return this.request("POST", path, body ?? {});
|
|
63
|
+
}
|
|
64
|
+
patch(path, body) {
|
|
65
|
+
return this.request("PATCH", path, body ?? {});
|
|
66
|
+
}
|
|
67
|
+
put(path, body) {
|
|
68
|
+
return this.request("PUT", path, body ?? {});
|
|
69
|
+
}
|
|
70
|
+
delete(path) {
|
|
71
|
+
return this.request("DELETE", path);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const enc = encodeURIComponent;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Os objetos que o bot recebe: User, Group, Channel, Message. Cada um sabe
|
|
2
|
+
// agir sobre si mesmo (message.reply, channel.send, …) usando o client.
|
|
3
|
+
|
|
4
|
+
import { enc } from "./rest.js";
|
|
5
|
+
|
|
6
|
+
/** `<@id>` — como mencionar alguém no texto. */
|
|
7
|
+
export const userMention = (id) => `<@${id}>`;
|
|
8
|
+
/** `<#id>` — como apontar uma sala no texto. */
|
|
9
|
+
export const channelMention = (id) => `<#${id}>`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normaliza o que se manda: um texto, ou { content, embeds, mentions, reply }.
|
|
13
|
+
* @param {string | Record<string, any>} input
|
|
14
|
+
*/
|
|
15
|
+
export function messagePayload(input) {
|
|
16
|
+
const options = typeof input === "string" ? { content: input } : input ?? {};
|
|
17
|
+
const embeds = (options.embeds ?? []).map((embed) => (typeof embed?.toJSON === "function" ? embed.toJSON() : embed));
|
|
18
|
+
const roles = (options.roleMentions ?? []).map((id) => `@role:${id}`);
|
|
19
|
+
return {
|
|
20
|
+
text: options.content ?? "",
|
|
21
|
+
...(embeds.length ? { embeds } : {}),
|
|
22
|
+
...(options.mentions?.length || roles.length || options.everyone
|
|
23
|
+
? { mentions: [...(options.mentions ?? []), ...roles, ...(options.everyone ? ["@everyone"] : [])] }
|
|
24
|
+
: {}),
|
|
25
|
+
...(options.reply ? { replyTo: typeof options.reply === "string" ? options.reply : options.reply.id } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class User {
|
|
30
|
+
/** @param {import("./client.js").Client} client @param {Record<string, any>} data */
|
|
31
|
+
constructor(client, data) {
|
|
32
|
+
this.client = client;
|
|
33
|
+
this.id = data.id;
|
|
34
|
+
this.username = data.username ?? null;
|
|
35
|
+
this.displayName = data.displayName ?? data.name ?? data.username ?? "";
|
|
36
|
+
this.avatarURL = data.avatarUrl ?? null;
|
|
37
|
+
this.bot = data.bot === true;
|
|
38
|
+
this.flags = data.flags ?? [];
|
|
39
|
+
}
|
|
40
|
+
/** `<@id>`, para colocar no texto. */
|
|
41
|
+
toString() {
|
|
42
|
+
return userMention(this.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class Channel {
|
|
47
|
+
/** @param {import("./client.js").Client} client @param {Group} group @param {Record<string, any>} data */
|
|
48
|
+
constructor(client, group, data) {
|
|
49
|
+
this.client = client;
|
|
50
|
+
this.group = group;
|
|
51
|
+
this.id = data.id;
|
|
52
|
+
this.name = data.name;
|
|
53
|
+
/** "text" | "voice" */
|
|
54
|
+
this.kind = data.kind;
|
|
55
|
+
this.categoryId = data.categoryId ?? null;
|
|
56
|
+
this.topic = data.topic ?? "";
|
|
57
|
+
}
|
|
58
|
+
get isText() {
|
|
59
|
+
return this.kind === "text";
|
|
60
|
+
}
|
|
61
|
+
get path() {
|
|
62
|
+
return `/bot/groups/${enc(this.group.id)}/channels/${enc(this.id)}`;
|
|
63
|
+
}
|
|
64
|
+
/** Manda uma mensagem. @param {string | Record<string, any>} content */
|
|
65
|
+
async send(content) {
|
|
66
|
+
const data = await this.client.rest.post(`${this.path}/messages`, messagePayload(content));
|
|
67
|
+
return new Message(this.client, data.message, this.client.user);
|
|
68
|
+
}
|
|
69
|
+
/** "Digitando…" por alguns segundos. */
|
|
70
|
+
async sendTyping() {
|
|
71
|
+
await this.client.rest.post(`${this.path}/typing`, { typing: true });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* As últimas mensagens (a mais nova por último).
|
|
75
|
+
* @param {{ limit?: number, after?: string }} [options]
|
|
76
|
+
*/
|
|
77
|
+
async fetchMessages(options = {}) {
|
|
78
|
+
const query = new URLSearchParams();
|
|
79
|
+
if (options.limit) query.set("limit", String(options.limit));
|
|
80
|
+
if (options.after) query.set("after", options.after);
|
|
81
|
+
const data = await this.client.rest.get(`${this.path}/messages?${query}`);
|
|
82
|
+
return data.messages.map((message) => new Message(this.client, message));
|
|
83
|
+
}
|
|
84
|
+
/** Uma mensagem pelo id. */
|
|
85
|
+
async fetchMessage(id) {
|
|
86
|
+
const data = await this.client.rest.get(`${this.path}/messages/${enc(id)}`);
|
|
87
|
+
return new Message(this.client, data.message);
|
|
88
|
+
}
|
|
89
|
+
toString() {
|
|
90
|
+
return `<#${this.id}>`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class Group {
|
|
95
|
+
/** @param {import("./client.js").Client} client @param {Record<string, any>} data */
|
|
96
|
+
constructor(client, data) {
|
|
97
|
+
this.client = client;
|
|
98
|
+
this._patch(data);
|
|
99
|
+
}
|
|
100
|
+
/** @param {Record<string, any>} data */
|
|
101
|
+
_patch(data) {
|
|
102
|
+
this.id = data.id;
|
|
103
|
+
this.name = data.name;
|
|
104
|
+
this.iconURL = data.iconUrl ?? null;
|
|
105
|
+
this.ownerId = data.ownerId ?? null;
|
|
106
|
+
this.memberCount = data.memberCount ?? 0;
|
|
107
|
+
/** @type {{ id: string, name: string, color: string | null, position: number, mentionable: boolean }[]} */
|
|
108
|
+
this.roles = data.roles ?? [];
|
|
109
|
+
/** O que o bot pode fazer aqui (kickMembers, banMembers, manageRoles, manageMessages…). */
|
|
110
|
+
this.permissions = data.permissions ?? {};
|
|
111
|
+
/** @type {Map<string, Channel>} */
|
|
112
|
+
this.channels = new Map((data.channels ?? []).map((channel) => [channel.id, new Channel(this.client, this, channel)]));
|
|
113
|
+
}
|
|
114
|
+
/** A sala pelo id ou pelo nome. @param {string} idOrName */
|
|
115
|
+
channel(idOrName) {
|
|
116
|
+
return this.channels.get(idOrName) ?? [...this.channels.values()].find((channel) => channel.name === idOrName) ?? null;
|
|
117
|
+
}
|
|
118
|
+
/** Os membros, com cargos e quem está online. */
|
|
119
|
+
async fetchMembers() {
|
|
120
|
+
const data = await this.client.rest.get(`/bot/groups/${enc(this.id)}/members`);
|
|
121
|
+
return data.members.map((member) => ({ ...member, user: this.client._cacheUser(member) }));
|
|
122
|
+
}
|
|
123
|
+
get path() {
|
|
124
|
+
return `/bot/groups/${enc(this.id)}`;
|
|
125
|
+
}
|
|
126
|
+
/** Se o bot tem a permissão (administrator vale por todas). @param {string} key */
|
|
127
|
+
can(key) {
|
|
128
|
+
return this.permissions.administrator === true || this.permissions[key] === true;
|
|
129
|
+
}
|
|
130
|
+
/** Expulsa alguém (precisa de kickMembers e de um cargo acima do da pessoa). @param {string} userId */
|
|
131
|
+
async kick(userId) {
|
|
132
|
+
await this.client.rest.post(`${this.path}/members/${enc(userId)}/kick`, {});
|
|
133
|
+
}
|
|
134
|
+
/** Bane alguém, mesmo que não esteja no grupo (banMembers). @param {string} userId @param {string} [reason] */
|
|
135
|
+
async ban(userId, reason) {
|
|
136
|
+
await this.client.rest.put(`${this.path}/bans/${enc(userId)}`, reason ? { reason } : {});
|
|
137
|
+
}
|
|
138
|
+
/** Tira o banimento (banMembers). @param {string} userId */
|
|
139
|
+
async unban(userId) {
|
|
140
|
+
await this.client.rest.delete(`${this.path}/bans/${enc(userId)}`);
|
|
141
|
+
}
|
|
142
|
+
/** Quem está banido (banMembers). */
|
|
143
|
+
async fetchBans() {
|
|
144
|
+
const data = await this.client.rest.get(`${this.path}/bans`);
|
|
145
|
+
return data.bans;
|
|
146
|
+
}
|
|
147
|
+
/** Dá um cargo (manageRoles; só cargos abaixo do mais alto do bot). @param {string} userId @param {string} roleId */
|
|
148
|
+
async addRole(userId, roleId) {
|
|
149
|
+
const data = await this.client.rest.put(`${this.path}/members/${enc(userId)}/roles/${enc(roleId)}`);
|
|
150
|
+
return data.roleIds;
|
|
151
|
+
}
|
|
152
|
+
/** Tira um cargo (manageRoles). @param {string} userId @param {string} roleId */
|
|
153
|
+
async removeRole(userId, roleId) {
|
|
154
|
+
const data = await this.client.rest.delete(`${this.path}/members/${enc(userId)}/roles/${enc(roleId)}`);
|
|
155
|
+
return data.roleIds;
|
|
156
|
+
}
|
|
157
|
+
/** O bot sai do grupo. */
|
|
158
|
+
async leave() {
|
|
159
|
+
await this.client.rest.post(`${this.path}/leave`, {});
|
|
160
|
+
}
|
|
161
|
+
/** `<@&id>` não existe aqui: para mencionar um cargo, use { roleMentions: [id] } no envio. */
|
|
162
|
+
role(idOrName) {
|
|
163
|
+
return this.roles.find((role) => role.id === idOrName || role.name === idOrName) ?? null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class Message {
|
|
168
|
+
/**
|
|
169
|
+
* @param {import("./client.js").Client} client
|
|
170
|
+
* @param {Record<string, any>} data
|
|
171
|
+
* @param {User | Record<string, any>} [author]
|
|
172
|
+
*/
|
|
173
|
+
constructor(client, data, author) {
|
|
174
|
+
this.client = client;
|
|
175
|
+
this.id = data.id;
|
|
176
|
+
this.groupId = data.groupId;
|
|
177
|
+
this.channelId = data.channelId;
|
|
178
|
+
/** O texto como está gravado — menções como `<@id>`. */
|
|
179
|
+
this.content = data.text ?? "";
|
|
180
|
+
this.embeds = data.embeds ?? [];
|
|
181
|
+
this.attachments = data.attachments ?? [];
|
|
182
|
+
this.images = data.images ?? [];
|
|
183
|
+
this.reactions = data.reactions ?? [];
|
|
184
|
+
this.replyTo = data.replyTo ?? null;
|
|
185
|
+
this.createdAt = new Date(data.ts ?? Date.now());
|
|
186
|
+
this.editedAt = data.editedAt ? new Date(data.editedAt) : null;
|
|
187
|
+
this.pinned = Boolean(data.pinnedAt);
|
|
188
|
+
this.webhook = Boolean(data.webhook);
|
|
189
|
+
const mentions = data.mentions ?? [];
|
|
190
|
+
this.mentions = {
|
|
191
|
+
/** ids das pessoas mencionadas */
|
|
192
|
+
users: mentions.filter((entry) => !entry.startsWith("@")),
|
|
193
|
+
/** ids dos cargos mencionados */
|
|
194
|
+
roles: mentions.filter((entry) => entry.startsWith("@role:")).map((entry) => entry.slice(6)),
|
|
195
|
+
everyone: mentions.includes("@everyone"),
|
|
196
|
+
raw: mentions,
|
|
197
|
+
};
|
|
198
|
+
this.author =
|
|
199
|
+
author instanceof User
|
|
200
|
+
? author
|
|
201
|
+
: author
|
|
202
|
+
? client._cacheUser(author)
|
|
203
|
+
: client.users.get(data.from) ?? new User(client, { id: data.from, name: data.fromName });
|
|
204
|
+
}
|
|
205
|
+
get group() {
|
|
206
|
+
return this.client.groups.get(this.groupId) ?? null;
|
|
207
|
+
}
|
|
208
|
+
get channel() {
|
|
209
|
+
return this.group?.channels.get(this.channelId) ?? null;
|
|
210
|
+
}
|
|
211
|
+
/** O texto com `<@id>` trocado por `@nome` (quando o nome é conhecido). */
|
|
212
|
+
get cleanContent() {
|
|
213
|
+
return this.content.replace(/<@([A-Za-z0-9:_-]+)>/g, (whole, id) => {
|
|
214
|
+
const user = this.client.users.get(id);
|
|
215
|
+
return user ? `@${user.displayName}` : whole;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Se a mensagem menciona o bot (pelo id, cargo não conta). */
|
|
219
|
+
get mentionsMe() {
|
|
220
|
+
return this.mentions.users.includes(this.client.user?.id) || this.mentions.everyone;
|
|
221
|
+
}
|
|
222
|
+
get path() {
|
|
223
|
+
return `/bot/groups/${enc(this.groupId)}/channels/${enc(this.channelId)}/messages`;
|
|
224
|
+
}
|
|
225
|
+
/** Responde citando esta mensagem. @param {string | Record<string, any>} content */
|
|
226
|
+
async reply(content) {
|
|
227
|
+
const payload = messagePayload(content);
|
|
228
|
+
const data = await this.client.rest.post(this.path, { ...payload, replyTo: this.id });
|
|
229
|
+
return new Message(this.client, data.message, this.client.user);
|
|
230
|
+
}
|
|
231
|
+
/** Edita (só as mensagens do próprio bot). @param {string | Record<string, any>} content */
|
|
232
|
+
async edit(content) {
|
|
233
|
+
const payload = messagePayload(content);
|
|
234
|
+
const data = await this.client.rest.patch(`${this.path}/${enc(this.id)}`, { text: payload.text, ...(payload.embeds ? { embeds: payload.embeds } : {}) });
|
|
235
|
+
return new Message(this.client, data.message, this.author);
|
|
236
|
+
}
|
|
237
|
+
/** Fixa na sala (manageMessages). */
|
|
238
|
+
async pin() {
|
|
239
|
+
await this.client.rest.put(`${this.path}/${enc(this.id)}/pin`);
|
|
240
|
+
}
|
|
241
|
+
/** Desafixa (manageMessages). */
|
|
242
|
+
async unpin() {
|
|
243
|
+
await this.client.rest.delete(`${this.path}/${enc(this.id)}/pin`);
|
|
244
|
+
}
|
|
245
|
+
/** Apaga: as do próprio bot sempre; as dos outros, com manageMessages. */
|
|
246
|
+
async delete() {
|
|
247
|
+
await this.client.rest.delete(`${this.path}/${enc(this.id)}`);
|
|
248
|
+
}
|
|
249
|
+
/** Reage com um emoji. @param {string} emoji */
|
|
250
|
+
async react(emoji) {
|
|
251
|
+
await this.client.rest.put(`${this.path}/${enc(this.id)}/reactions/${enc(emoji)}`);
|
|
252
|
+
}
|
|
253
|
+
/** Tira a reação do bot. @param {string} emoji */
|
|
254
|
+
async unreact(emoji) {
|
|
255
|
+
await this.client.rest.delete(`${this.path}/${enc(this.id)}/reactions/${enc(emoji)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|