Skip to content

Biblioteca para Rust ​

zynko é a biblioteca oficial para bots do Zynko em Rust. Ela conecta no gateway e se reconecta sozinha, mantém servidores, canais e membros atualizados, transforma os eventos em chamadas de um EventHandler e cobre a API. Com a feature voice, o bot entra em salas, toca áudio e ouve quem fala.

Usa tokio e o Rust estável (testada com o 1.98).

Instalação ​

sh
cargo add zynko
cargo add tokio --features full
# para voz, em vez da primeira linha:
cargo add zynko --features voice

Para tocar arquivos e links, o bot usa o ffmpeg, que precisa estar instalado na máquina (ou indicado em FFMPEG_PATH).

A voz usa o SDK oficial do LiveKit, que baixa uma libwebrtc pronta na primeira compilação (uns 500 MB descompactados, alguns minutos). Sem a feature voice, nada disso entra.

Voz no Windows

A libwebrtc do LiveKit usa o runtime estático do C. No Windows, crie .cargo/config.toml no seu projeto com:

toml
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]

Sem isso, a compilação para no link com o erro LNK2038 (RuntimeLibrary).

Primeiro bot ​

rust
use zynko::{async_trait, Client, EventHandler, HandlerResult, Intents, Message, User};

struct Bot;

#[async_trait]
impl EventHandler for Bot {
    async fn ready(&self, ctx: Client, me: User) -> HandlerResult {
        println!("{} conectado em {} servidores", me.display_name, ctx.cache().guilds.len());
        Ok(())
    }

