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 voicePara 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 runstart() 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().await | Conecta. Devolve a conta do bot quando o READY chega. |
client.start().await | login e depois espera até o cliente parar. |
client.destroy().await | Sai 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_STATESGUILDS, 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étodo | argumentos | quando |
|---|---|---|
ready | User | Conectou (de novo). |
message_create | Message | Mensagem nova. |
message_update | Message | Mensagem editada (ou prévia de link pronta). Só vêm os campos que mudaram. |
message_delete | MessageDeleteEvent { id, channel_id } | Mensagem apagada. |
reaction_add, reaction_remove | ReactionEvent { message_id, channel_id, user_id, emoji } | Reação. |
typing_start | TypingEvent { channel_id, user_id, timestamp } | Alguém digitando. |
guild_create | CachedGuild | Bot adicionado a um servidor (e cada servidor do READY). |
guild_update | CachedGuild | Nome, ícone ou dono mudou. |
guild_delete | id | Bot saiu ou o servidor foi apagado. |
channel_create, channel_delete | Channel | Canal novo ou apagado (uma DM nova é um canal novo). |
channel_update | Channel, anterior | Canal mudou. |
member_add | Member | Alguém entrou num servidor. |
member_update | Member, anterior | Apelido ou cargos mudaram. |
member_remove | MemberRemoveEvent { guild_id, user_id } | Alguém saiu. |
presence_update | PresenceEvent { user_id, status } | Ficou online ou offline. |
voice_state_update | VoiceState, anterior | Alguém entrou, saiu ou mudou na voz. |
interaction | Interaction | Alguém usou um comando do bot ou clicou num botão dele. |
raw | tipo, dados (serde_json::Value) | Todo evento do gateway, como chegou. |
reconnecting | ReconnectInfo { code, reason, wait } | A conexão caiu e vai voltar. |
error | Arc<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_to | Os 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().await | Sai 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 caminho | Aberto 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 voiceO bot de música completo, em TypeScript, mostra o resto: volume e saída automática quando a sala esvazia.