Pular para o conteúdo principal

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çãoPadrãoO que é
baseUrlCONCORD_URL ou https://app.concord.winBase HTTP
gatewayUrlbaseUrl com ws/wss + /wsSobrescreve a URL do gateway
tokenCONCORD_TOKENToken de sessão. Nunca no código
installTokenCONCORD_INSTALL_TOKENCredencial de instalação, repassada a rest.bots
reconnectver abaixoPolítica de reconexão
heartbeatInterval25000ping de keepalive em ms; 0 desliga
requestTimeout10000Timeout 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​

CampoPadrãoO que é
enabledtrueReconectar ao cair
initialDelay500ms da primeira espera
maxDelay30000Teto do backoff
maxAttemptsInfinityTentativas
jitter0.25Variação aleatória, para não sincronizar reconexões
voiceGraceMs20000Carência da vaga de voz no servidor
Por que voiceGraceMs existe

Quando 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​

PropriedadeTipoO que é
userClientUser | nullEu. Existe a partir do ready
statusConnectionStatusidle, connecting, authenticating, ready, reconnecting, closed
readybooleanJá ficou pronto
sfuEnabledbooleanwelcome.sfu. false = malha P2P, /api/sfu dá 404
resumedRoomIdChannelId | nullVaga preservada numa reconexão
iceServersreadonly unknown[]Credenciais ICE/TURN mais recentes
pingnumberms entre o último ping e o pong. -1 antes do primeiro
uptimenumber | nullms desde o ready
restRESTCliente HTTP
serversServerManager
channelsChannelManager
usersUserManager
voiceVoiceManager

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 rede

Os ú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"
Qual mutação dá para confirmar, e qual não

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.

Por que send() espera

O 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 instalado

Quando 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"
Menção é texto

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​

ClasseQuando
ConcordErrorBase de todas, com code
GatewayErrorFrame error ou warn. kind diz qual
MessageActionErrorFrame message_error: ação numa mensagem recusada
RestErrorHTTP não-2xx. Traz status, path, body
TimeoutErrorEstourou o tempo esperando confirmação
SessionReplacedErrorVeio replaced: outra sessão assumiu o token
VoiceErrorFalha 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';
ConstanteValorO que é
Limits.MAX_FRAME_BYTES262144256 KB por frame
Limits.MESSAGES_PER_SECOND60Por usuário, no gateway
Limits.MAX_MESSAGE_CHARS2000Por mensagem
Limits.MAX_STATUS_CHARS128Do recado de status
Limits.AUTH_TIMEOUT_MS10000Prazo do primeiro frame
Limits.CHAT_KEEP100Mensagens que o hub guarda em memória por canal
RECONNECT_GRACE_MS20000Carência da vaga de voz

Fora dessas, o servidor também limita 30 conexões por minuto por IP.