    async fn message_create(&self, _ctx: Client, msg: Message) -> HandlerResult {
        if msg.mine() {
            return Ok(());
        }
        if msg.content == "!ping" {
            msg.reply("pong").await?;
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> zynko::Result<()> {
    let client = Client::builder(std::env::var("ZYNKO_TOKEN").expect("ZYNKO_TOKEN"))
        .intents(Intents::GUILDS | Intents::MESSAGES)
        .event_handler(Bot)
        .build()?;
    client.start().await
}
sh
ZYNKO_TOKEN=bot_... cargo run

start() conecta e roda até destroy() (ou até o token ser recusado). Para fazer algo depois de conectar, use login(), que devolve a conta do bot quando o READY chega, e depois closed(), que espera o cliente parar.

O cliente ​

rust
Client::builder(token)          // o token do bot (dev.zynko.org)
    .intents(intents)           // o que receber; padrão: GUILDS | MESSAGES
    .api("https://...")         // outro servidor; padrão https://zynko.org/api
    .event_handler(Bot)         // quem trata os eventos
    .build()?
client.login().awaitConecta. Devolve a conta do bot quando o READY chega.
client.start().awaitlogin e depois espera até o cliente parar.
client.destroy().awaitSai da voz e fecha a conexão.
client.user()A conta do bot.
client.guild(id), client.channel(id)Um servidor ou canal, como estão agora.
client.cache()Tudo o que o cliente sabe: guilds, channels, users, voice_states.
client.rest()A API inteira (veja API).

client.cache() devolve uma trava de leitura com HashMaps: guilds (cada servidor com channels, members, roles, emojis e online), channels (todos os canais que o bot vê, DMs incluídas), users (as contas que o bot já viu) e voice_states (quem está em voz, por id da conta; precisa de VOICE_STATES). Leia e solte: não segure a trava através de um .await.

rust
let nomes: Vec<String> = client.cache().guilds.values().map(|g| g.name.clone()).collect();

Os mapas se atualizam sozinhos com os eventos. Se a conexão cair, o cliente reconecta (esperando 1 s, 2 s, 4 s... até 30 s) e reconstrói tudo com o READY novo. O Client é barato de clonar: todos os clones são o mesmo bot.

Intents ​

rust
use zynko::Intents;
Intents::GUILDS | Intents::MESSAGES | Intents::VOICE_STATES

GUILDS, GUILD_MEMBERS, PRESENCES, MESSAGES, REACTIONS, TYPING, VOICE_STATES e ALL. Peça só o que o bot usa. Detalhes em Gateway.

Eventos ​

Implemente no seu EventHandler só os métodos que o bot usa; os outros não fazem nada. Todos recebem o Client (ctx) primeiro.

métodoargumentosquando
readyUserConectou (de novo).
message_createMessageMensagem nova.
message_updateMessageMensagem editada (ou prévia de link pronta). Só vêm os campos que mudaram.
message_deleteMessageDeleteEvent { id, channel_id }Mensagem apagada.
reaction_add, reaction_removeReactionEvent { message_id, channel_id, user_id, emoji }Reação.
typing_startTypingEvent { channel_id, user_id, timestamp }Alguém digitando.
guild_createCachedGuildBot adicionado a um servidor (e cada servidor do READY).
guild_updateCachedGuildNome, ícone ou dono mudou.
guild_deleteidBot saiu ou o servidor foi apagado.
channel_create, channel_deleteChannelCanal novo ou apagado (uma DM nova é um canal novo).
channel_updateChannel, anteriorCanal mudou.
member_addMemberAlguém entrou num servidor.
member_updateMember, anteriorApelido ou cargos mudaram.
member_removeMemberRemoveEvent { guild_id, user_id }Alguém saiu.
presence_updatePresenceEvent { user_id, status }Ficou online ou offline.
voice_state_updateVoiceState, anteriorAlguém entrou, saiu ou mudou na voz.
interactionInteractionAlguém usou um comando do bot ou clicou num botão dele.
rawtipo, dados (serde_json::Value)Todo evento do gateway, como chegou.
reconnectingReconnectInfo { code, reason, wait }A conexão caiu e vai voltar.
errorArc<Error>Erro em outro método, ou o token foi recusado. Por padrão, vai para o stderr.

Cada chamada roda na sua própria tarefa. Os métodos devolvem HandlerResult, então ? funciona com qualquer erro: um Err (ou um panic!) vira uma chamada a error, sem derrubar o bot.

Também dá para receber os eventos num laço, sem EventHandler, com client.subscribe() e o enum Event. E client.wait_for(tempo, filtro) espera o próximo evento que passar no filtro:

rust
use std::time::Duration;

msg.reply("Qual o seu nome?").await?;
let autor = msg.author.id.clone();
let resposta = ctx
    .wait_for_message(Duration::from_secs(30), |m| m.author.id == autor)
    .await?;

Se o tempo acabar, vem Err(Error::Timeout(..)).

Mensagens ​

message_create e message_update entregam um Message:

id, channel_id, author, content, timestamp, attachments, reactions, embeds, reply_toOs dados da mensagem.
guild_id()O servidor, ou None numa DM.
channel()O Channel, se o bot já o viu.
member()O autor como membro do servidor (apelido, cargos).
mine()Se foi o próprio bot que mandou.
reply(texto ou SendOptions)Responde no mesmo canal, como resposta a ela.
react(emoji), unreact(emoji)Reage ou tira a reação.
edit(texto ou EditOptions), delete()O bot edita as próprias mensagens; apaga as próprias ou, com Gerenciar mensagens, as dos outros.

Para mandar em qualquer canal, ou com arquivos:

rust
use zynko::{FileInput, SendOptions};

client.send(&channel_id, "Olá!").await?;
client
    .send(
        &channel_id,
        SendOptions::new("O relatório de hoje").file(FileInput::from_path("relatorio.pdf").await?),
    )
    .await?;
client.send_dm(&user_id, "Oi! Isso é uma mensagem direta.").await?;

Onde a biblioteca pede impl Into<SendOptions>, um &str ou String serve. FileInput::new(nome, bytes) manda bytes que já estão na memória.

Comandos, cartões e botões ​

rust
use zynko::{Button, Card, Command, ReplyOptions, SendOptions};

client.set_commands(&[Command::new("ping", "Responde pong")]).await?;

// no EventHandler:
async fn interaction(&self, _ctx: Client, i: Interaction) -> HandlerResult {
    if i.command.as_deref() == Some("ping") {
        i.reply(ReplyOptions::new("pong").ephemeral()).await?;
    }
    Ok(())
}

client
    .send(
        &channel_id,
        SendOptions::default()
            .card(Card::new().title("Placar").field("Ana", "3", true))
            .row(vec![Button::primary("Mais um", "mais")]),
    )
    .await?;

Na interação, i.option_str("nome"), i.option_i64(..), i.option_f64(..) e i.option_bool(..) leem as opções (None se ficou vazia; user, channel e role chegam como ids, em option_str). Num clique, i.is_button() é verdadeiro, i.custom_id traz o botão e i.update(EditOptions::new().content(..).components(vec![])) muda a mensagem clicada.

Tudo sobre isso em Comandos, cartões e botões. Os exemplos de lá estão em TypeScript; em Rust os campos têm os mesmos nomes, e Command, CommandOption, Card e Button têm construtores (Command::new(..).option(..), Card::new().title(..), Button::primary(rótulo, custom_id), Button::link(rótulo, url)). O exemplo dado faz o /dado daquela página.

Permissões ​

rust
use zynko::Permissions;

if !ctx.can(&msg.channel_id, &msg.author.id, Permissions::KICK_MEMBERS) {
    msg.reply("Você não pode remover pessoas.").await?;
    return Ok(());
}

client.permissions_in(channel_id, user_id) devolve todos os bits (u64), calculados como o servidor calcula: @everyone, os cargos, as permissões do canal, ADM e dono. A lista dos bits está em Permissões.

API ​

client.rest() tem um método para cada ação comum, e request para o resto:

rust
use serde_json::{json, Value};
use zynko::{Method, MessagesQuery};

let rest = client.rest();
rest.kick(&guild_id, &user_id).await?;
rest.add_role(&guild_id, &user_id, &role_id).await?;
let msgs = rest.messages(&channel_id, MessagesQuery { limit: Some(50), ..Default::default() }).await?;
rest.request::<Value>(Method::PATCH, &format!("/guilds/{guild_id}/members/{user_id}"), Some(json!({ "nick": "Novato" })))
    .await?;

send, upload, edit_message, delete_message, messages, react, unreact, typing, me, profile, dm, members, kick, ban, unban, add_role, remove_role, create_role, create_channel, edit_channel, delete_channel, create_invite, set_commands, commands, interaction_callback e voice_token. Todas as rotas estão em Rotas.

Erros da API viram Error::Api { status, code, message } (também em err.status() e err.code()). Quando um limite de requisições estoura (429), a biblioteca espera o tempo pedido e tenta de novo, até 3 vezes (.retries(n) no builder muda isso).

Voz ​

Precisa da feature voice.

rust
use zynko::voice::JoinVoiceOptions;

let voice = client.join_voice(&channel_id, JoinVoiceOptions::default()).await?;
voice.player().enqueue("https://exemplo.com/musica.mp3", serde_json::Value::Null);

O bot precisa de Entrar em salas e Falar na sala, e do intent VOICE_STATES para saber onde as pessoas estão. Ele fica em uma sala por vez, como qualquer conta.

rust
JoinVoiceOptions {
    receive: false,  // ouvir a sala (eventos Audio)
    relay: false,    // a conexão alternativa, pelo relay do servidor
    deaf: false,     // aparecer com o som desligado
    player: PlayerOptions {
        ffmpeg: None,  // caminho do ffmpeg; padrão FFMPEG_PATH ou "ffmpeg"
        ytdlp: None,   // Some("yt-dlp"): passar links pelo yt-dlp antes
    },
}
voice.player()O tocador (abaixo).
voice.leave().awaitSai da sala.
voice.channel_id(), voice.server()A sala e o servidor de voz dela (Brasil 1, Brasil 2...).
voice.can_speak()Se os cargos deixam o bot falar ali.
voice.status()Connecting, Connected, Reconnecting ou Closed.
voice.events()Os eventos: Ready, Speaking(ids), Audio { .. }, Disconnect(motivo), Error.
client.voice()A conexão de voz atual, se houver.
rust
use zynko::VoiceEvent;

let mut eventos = voice.events();
tokio::spawn(async move {
    while let Ok(ev) = eventos.recv().await {
        match ev {
            VoiceEvent::Speaking(ids) => println!("falando: {ids:?}"),
            VoiceEvent::Disconnect(motivo) => println!("saiu: {motivo}"),
            _ => {}
        }
    }
});

Se alguém trocar o servidor da sala (Brasil 1 ↔ Brasil 2), o bot vai junto sozinho e a música continua de onde estava.

Tocador ​

rust
use serde_json::json;
use zynko::{tone, PlayerEvent};

let p = voice.player();
p.enqueue("https://radio.exemplo.com/stream", json!({ "title": "Rádio" })); // fila
p.play("https://exemplo.com/agora.mp3", json!(null));                      // toca já
p.skip(); p.pause(); p.resume(); p.stop();
p.set_volume(0.5);                                                          // 0 a 2
p.current(); p.queue(); p.volume(); p.is_paused();

let mut eventos = p.events();
while let Ok(ev) = eventos.recv().await {
    match ev {
        PlayerEvent::Start(track) => println!("tocando {}", track.meta["title"]),
        PlayerEvent::Finish(track) => {}
        PlayerEvent::Error { error, track } => eprintln!("{error}"),
        PlayerEvent::Idle => {} // a fila acabou
    }
}

O que dá para tocar (AudioInput):

entrada
"https://..." ou um caminhoAberto pelo ffmpeg: mp3, ogg, wav, flac, rádios por stream...
AudioInput::Stream(leitor)Áudio codificado vindo de qualquer AsyncRead (um download, um pipe); o ffmpeg decodifica.
AudioInput::Pcm(leitor)PCM cru: 48 kHz, estéreo, 16 bits little-endian. Não passa pelo ffmpeg.
tone(440.0, 3.0)Um tom de teste (frequência, segundos).

O segundo argumento de play e enqueue é um serde_json::Value com o que você quiser (título, quem pediu) e volta em track.meta nos eventos. O áudio sai em quadros de 20 ms, no ritmo real.

Caminhos de arquivo

O tocador abre o que receber. Num bot público, aceite só links (https://) do chat, nunca caminhos: senão qualquer pessoa toca arquivos da máquina do bot.

Ouvir a sala ​

rust
let voice = client
    .join_voice(&channel_id, JoinVoiceOptions { receive: true, ..Default::default() })
    .await?;
let mut eventos = voice.events();
while let Ok(ev) = eventos.recv().await {
    if let VoiceEvent::Audio { user_id, samples, channels } = ev {
        // samples: Arc<[i16]>, 48 kHz, mono, em pedaços de 10 ms
    }
}

Para gravar, transcrever ou medir volume. Avise as pessoas da sala: elas não sabem que um bot está ouvindo.

Exemplo completo ​

O exemplo musica é um bot de música pequeno: /tocar <link> (ou !tocar <link>), fila, e um cartão "Tocando agora" com os botões Pausar, Pular e Parar.

sh
ZYNKO_TOKEN=bot_... cargo run --example musica --features voice

O bot de música completo, em TypeScript, mostra o resto: volume e saída automática quando a sala esvazia.

Zynko · zynko.org