ctx.send

API de envio. Todo método retorna um MessageHandle (thenable) com ações encadeáveis.

// chat atual (runtime only)
await ctx.send.text("Olá!");
await ctx.send.text("https://exemplo.com", { linkPreview: false });
await ctx.send.text("Olá @user", { mentions: ["5511999999999@c.us"] });

await ctx.send.image("/tmp/foto.jpg", "legenda");
await ctx.send.image("/tmp/foto.jpg", "secreta", { viewOnce: true });

await ctx.send.video("/tmp/video.mp4", "legenda");
await ctx.send.audio("/tmp/audio.ogg");                      // como voz (padrão)
await ctx.send.audio("/tmp/audio.ogg", { asVoice: false });   // como áudio normal
await ctx.send.sticker("/tmp/sticker.webp");
await ctx.send.sticker(bufferGeradoEmMemoria);
await ctx.send.file("/tmp/relatorio.pdf", "relatorio-2025.pdf");
await ctx.send.poll("Qual sabor?", ["Morango", "Chocolate"], { allowMultipleAnswers: true });

// qualquer chat, por ID (setup + runtime)
await ctx.send.to(chatId).text("notificação");

Opções por método

Método Opções
text { linkPreview?, mentions?: string[] }
image / video { viewOnce? }
audio { asVoice? (padrão true), viewOnce? }
poll { allowMultipleAnswers? }
sticker / file sem opções extra

Ações pós-envio (chaining)

await ctx.send.text("Pronto!").react("✅");

const handle = await ctx.send.image("/tmp/x.jpg"); // handle = Message enviada
await handle.delete();

delete(forEveryone? = true) e react(emoji) retornam Promise. Hoje, delete(false) ("apagar só pra mim") não faz nada — só delete()/delete(true) (apagar pra todos) realmente tem efeito.

pin(duration?) também existe na interface, mas atualmente não tem efeito — o Baileys (a biblioteca por trás da conexão com o WhatsApp) ainda não suporta fixar mensagens, então chamar .pin() só registra um aviso no log e não faz nada. Deixamos o método aí pra quando isso mudar.

send vs reply: ctx.send.* manda sem citar nada. ctx.msg.reply.* cita a mensagem que disparou o handler — prefira reply em grupos.

ctx.send.to() dentro de um handler não aplica cooldown/jitter do guardOptions do seu plugin (só o limite global de envios continua valendo) — é assim porque .to() normalmente serve pra notificar outro chat, não o atual. Já em setup(), ctx.send.to() aplica cooldown/jitter normalmente (os padrões, true/true, já que não há guardOptions de chat nenhum nesse ponto).


ctx.msg

Contexto da mensagem que disparou o handler (runtime only).

ctx.msg.body;         // string — texto completo
ctx.msg.type;         // "chat" | "image" | "video" | "audio" | ...
ctx.msg.fromMe;       // true se enviada pelo próprio bot
ctx.msg.sender;       // ID de quem enviou — ex: "5511999999999@c.us"
ctx.msg.senderName;   // nome de exibição
ctx.msg.command;      // primeira palavra, sem prefixo, minúscula
ctx.msg.args;         // string[] — palavras após o comando
ctx.msg.hasMedia;     // boolean
ctx.msg.isGif;        // boolean
ctx.msg.hasReply;     // true se é resposta a outra msg
ctx.msg.hasPrefix;    // true se começa com CMD_PREFIX

Formato de ID: o ManyBot normaliza conversas privadas pro sufixo @c.us (o formato que o ManyBot já usava antes de migrar pro Baileys) em tudo que expõe a plugins — ctx.msg.sender, ctx.chat.id, objetos de contato, CHATS no manybot.toml, etc. Grupos continuam com @g.us normalmente. Use sempre @c.us ao montar ou comparar um ID de conversa privada manualmente — um valor com @s.whatsapp.net (o formato nativo do Baileys) não vai bater numa comparação === com ctx.msg.sender.

Detectando comandos

// Mensagem: "!ping"  (CMD_PREFIX = "!")
if (ctx.msg.is("ping")) {
  await ctx.send.text("pong!");
}

Lendo argumentos

// Mensagem: "!video https://youtube.com/watch?v=..."
const url = ctx.msg.args[0];
if (!url) {
  await ctx.msg.reply.text("Informe uma URL.");
  return;
}

Respondendo com quote

ctx.msg.reply é um sender completo — mesmos métodos de ctx.send, mas citando a mensagem original:

await ctx.msg.reply.text("Aqui está sua resposta!");
await ctx.msg.reply.image("/tmp/foto.jpg", "Sua imagem.");

Baixando mídia

if (ctx.msg.hasMedia) {
  const media = await ctx.msg.downloadMedia(); // { mimetype, data(base64) } | null
  if (!media) return void await ctx.msg.reply.text("Não consegui baixar.");
  const buf = Buffer.from(media.data, "base64");
}

Mensagem citada

if (ctx.msg.hasReply) {
  const quoted = await ctx.msg.getReply(); // objeto igual ao ctx.msg | null
  console.log(quoted?.senderName, quoted?.hasMedia, quoted?.body);
}

getReply() devolve um objeto na mesma shape do ctx.msg (body, type, sender, senderName, hasMedia, isGif, downloadMedia(), hasPrefix, command, args, reply, etc.) — não é o formato bruto do Baileys. Isso quer dizer que dá sim pra usar quoted.hasMedia/quoted.downloadMedia()/quoted.reply.text(...) direto:

if (ctx.msg.hasReply) {
  const quoted = await ctx.msg.getReply();
  if (quoted?.hasMedia) {
    const media = await quoted.downloadMedia(); // baixa a mídia da mensagem citada
  }
}

Não existe campo quoted.pushName nem quoted.message — use quoted.senderName (igual ao ctx.msg.senderName) e, se precisar da estrutura crua do Baileys por algum motivo específico, não tem atalho público pra isso hoje.

getReply() não faz nenhuma chamada de rede — ele só reprocessa dados que a mensagem atual já trouxe consigo (a citação vem embutida em contextInfo). Não tem delay associado a ele.

quoted.senderName sempre cai pro número de telefone — o protocolo de citação do WhatsApp (contextInfo) não carrega o pushName de quem mandou a mensagem original, só o JID. Isso é diferente do ctx.contacts.get()/ctx.msg.getContact(), que de fato aprende e guarda o nome real ao vivo (veja a nota em ctx.contacts) — mas getReply() não passa por esse caminho, então não se beneficia disso.

Contato do remetente

ctx.msg.getContact() resolve @lid automaticamente — prefira isso a ctx.contacts.get(ctx.msg.sender) dentro de um handler. Mesma shape de objeto de contato, incluindo o mesmo comportamento de devolver null pra alguém que o bot ainda não "viu" mandar mensagem (veja a nota em ctx.contacts) — trate esse caso antes de usar contact.mention ou qualquer outro campo:

const contact = await ctx.msg.getContact();
if (!contact) return; // ainda não temos dados desse contato
await ctx.msg.reply.text(`oi ${contact.mention.text}`, contact.mention);

Evitando loops

O bot recebe as próprias mensagens também:

export default async function (ctx) {
  if (ctx.msg.fromMe) return;
  // ...
}