Eventos
Tudo que o bot faz começa num evento. O Client do concord.js é um emitter tipado: o nome do
evento determina os tipos dos argumentos, e o @On do nescord herda essa tipagem.
client.on('messageCreate', (message) => { /* message é Message */ });
@On('messageCreate')
onMessage(@Ctx() [message]: EventContext<'messageCreate'>) { /* idem */ }
error não é opcionalComo em qualquer EventEmitter do Node, erro emitido sem ouvinte derruba o processo. Registre
error antes de qualquer outra coisa.
De onde os eventos vêm
Vale entender a divisão, porque ela explica o atraso de alguns eventos:
- Frames diretos.
chatviramessageCreate,joinedviravoiceJoin. Chegam na hora. - Diffs de snapshot. O servidor reenvia o
stateinteiro a cada mudança. O SDK compara com o snapshot anterior e emitechannelCreate,memberUpdate,voiceStateUpdatee companhia. Chegam quando o próximo snapshot chega.
Ciclo de vida
ready
[client: Client] — depois do welcome e do primeiro state. É o primeiro ponto em que
client.user existe e os caches de servers, channels, users e membros estão cheios.
@Once('ready')
onReady(@Ctx() [client]: EventContext<'ready'>) {
console.log(`@${client.user!.username} em ${client.servers.size} servidor(es)`);
console.log(`SFU: ${client.sfuEnabled ? 'ligado' : 'malha P2P'}`);
}
resumed
[{ voiceResumed: boolean; roomId: ChannelId | null }] — reconectou. voiceResumed é true
quando o welcome devolveu a sala: a vaga na voz sobreviveu à carência de 20 segundos do
servidor.
@On('resumed')
onResumed(@Ctx() [{ voiceResumed, roomId }]: EventContext<'resumed'>) {
// Voltou na janela de 20 s: a sessão de mídia ainda vale, não precisa republicar.
if (voiceResumed) console.log(`voz preservada na sala ${roomId}`);
}
disconnect
[{ code: number; reason: string; replaced: boolean; willReconnect: boolean }] — o socket fechou.
@On('disconnect')
onDisconnect(@Ctx() [info]: EventContext<'disconnect'>) {
// `replaced` significa que outra sessão do mesmo usuário entrou. O SDK NÃO
// reconecta nesse caso: duas sessões reconectando ficariam se derrubando.
if (info.replaced) console.warn('outra sessão assumiu o token');
}
reconnecting
[{ attempt: number; delayMs: number; withinVoiceGrace: boolean }] — antes de cada tentativa.
Enquanto withinVoiceGrace é true, o backoff fica curto (até 2 s) de propósito: voltar dentro dos
20 s salva a vaga na sala e a sessão de mídia.
error e debug
error recebe [error: Error]: socket, JSON inválido, ou handler que rejeitou. debug recebe
[message: string]: rastro de handshake, backoff e fila do SFU.
Mensagens
messageCreate
[message: Message] — frame chat. Vale para canal de texto, de voz e tópico.
@On('messageCreate')
async onMessage(@Ctx() [message]: EventContext<'messageCreate'>) {
// O servidor reemite o `chat` para todos, inclusive o autor. Sem este
// guarda, o bot responde a si mesmo em laço.
if (message.fromMe) return;
if (message.content === 'ping') await message.reply('pong');
}
messageUpdate
[message: Message, previous: Message | null] — frame chat_edited. previous só vem se a
mensagem estava em cache.
messageDelete
[{ channelId, messageId, channel: Channel | null, message: Message | null }] — frame
chat_deleted. O servidor manda só o id; message vem do cache quando existia.
messageReactionUpdate
[message: Message, previous: Message | null] — frame chat_update. Hoje cobre reação e
fixar/desafixar: o servidor manda a mensagem inteira, não o delta da reação.
@On('messageReactionUpdate')
onReaction(@Ctx() [message, previous]: EventContext<'messageReactionUpdate'>) {
const antes = previous?.reactions.length ?? 0;
if (message.reactions.length > antes) console.log('alguém reagiu');
}
messageError
[error: MessageActionError] — frame message_error. A edição, remoção ou reação foi recusada.
Traz scope, target, messageId e action.
Mensagens diretas
| Evento | Argumentos | Frame |
|---|---|---|
directMessageCreate | [dm: DirectMessage] | dm |
directMessageUpdate | [dm: DirectMessage] | dm_edited |
directMessageDelete | [{ id, from, to }] | dm_deleted |
O servidor recusa mensagem direta para quem não é amigo. Não é uma configuração do bot: é a regra social do Concord. Se o bot precisa avisar alguém em privado, a pessoa tem de ter aceitado a amizade antes.
Servidores, canais e cargos
Todos vêm do diff de state.
| Evento | Argumentos | Vem de |
|---|---|---|
serverCreate | [server: Server] | servidor novo no snapshot |
serverUpdate | [server, previous | null] | qualquer campo do servidor mudou |
serverDelete | [server: Server] | saiu do snapshot: saí, fui expulso, banido, ou o servidor foi apagado |
channelCreate | [channel: Channel] | canal novo — inclui sala temporária nascendo |
channelUpdate | [channel, previous | null] | nome, tópico, slowmode, trancado, arquivado… |
channelDelete | [channel: Channel] | sala temporária some sozinha ao esvaziar |
roleCreate | [role: Role] | cargo novo |
roleUpdate | [role, previous | null] | nome, cor, posição, permissões |
roleDelete | [role: Role] | cargo removido |
memberAdd | [member: Member] | entrou no servidor |
memberRemove | [member: Member] | saiu do servidor |
memberUpdate | [member, previous | null] | cargos (member_roles) ou dados exibidos |
@On('memberAdd')
async boasVindas(@Ctx() [member]: EventContext<'memberAdd'>) {
const canal = member.server.textChannels().find((c) => c.name === 'boas-vindas');
// Menção no Concord é texto puro: `member.toString()` devolve "@usuario".
await canal?.send(`${member} chegou. Boas-vindas.`);
}
Presença e voz
presenceUpdate
[{ userId, presence, status, roomId }] — diff de state.online. presence vazia significa
online: os únicos outros valores são idle, dnd e invisible.
voiceStateUpdate
[state: VoiceState | null, previous: VoiceState | null] — diff da presença de voz do canal.
state nulo quer dizer que a pessoa saiu; previous nulo, que entrou. Também dispara quando
mic, câmera, tela ou surdo mudam.
@On('voiceStateUpdate')
onVoice(@Ctx() [state, previous]: EventContext<'voiceStateUpdate'>) {
if (!previous) console.log(`${state!.displayName} entrou em ${state!.channel?.name}`);
else if (!state) console.log(`${previous.displayName} saiu`);
else if (state.mic !== previous.mic) console.log(`mic: ${state.mic}`);
}
voiceJoin e voiceLeave
voiceJoin recebe [channel: VoiceChannel, raw: RawChannel] — frame joined: foi o próprio
bot que entrou. Só depois disto as rotas /api/sfu/* liberam. voiceLeave recebe
[{ reason: string }] — frame left: o bot saiu ou foi tirado.
signal
[{ from: UserId; data: unknown }] — sinalização WebRTC em malha. Só aparece quando
client.sfuEnabled é false, ou seja, quando o servidor está rodando sem SFU.
iceUpdate
[iceServers: readonly unknown[]] — frame ice: credenciais TURN renovadas. Valem para as
próximas conexões, não para as abertas.
Outros
musicUpdate
[state: MusicState] — frame music: a fila nativa do servidor, a do player de YouTube do
cliente web. Não é a fila do seu bot de música.
gatewayError
[error: GatewayError] — frames error e warn. O campo kind diz qual dos dois foi.
@On('gatewayError')
onGatewayError(@Ctx() [err]: EventContext<'gatewayError'>) {
console.warn(`[${err.kind}] ${err.code}: ${err.message}`);
}
info
[message: string] — frame info: aviso neutro. O cliente web mostra como toast.
raw
[payload: GatewayReceive] — todo frame recebido, cru e já tipado por type. É a saída de
emergência para o que o SDK ainda não embrulhou em estrutura: bans, audit, invites,
directory, discover, nudge, wink, profile, thread_created, pong.
@On('raw')
onRaw(@Ctx() [payload]: EventContext<'raw'>) {
if (payload.type === 'nudge') console.log(`${payload.name} chamou atenção`);
}
Tabela completa
| Evento | Argumentos |
|---|---|
ready | [client: Client] |
resumed | [{ voiceResumed, roomId }] |
messageCreate | [message: Message] |
messageUpdate | [message, previous | null] |
messageDelete | [{ channelId, messageId, channel, message }] |
messageReactionUpdate | [message, previous | null] |
directMessageCreate | [dm: DirectMessage] |
directMessageUpdate | [dm: DirectMessage] |
directMessageDelete | [{ id, from, to }] |
serverCreate | [server: Server] |
serverUpdate | [server, previous | null] |
serverDelete | [server: Server] |
memberAdd | [member: Member] |
memberRemove | [member: Member] |
memberUpdate | [member, previous | null] |
channelCreate | [channel: Channel] |
channelUpdate | [channel, previous | null] |
channelDelete | [channel: Channel] |
roleCreate | [role: Role] |
roleUpdate | [role, previous | null] |
roleDelete | [role: Role] |
voiceStateUpdate | [state | null, previous | null] |
presenceUpdate | [{ userId, presence, status, roomId }] |
voiceJoin | [channel: VoiceChannel, raw: RawChannel] |
voiceLeave | [{ reason: string }] |
signal | [{ from, data }] |
musicUpdate | [state: MusicState] |
gatewayError | [error: GatewayError] |
info | [message: string] |
messageError | [error: MessageActionError] |
iceUpdate | [iceServers] |
error | [error: Error] |
disconnect | [{ code, reason, replaced, willReconnect }] |
reconnecting | [{ attempt, delayMs, withinVoiceGrace }] |
raw | [payload: GatewayReceive] |
debug | [message: string] |