Pular para o conteúdo principal

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 */ }
Um listener de error não é opcional

Como 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. chat vira messageCreate, joined vira voiceJoin. Chegam na hora.
  • Diffs de snapshot. O servidor reenvia o state inteiro a cada mudança. O SDK compara com o snapshot anterior e emite channelCreate, memberUpdate, voiceStateUpdate e 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​

EventoArgumentosFrame
directMessageCreate[dm: DirectMessage]dm
directMessageUpdate[dm: DirectMessage]dm_edited
directMessageDelete[{ id, from, to }]dm_deleted
DM só entre amigos

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.

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

EventoArgumentos
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]