Biblioteca para Go
zynko.org/go é a biblioteca oficial para bots do Zynko em Go. Ela conecta no gateway e se reconecta sozinha, mantém servidores, canais e membros atualizados, entrega os eventos para funções tipadas e cobre a API. O pacote zynko.org/go/voice entra em salas, toca áudio e ouve quem fala.
Precisa de Go 1.25 ou mais novo. Tudo funciona sem cgo (CGO_ENABLED=0), inclusive a voz.
Instalação
sh
go get zynko.org/goA voz vem no mesmo módulo, no pacote zynko.org/go/voice: um bot que não importa esse pacote não leva o LiveKit para o binário. Para tocar e ouvir, o bot usa o ffmpeg, que precisa estar instalado na máquina (ou indicado em FFMPEG_PATH).
Primeiro bot
go
package main
import (
"context"
"log"
"os"
"os/signal"
"zynko.org/go"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
client, err := zynko.New(zynko.Options{
Token: os.Getenv("ZYNKO_TOKEN"),
Intents: zynko.IntentGuilds | zynko.IntentMessages,
})
if err != nil {
log.Fatal(err)
}
client.OnMessageCreate(func(ctx context.Context, m *zynko.Message) error {
if m.Mine() || m.Content != "!ping" {
return nil
}
_, err := m.Reply(ctx, "pong")
return err
})
me, err := client.Login(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("%s conectado em %d servidores", me.DisplayName, len(client.Guilds()))
<-ctx.Done()
client.Close()
}sh
ZYNKO_TOKEN=bot_... go run .O cliente
go
zynko.New(zynko.Options{
Token: token, // o token do bot (dev.zynko.org)
Intents: intents, // o que receber; padrão: IntentGuilds | IntentMessages
API: "", // outro servidor; padrão https://zynko.org/api
Retries: 0, // vezes que um 429 é esperado e repetido; padrão 3, -1 nunca
SyncEvents: false, // rodar os handlers em ordem, na goroutine do gateway
})client.Login(ctx) | Conecta. Volta com a conta do bot quando o READY chega. Se o ctx acabar antes, ou o token for recusado, o cliente fecha. |
client.Close() | Sai da voz e fecha a conexão. |
client.Done() | Fecha quando o cliente para de vez (Close, ou token recusado). |
client.User(), client.UserID() | A conta do bot. |
client.Guilds(), client.Guild(id) | Os servidores. Cada *CachedGuild tem Channels(), Members(), Roles(), Emojis(), OnlineIDs() e as versões por id (Channel(id), Member(id), Role(id), Online(id)). |
client.Channels(), client.Channel(id) | Todos os canais que o bot vê, DMs incluídas. |
client.Users(), client.CachedUser(id) | As contas que o bot já viu. |
client.Member(guildID, userID) | Alguém como membro de um servidor. |
client.VoiceStates(), client.VoiceState(userID) | Quem está em voz (precisa de IntentVoiceStates). |
client.Rest | A API inteira (veja API). |
Os caches se atualizam sozinhos com os eventos e podem ser lidos de qualquer goroutine: os métodos devolvem cópias. 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.
Intents
go
zynko.IntentGuilds | zynko.IntentMessages | zynko.IntentVoiceStatesIntentGuilds, IntentGuildMembers, IntentPresences, IntentMessages, IntentReactions, IntentTyping, IntentVoiceStates e IntentsAll. Peça só o que o bot usa. Detalhes em Gateway.
Eventos
Cada evento é um tipo. zynko.On registra uma função para um deles, escolhido pelo tipo do argumento:
go
zynko.On(client, func(ctx context.Context, e *zynko.MemberAdd) error {
log.Printf("%s entrou", e.Member.User.DisplayName)
return nil
})Os mais comuns têm atalhos: client.OnReady, client.OnMessageCreate, client.OnInteraction e client.OnError. Todos devolvem uma função que remove o handler.
| evento | campos | quando |
|---|---|---|
*Ready | User | Conectou (de novo). |
*MessageCreate | *Message | Mensagem nova. |
*MessageUpdate | *Message | Mensagem editada (ou prévia de link pronta). |
*MessageDelete | ID, ChannelID | Mensagem apagada. |
*ReactionAdd, *ReactionRemove | MessageID, ChannelID, UserID, Emoji | Reação. |
*TypingStart | ChannelID, UserID, Timestamp | Alguém digitando. |
*GuildCreate | Guild | Bot adicionado a um servidor (e cada servidor do READY). |
*GuildUpdate | Guild | Nome, ícone ou dono mudou. |
*GuildDelete | GuildID | Bot saiu ou o servidor foi apagado. |
*ChannelCreate, *ChannelDelete | Channel | Canal novo ou apagado (uma DM nova é um canal novo). |
*ChannelUpdate | Channel, Previous | Canal mudou. |
*MemberAdd | Member | Alguém entrou num servidor. |
*MemberUpdate | Member, Previous | Apelido ou cargos mudaram. |
*MemberRemove | GuildID, UserID | Alguém saiu. |
*PresenceUpdate | UserID, Status | Ficou online ou offline. |
*VoiceStateUpdate | State, Previous | Alguém entrou, saiu ou mudou na voz. |
*InteractionCreate | *Interaction | Alguém usou um comando do bot ou clicou num botão dele. |
*Raw | Type, Data | Todo evento do gateway, como chegou. |
*Reconnecting | Code, Reason, Wait | A conexão caiu e vai voltar. |
*ErrorEvent | Err | Erro num handler, ou o token foi recusado. |
Previous é nil quando o cliente ainda não conhecia o canal, membro ou estado.
Um handler que devolve erro, ou entra em pânico, vira um *ErrorEvent, sem derrubar o bot. Cada evento roda na sua própria goroutine (os handlers de um mesmo evento, em ordem); com SyncEvents: true, tudo roda em ordem na goroutine do gateway, e um handler lento segura os outros eventos. O ctx que os handlers recebem acaba quando o cliente fecha.
zynko.Wait espera o próximo evento de um tipo que passar no filtro. O prazo vem do ctx:
go
_, err := m.Reply(ctx, "Qual o seu nome?")
wctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
answer, err := zynko.Wait(wctx, client, func(e *zynko.MessageCreate) bool {
return e.Author.ID == m.Author.ID
})Mensagens
*MessageCreate e *MessageUpdate trazem um *Message:
ID, ChannelID, Author, Content, Timestamp, Attachments, Reactions, Embeds, ReplyTo, Cards, Components | Os dados da mensagem. |
GuildID() | O servidor, ou "" 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(ctx, texto), ReplyWith(ctx, SendOptions) | Responde no mesmo canal, como resposta a ela. |
React(ctx, emoji), Unreact(ctx, emoji) | Reage ou tira a reação. |
Edit(ctx, texto), EditWith(ctx, EditOptions), Delete(ctx) | 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:
go
client.Send(ctx, channelID, "Olá!")
relatorio, err := zynko.FileFromPath("relatorio.pdf")
client.SendWith(ctx, channelID, zynko.SendOptions{
Content: "O relatório de hoje",
Files: []zynko.File{relatorio},
})
client.SendDM(ctx, userID, zynko.SendOptions{Content: "Oi! Isso é uma mensagem direta."})Um zynko.File também pode vir da memória: zynko.File{Name: "dados.csv", Data: bytes}.
Comandos, cartões e botões
go
client.SetCommands(ctx, []zynko.Command{{Name: "ping", Description: "Responde pong"}})
client.OnInteraction(func(ctx context.Context, i *zynko.Interaction) error {
if i.Command == "ping" {
_, err := i.ReplyEphemeral(ctx, "pong")
return err
}
return nil
})
client.SendWith(ctx, channelID, zynko.SendOptions{
Cards: []zynko.Card{{
Title: "Placar",
Fields: []zynko.CardField{{Name: "Ana", Value: "3", Inline: true}},
}},
Components: zynko.Components{{
{Style: zynko.ButtonPrimary, Label: "Mais um", CustomID: "mais"},
}},
})A interação:
i.Type, i.IsCommand(), i.IsButton() | "command" ou "button". |
i.Command | O nome do comando. |
i.String(nome), i.Int(nome), i.Float(nome), i.Bool(nome) | O valor de uma opção, e se ela veio. user, channel e role chegam como ids (use String). |
i.CustomID, i.Message | No clique: o custom_id do botão e a mensagem dele. |
i.User, i.Member | Quem usou, e como membro do servidor (nil numa DM). |
i.ChannelID, i.GuildID | Onde. |
i.Reply(ctx, texto), i.ReplyEphemeral(ctx, texto), i.ReplyWith(ctx, ReplyOptions) | Responde com uma mensagem; Ephemeral: true só para a pessoa. |
i.Update(ctx, texto), i.UpdateWith(ctx, EditOptions) | Só em cliques: muda a mensagem do botão. |
Em EditOptions, o que fica vazio não muda. Para tirar os botões, mande Components: zynko.Components{}. Nas opções de comando, zynko.Limit(2) preenche MinValue e MaxValue.
Tudo sobre isso em Comandos, cartões e botões.
Permissões
go
if !client.Can(m.ChannelID, m.Author.ID, zynko.PermKickMembers) {
_, err := m.Reply(ctx, "Você não pode remover pessoas.")
return err
}client.PermissionsIn(channelID, userID) devolve todos os bits, calculados como o servidor calcula: @everyone, os cargos, as permissões do canal, ADM e dono. zynko.ComputePermissions faz a mesma conta com dados que você já tem. A lista dos bits está em Permissões; em Go, PermAdministrator, PermSendMessages, PermConnect...
API
client.Rest tem um método para cada ação comum, e Request para o resto:
go
client.Rest.Kick(ctx, guildID, userID)
client.Rest.AddRole(ctx, guildID, userID, roleID)
msgs, err := client.Rest.Messages(ctx, channelID, zynko.MessagesQuery{Limit: 50})
client.Rest.Request(ctx, "PATCH", "/guilds/"+guildID+"/members/"+userID, map[string]any{"nick": "Novato"}, nil)Send, Upload, EditMessage, DeleteMessage, Messages, React, Unreact, Typing, Me, Profile, DM, Members, Kick, Ban, Unban, AddRole, RemoveRole, CreateRole, CreateChannel, EditChannel, DeleteChannel, CreateInvite, SetCommands, Commands, InteractionCallback e VoiceToken. Todas as rotas estão em Rotas. Para usar só a API, sem gateway: zynko.NewRest(zynko.RestOptions{Token: token}).
Erros da API viram *zynko.Error, com Status, Code e Message:
go
var zerr *zynko.Error
if errors.As(err, &zerr) && zerr.Code == "forbidden" { /* ... */ }Quando um limite de requisições estoura (429), a biblioteca espera o tempo pedido e tenta de novo, até 3 vezes.
Voz
go
import "zynko.org/go/voice"
conn, err := voice.Join(ctx, client, channelID, voice.Options{})
conn.Player.Enqueue(voice.URL("https://exemplo.com/musica.mp3"), nil)O bot precisa de Entrar em salas e Falar na sala, e do intent IntentVoiceStates para saber onde as pessoas estão. Ele fica em uma sala por vez, como qualquer conta: voice.Join noutra sala sai da anterior, e voice.Of(client) é a conexão atual (ou nil).
go
voice.Join(ctx, client, channelID, voice.Options{
Receive: false, // ouvir a sala (OnOpus e OnAudio)
Relay: false, // a conexão alternativa, pelo relay do servidor
Deaf: false, // aparecer com o som desligado
YTDLP: "", // passar links pelo yt-dlp antes ("yt-dlp", ou o caminho dele)
FFmpeg: "ffmpeg", // caminho do ffmpeg
Bitrate: 128000, // bits por segundo do Opus
})conn.Player | O tocador (abaixo). |
conn.Leave(ctx) | Sai da sala. |
conn.ChannelID(), conn.Server() | A sala e o servidor de voz dela (Brasil 1, Brasil 2...). |
conn.CanSpeak() | Se os cargos deixam o bot falar ali. |
conn.OnReady(func()) | Conectou (de novo, depois de uma troca de servidor). |
conn.OnSpeaking(func(ids []string)) | Quem está falando agora (muda quando alguém começa ou para). |
conn.OnOpus(func(userID, pacote)) | Com Receive: cada pacote Opus de cada pessoa. |
conn.OnAudio(func(userID, samples)) | Com Receive: o som de cada pessoa, decodificado. |
conn.OnDisconnect(func(motivo)) | Saiu de vez: pediu, foi removido, a sala sumiu. |
conn.Done() | Fecha quando a conexão acaba. |
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.
Como o áudio sai, sem cgo
O tocador decodifica a entrada para PCM (48 kHz, estéreo, 16 bits) e manda quadros de 20 ms no ritmo real. Um ffmpeg por conexão transforma esses quadros em Opus, e os pacotes vão para o LiveKit pelo SDK em Go puro. Quando nada está tocando, ou está pausado, a conexão manda silêncio. A espera entre o tocador e a sala fica em torno de 150 ms.
Tocador
go
p := conn.Player
p.Enqueue(voice.URL("https://radio.exemplo.com/stream"), "Rádio") // fila
p.Play(voice.URL("https://exemplo.com/agora.mp3"), nil) // toca já
p.Skip(); p.Pause(); p.Resume(); p.Stop()
p.SetVolume(0.5) // 0 a 2
p.Current(); p.Queue(); p.Paused(); p.Volume()
p.OnStart(func(t *voice.Track) {})
p.OnFinish(func(t *voice.Track) {})
p.OnError(func(err error, t *voice.Track) {})
p.OnIdle(func() {})O que dá para tocar:
| entrada | |
|---|---|
voice.URL("https://...") ou voice.URL(caminho) | Aberto pelo ffmpeg: mp3, ogg, wav, flac, rádios por stream... |
voice.Stream(r) | Áudio codificado vindo de um io.Reader (um download, um pipe); o ffmpeg decodifica. |
voice.PCM(r) | PCM cru: 48 kHz, estéreo, 16 bits little-endian. Não passa pelo ffmpeg. |
voice.Tone(440, 3) | Um tom de teste. |
O segundo argumento de Play e Enqueue é o que você quiser (título, quem pediu) e volta em track.Meta nos eventos. Os eventos do tocador rodam em ordem, numa goroutine dele.
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
go
conn, err := voice.Join(ctx, client, channelID, voice.Options{Receive: true})
conn.OnAudio(func(userID string, samples []int16) {
// 48 kHz, mono, em pedaços de 20 ms (960 amostras)
})Com Receive, cada pessoa chega de dois jeitos:
OnOpus: os pacotes Opus como vêm do LiveKit (20 ms cada), sem custo extra. Bom para gravar (num arquivo Ogg, por exemplo) ou repassar.OnAudio: o som já decodificado em PCM. Como o Go não tem decodificador Opus sem cgo, a biblioteca usa um ffmpeg por pessoa que fala. Registre oOnAudioantes de as pessoas falarem.
Para gravar, transcrever ou medir volume. Avise as pessoas da sala: elas não sabem que um bot está ouvindo.
Exemplos
Na pasta examples do repositório ZynkoOrg/zynko-go:
ping: responde!pingcompong;dado: o comando/dadocom um cartão e o botão "De novo";musica: um bot de música pequeno, com/tocar <link>(ou!tocar), fila e os botões Pausar, Pular e Parar.
sh
ZYNKO_TOKEN=bot_... go run ./examples/musicaO bot de música completo, em JavaScript, mostra mais ideias: volume, ajuda e saída automática quando a sala esvazia.