Client
O Client é o gateway, os caches e a porta para o REST. Tudo no concord.js passa por ele.
import { Client } from 'concord.js';
const client = new Client({ token: process.env.CONCORD_TOKEN });
await client.login();
Opções
new Client({
baseUrl: 'http://localhost:3000',
token: process.env.CONCORD_TOKEN,
presence: '',
status: 'ouvindo /play',
});
| Opção | Padrão | O que é |
|---|---|---|
baseUrl | CONCORD_URL ou https://app.concord.win | Base HTTP |
gatewayUrl | baseUrl com ws/wss + /ws | Sobrescreve a URL do gateway |
token | CONCORD_TOKEN | Token de sessão. Nunca no código |
installToken | CONCORD_INSTALL_TOKEN | Credencial de instalação, repassada a rest.bots |
reconnect | ver abaixo | Política de reconexão |
heartbeatInterval | 25000 | ping de keepalive em ms; 0 desliga |
requestTimeout | 10000 | Timeout padrão de operação que espera confirmação |
presence | — | Anunciada depois do ready |
status | — | Recado anunciado depois do ready |
rest | — | Partial<RestOptions> |
logger | — | debug / info / warn / error |
reconnect
| Campo | Padrão | O que é |
|---|---|---|
enabled | true | Reconectar ao cair |
initialDelay | 500 | ms da primeira espera |
maxDelay | 30000 | Teto do backoff |
maxAttempts | Infinity | Tentativas |
jitter | 0.25 | Variação aleatória, para não sincronizar reconexões |
voiceGraceMs | 20000 | Carência da vaga de voz no servidor |
voiceGraceMs existeQuando o WebSocket cai, o servidor segura a vaga do bot na sala de voz por 20 segundos
(RECONNECT_GRACE). Enquanto a queda cabe nessa janela, o SDK mantém o backoff curto — até 2 s —
de propósito: voltar dentro dela preserva a vaga e a sessão de mídia. Passada a janela, o
backoff cresce normalmente.
Propriedades
| Propriedade | Tipo | O que é |
|---|---|---|
user | ClientUser | null | Eu. Existe a partir do ready |
status | ConnectionStatus | idle, connecting, authenticating, ready, reconnecting, closed |
ready | boolean | Já ficou pronto |
sfuEnabled | boolean | welcome.sfu. false = malha P2P, /api/sfu dá 404 |
resumedRoomId | ChannelId | null | Vaga preservada numa reconexão |
iceServers | readonly unknown[] | Credenciais ICE/TURN mais recentes |
ping | number | ms entre o último ping e o pong. -1 antes do primeiro |
uptime | number | null | ms desde o ready |
rest | REST | Cliente HTTP |
servers | ServerManager | |
channels | ChannelManager | |
users | UserManager | |
voice | VoiceManager |
Métodos
login(token?)
Abre o gateway e espera welcome + o primeiro state. Resolve com o ClientUser.
const eu = await client.login();
console.log(`@${eu.username}`);
O token vai no cabeçalho Authorization: Bearer do handshake e repetido no primeiro frame. O
servidor prefere o cabeçalho; o frame é o caminho que funciona quando não há como definir cabeçalho.
destroy()
Fecha o socket e cancela a reconexão. Idempotente.
send(payload)
Manda um frame cru. Tudo no SDK passa por aqui. Lança se o socket não está aberto.
client.send({ type: 'presence', presence: 'dnd' });
request(payload, match, options?)
Manda e espera o primeiro frame que casar com o predicado. Rejeita com TimeoutError, ou com
GatewayError se vier error/warn.
const bans = await client.request(
{ type: 'bans', server: 42 },
(p): p is Extract<GatewayReceive, { type: 'bans' }> => p.type === 'bans' && p.server === 42,
);
waitFor(type, options?)
Espera um frame sem mandar nada. Útil para o próximo state.
await client.waitFor('state', { timeoutMs: 5000 });
Presença, status e diretório
client.setPresence('idle'); // '' | 'idle' | 'dnd' | 'invisible'
client.setStatus('em manutenção');
const pessoas = await client.fetchDirectory(); // todo mundo cadastrado, do meu ponto de vista
const publicos = await client.fetchDiscover(); // servidores públicos
'online' não existe na redeOs únicos valores aceitos são '', 'idle', 'dnd' e 'invisible'. String vazia é o estado
online. Qualquer outro valor é normalizado para vazio pelo servidor — então mandar 'online'
funciona por acidente, e é melhor não depender disso.
Mensagem direta
await client.sendDirectMessage(userId, 'oi');
const conversa = await client.fetchDirectMessages(userId);
DM só funciona entre amigos: o servidor recusa o resto.
Managers
Todos herdam de CachedManager, que é iterável e tem get, has, find, filter, map,
first, toArray e size. O cache fica cheio desde o ready.
client.servers
client.servers.get(42);
client.servers.find((s) => s.name === 'Concord');
await client.servers.resync(); // espera o próximo snapshot `state`
await client.servers.join('CODIGO'); // entra por convite
await client.servers.joinPublic(7);
await client.servers.create({ name: 'Novo', channels: [{ kind: 'text', name: 'geral' }] });
client.channels
client.channels.text(); // Collection<ChannelId, TextChannel>
client.channels.voice();
client.channels.threads();
client.channels.byName('geral', 42); // sem o "#", opcionalmente dentro de um servidor
client.users
client.users.get(7);
client.users.byUsername('tocadiscos'); // pelo @
client.voice
client.voice.connection; // VoiceConnection | null
client.voice.channelId;
const conexao = await client.voice.join(canalId, { selfDeaf: true });
await client.voice.leave();
client.voice.states(); // por canal, por usuário
Uma conexão por usuário: o servidor mantém uma sala por sessão.
Estruturas
Channel
Base de TextChannel, VoiceChannel e ThreadChannel.
canal.id; canal.serverId; canal.kind; canal.name; canal.emoji; canal.position;
canal.topic; canal.visibility; canal.locked; canal.temp; canal.ownerId; canal.lastMessageId;
canal.server; canal.permissions; canal.messages;
canal.isText(); canal.isVoice(); canal.isThread(); // estreitam o tipo
const msg = await canal.send('olá');
canal.sendRaw('olá'); // sem esperar confirmação
const { messages, more } = await canal.fetchMessages({ before: 120 });
const fixadas = await canal.fetchPins();
canal.edit({ topic: 'só avisos' }); // void: o servidor não confirma edição
await canal.delete(); // espera o snapshot sem o canal
`${canal}`; // "#nome"
state é o snapshot inteiro, e o servidor o reenvia para todo mundo a cada mudança qualquer —
inclusive quando um terceiro liga o microfone (broadcast, em hub.rs). Então "esperar o próximo
state" não diz nada sobre a sua operação.
Por isso o SDK separa as duas coisas: o que tem resultado observável no snapshot (createChannel,
createRole, createInvite, createRoom, delete, leave, kick, ban, setRoles) devolve
Promise e só resolve quando a mudança aparece; o que não tem (edit de servidor, canal e cargo,
setPassword) é void, fire-and-forget. Nesses últimos, a recusa por permissão chega como frame
error — ouça o evento gatewayError.
send() esperaO servidor não confirma envio com um id: ele reemite o frame chat para todos, inclusive o
autor. Para devolver um Message, o SDK espera o próximo chat desse canal com
msg.from === client.user.id e o mesmo texto, com timeout. Se estourar, lança TimeoutError — e
sendRaw() existe justamente para quando você não quer essa espera. O mesmo vale para
Message.edit e Message.delete, que casam com chat_edited e chat_deleted e são rejeitados por
message_error.
Canal de texto, de voz e tópico todos aceitam chat: dá para mandar mensagem num canal de voz.
TextChannel, VoiceChannel, ThreadChannel
// TextChannel
texto.slowmode;
const topico = await texto.createThread({ name: 'thread do bug', message: msgId });
// VoiceChannel
voz.userLimit; voz.bitrate; voz.video; voz.full;
voz.voiceStates; // Collection<UserId, VoiceState>
const conexao = await voz.join();
// `rename` e `setPassword` agem na sala TEMPORÁRIA em que o bot está agora — `ClientMsg::Rename` e
// `ClientMsg::SetPassword` não têm campo de canal, o servidor resolve o alvo por `my_room` e só
// aceita se a sala for temporária e o bot for o dono dela. Chamar num canal que não é o atual
// lança `VoiceError('wrong_room')`, justamente para não renomear outra sala sem aviso. Quando o
// servidor recusa, ele não responde nada: `rename` estoura `TimeoutError` e `setPassword`, que é
// fire-and-forget, não tem como avisar.
await voz.rename('sala do rock'); // Promise<void>: espera o snapshot com o nome novo
voz.setPassword('segredo'); // void: o servidor não confirma
// ThreadChannel
topico.parentId; topico.rootMessageId; topico.archived; topico.parent;
topico.setArchived(true); // void: `channel_edit` não é confirmado
Message
msg.id; msg.channelId; msg.authorId; msg.authorName; msg.content;
msg.createdAt; msg.editedAt; msg.replyToId; msg.reactions; msg.attachment;
msg.pinned; msg.threadId; msg.bot;
msg.channel; msg.server; msg.author; msg.member; msg.fromMe;
await msg.reply('respondendo');
await msg.edit('corrigido');
await msg.delete();
await msg.react('👍'); // alterna a reação
await msg.pin(true);
const topico = await msg.startThread('discussão');
const citada = await msg.fetchReference();
authorId em mensagem de bot instaladoQuando a mensagem veio de um bot instalado via REST, msg.bot está preenchido e msg.authorId é o
dono da instalação, não o bot. Para filtrar autoria de bot, teste msg.bot != null.
Server
server.id; server.name; server.ownerId; server.description;
server.community; server.public; server.tempRooms; server.verification;
server.category; server.rulesChannelId; server.createdAt; server.invites;
server.channels; server.roles; server.members;
server.everyonePermissions; server.myPermissions;
server.owner; server.me;
server.member(7);
server.permissionsFor(7);
server.textChannels(); server.voiceChannels(); server.iconURL();
server.edit({ description: 'voz, câmera e tela' }); // void: `server_edit` não é confirmado
await server.createChannel({ kind: 'voice', name: 'estúdio' });
await server.createRole('DJ');
const convite = await server.createInvite({ maxUses: 5, expires: 86400 });
await server.revokeInvite(convite.code);
const sala = await server.createRoom(); // sala temporária: quem cria é dono e já entra
await server.kick(7);
await server.ban(7, 'spam');
server.unban(7); // void: a lista de banidos não vem no snapshot
const bans = await server.fetchBans();
const log = await server.fetchAuditLog();
Member
member.id; member.user; member.displayName; member.roles; member.server;
member.permissions; member.isOwner; member.presence; member.status;
member.voiceState; member.voiceChannel;
member.hasPermission(PermissionFlags.BAN);
await member.setRoles([3, 5]); // lista COMPLETA; espera o snapshot com ela
await member.addRole(7);
await member.removeRole(7);
await member.kick();
await member.ban('motivo');
await member.disconnect(); // expulsa do canal de voz
await member.send('oi'); // DM: só entre amigos
`${member}`; // "@usuario"
User
user.id; user.displayName; user.username; user.avatarVersion;
user.bio; user.color; user.bannerRaw;
user.avatarURL();
`${user}`; // "@usuario"
No Concord menção é texto puro: @usuario, @everyone, @todos, @aqui, @here. Não existe
<@id>. É por isso que toString() devolve @username.
Attachment e Reaction
anexo.id; anexo.key; anexo.name; anexo.contentType; anexo.size;
anexo.url; // /file/{id}/{key} — exige Bearer, não é público
const bytes = await anexo.download();
reacao.emoji; reacao.userIds; reacao.count; reacao.me;
REST
// Anexo: faz o upload e manda a referência na mensagem.
const arquivo = await client.rest.upload({ data: bytes, name: 'capa.png', contentType: 'image/png' });
await canal.send({ text: 'olha', file: arquivo });
client.rest.fileURL(arquivo);
client.rest.avatarURL(user.raw);
client.rest.serverIconURL(server.raw);
await client.rest.inviteInfo('CODIGO');
// Webhook de canal.
await client.rest.webhooks.execute(id, token, { content: 'build verde', username: 'CI' });
// Comandos da aplicação: o que o seletor do chat mostra ao digitar /. Token de bot.
await client.rest.applications.setCommands([{ name: 'toca', description: 'Toca uma música' }]);
await client.rest.applications.getCommands();
// Credencial de instalação (Authorization: Bot). Não serve no gateway nem no SFU.
await client.rest.bots.me();
await client.rest.bots.sendMessage({ channel: 101, text: 'oi', requestId: 'aviso-42' });
Há também client.rest.request/get/post/put/delete para rota que o SDK não embrulhou. O parâmetro
auth escolhe a credencial: 'bearer' (padrão), 'bot' ou 'none'.
Erros
| Classe | Quando |
|---|---|
ConcordError | Base de todas, com code |
GatewayError | Frame error ou warn. kind diz qual |
MessageActionError | Frame message_error: ação numa mensagem recusada |
RestError | HTTP não-2xx. Traz status, path, body |
TimeoutError | Estourou o tempo esperando confirmação |
SessionReplacedError | Veio replaced: outra sessão assumiu o token |
VoiceError | Falha de voz |
Limites do servidor
Expostos como constantes para o SDK falhar cedo em vez de levar derrubada:
import { Limits, RECONNECT_GRACE_MS, version } from 'concord.js';
| Constante | Valor | O que é |
|---|---|---|
Limits.MAX_FRAME_BYTES | 262144 | 256 KB por frame |
Limits.MESSAGES_PER_SECOND | 60 | Por usuário, no gateway |
Limits.MAX_MESSAGE_CHARS | 2000 | Por mensagem |
Limits.MAX_STATUS_CHARS | 128 | Do recado de status |
Limits.AUTH_TIMEOUT_MS | 10000 | Prazo do primeiro frame |
Limits.CHAT_KEEP | 100 | Mensagens que o hub guarda em memória por canal |
RECONNECT_GRACE_MS | 20000 | Carência da vaga de voz |
Fora dessas, o servidor também limita 30 conexões por minuto por IP.