Skip to content

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/go

A 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.RestA 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.IntentVoiceStates

IntentGuilds, 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.

eventocamposquando
*ReadyUserConectou (de novo).
*MessageCreate*MessageMensagem nova.
*MessageUpdate*MessageMensagem editada (ou prévia de link pronta).
*MessageDeleteID, ChannelIDMensagem apagada.
*ReactionAdd, *ReactionRemoveMessageID, ChannelID, UserID, EmojiReação.
*TypingStartChannelID, UserID, TimestampAlguém digitando.
*GuildCreateGuildBot adicionado a um servidor (e cada servidor do READY).
*GuildUpdateGuildNome, ícone ou dono mudou.
*GuildDeleteGuildIDBot saiu ou o servidor foi apagado.
*ChannelCreate, *ChannelDeleteChannelCanal novo ou apagado (uma DM nova é um canal novo).
*ChannelUpdateChannel, PreviousCanal mudou.
*MemberAddMemberAlguém entrou num servidor.
*MemberUpdateMember, PreviousApelido ou cargos mudaram.
*MemberRemoveGuildID, UserIDAlguém saiu.
*PresenceUpdateUserID, StatusFicou online ou offline.
*VoiceStateUpdateState, PreviousAlguém entrou, saiu ou mudou na voz.
*InteractionCreate*InteractionAlguém usou um comando do bot ou clicou num botão dele.
*RawType, DataTodo evento do gateway, como chegou.
*ReconnectingCode, Reason, WaitA conexão caiu e vai voltar.
*ErrorEventErrErro 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, ComponentsOs 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.CommandO 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.MessageNo clique: o custom_id do botão e a mensagem dele.
i.User, i.MemberQuem usou, e como membro do servidor (nil numa DM).
i.ChannelID, i.GuildIDOnde.
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.PlayerO 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 o OnAudio antes 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 !ping com pong;
  • dado: o comando /dado com 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/musica

O bot de música completo, em JavaScript, mostra mais ideias: volume, ajuda e saída automática quando a sala esvazia.

Zynko · zynko.org