Documentation
¶
Overview ¶
Package sikasa: bot.go Purpose: Defines the top-level Bot type, lifecycle (New, Start, Stop), intent helpers, and the registries that command/keyword builders write into.
Key Components:
- Bot: wraps *bot.Client (disgo) and owns the registries
- New(): constructs a Bot with sensible defaults; defers wiring until Start() so options can be appended fluently
- WithIntents(): fluent setter for gateway intents
- Start()/Stop(): lifecycle, opens the gateway and syncs slash commands
Dependencies:
- github.com/disgoorg/disgo: high-level Discord bot framework
- github.com/disgoorg/disgo/voice: voice connection manager + DAVE
- github.com/thomas-vilte/dave-go: pure-Go DAVE/E2EE backend
Note: discordgo has been retired in favour of disgo because the latter is the only major Go Discord library with native DAVE (E2EE) support, which Discord enforces in many voice regions (close code 4017 without it).
Package sikasa: buttonctx.go Purpose: Defines ButtonCtx, the per-interaction context passed to button click handlers registered via Bot.OnButton. Provides reply, update, and defer helpers that mirror MsgCtx / CmdCtx so handler code stays uniform.
Key Components:
- ButtonCtx: handler context for a single button interaction
- Reply: sends a fresh ephemeral or public response
- Update: edits the message that hosted the button
- Defer: acks the interaction without producing a visible reply
Dependencies:
- github.com/disgoorg/disgo/discord: message and component types
- github.com/disgoorg/disgo/handler: ComponentEvent (interaction wrapper)
- github.com/disgoorg/disgo/events: underlying event type
Note: Discord requires a response within 3 seconds or the interaction fails. Reply, Update, and Defer all satisfy that requirement; long-running work should call Defer first and then Followup later.
Package sikasa: buttons.go Purpose: Provides Bot.OnButton, a thin sugar over disgo's handler.Mux.ButtonComponent that hands the user a ButtonCtx instead of raw disgo types. Pattern strings are passed straight through to disgo, so customID lookup uses the same chi-style {var} syntax.
Key Components:
- ButtonHandler: signature for sikasa-style button handlers
- Bot.OnButton(): registers a button route on the underlying router
Note: OnButton must be called BEFORE Bot.Start(), because the disgo handler.Mux is constructed there and routes are registered on it during Start. Calling OnButton after Start would panic.
Package sikasa: cmdctx.go Purpose: Defines CmdCtx, the per-invocation context passed to slash command handlers. Bundles the disgo handler.CommandEvent and SlashCommandInteractionData with reply helpers for text, embeds, and media so handlers stay one-liners.
Key Components:
- CmdCtx: per-invocation handler context
- newCmdCtx(): internal constructor; called by command.go's router glue
- Reply / ReplyEmbed / ReplyFile / ReplyURL: response helpers
- String / Int / Bool / User / Channel / Attachment: option accessors
- Defer / Followup: long-running command pattern
Dependencies:
- github.com/disgoorg/disgo/discord: message, embed, and option types
- github.com/disgoorg/disgo/handler: CommandEvent, the response surface
Note: Reply must be called within 3 seconds of the interaction firing; otherwise call Defer() first and finish with Followup().
Package sikasa: command.go Purpose: Provides a fluent CommandBuilder for declaring slash commands alongside their handlers in one place. Builders translate to disgo's discord.SlashCommandCreate at Start() time, and their handlers are wired into the disgo handler.Router for dispatch.
Key Components:
- CommandBuilder: fluent type for one slash command
- Bot.Command(): entry point that returns a fresh builder
- StringArg/IntArg/BoolArg/UserArg/ChannelArg/AttachmentArg: option helpers
- CmdHandler: function signature for command handlers
Dependencies:
- github.com/disgoorg/disgo/discord: ApplicationCommandCreate types
- github.com/disgoorg/disgo/handler: router and CommandEvent
Package sikasa: embed.go Purpose: Provides a fluent EmbedBuilder so callers can compose Discord embeds without dropping into the raw discord.Embed struct. Pairs with MsgCtx.ReplyEmbed / SendEmbed and CmdCtx.ReplyEmbed.
Key Components:
- EmbedBuilder: immutable, chainable wrapper over discord.Embed
- NewEmbed(): constructor; preferred entry point
- Build(): returns the underlying discord.Embed for direct use
Note: Discord enforces hard limits on embed fields. Setters do NOT truncate; they pass through whatever you give them. The package-level Truncate helper is provided for callers that want explicit clamping.
Package sikasa: errors.go Purpose: Exports sentinel errors used across the package so callers can distinguish failure modes via errors.Is() instead of string matching.
Key Components:
- Voice errors: ErrBotNotStarted, ErrInvalidGuildID, ErrInvalidChannelID, ErrNotInVoice, ErrNoAudio, ErrNotPaused
- Bot lifecycle: ErrEmptyToken
- Prefix command errors: ErrUnknownCommand, ErrMissingArg, ErrInvalidArg
Note: Wrapped errors (fmt.Errorf("%w: ...", ErrXxx, ...)) preserve the sentinel chain, so errors.Is(err, sikasa.ErrXxx) works on both bare and wrapped values.
Package sikasa: keyword.go Purpose: Provides a fluent KeywordBuilder for matching plain-message content against a set of substrings or a regular expression and firing a handler when a match occurs.
Key Components:
- KeywordBuilder: fluent type for one keyword/regex rule
- Bot.OnKeyword(): substring-based matcher (case-insensitive)
- Bot.OnRegex(): regex-based matcher (compiled once at registration)
- MsgHandler: function signature for keyword handlers
Dependencies:
- regexp: standard library regex engine
Note: Multiple keyword rules may match the same message; all matching rules fire in registration order.
Package sikasa: msgctx.go Purpose: Defines MsgCtx, the per-message context passed to keyword/regex handlers. Bundles the disgo MessageCreate event with reply helpers for text, embeds, and media.
Key Components:
- MsgCtx: per-message handler context
- newMsgCtx(): internal constructor; called by bot.go's keyword dispatcher
- Reply / Send / ReplyFile / ReplyURL: response helpers
- AuthorMention / Author / ChannelID / GuildID: shortcuts to common fields
Dependencies:
- github.com/disgoorg/disgo/discord: message and reference types
- github.com/disgoorg/disgo/events: MessageCreate event
Note: Reply uses Discord's inline reply (message reference) so the bot's reply links back to the user's message; Send posts to the channel without the reference link.
Package sikasa: prefix.go Purpose: Provides a fluent PrefixBuilder for declaring text-based prefix commands (e.g. "!play song.mp3"). Mirrors the bot.Command builder so users get a consistent feel across slash and prefix invocation paths.
Key Components:
- PrefixBuilder: fluent type for one prefix command
- Bot.OnPrefix(): entry point that returns a fresh builder
- Bot.SetPrefix(): sets the global trigger prefix (default: "")
- StringArg/IntArg/BoolArg: option helpers, hybrid with ctx.Rest()
- PrefixHandler: function signature for prefix command handlers
- dispatchPrefix: internal MessageCreate router
Note: Last StringArg consumes the entire remaining message tail. Quote-aware tokenization (e.g. "!echo \"hello world\"" as a single arg) is intentionally out of scope for the MVP; callers can fall back to ctx.Rest() if they need the raw string.
Package sikasa: prefixctx.go Purpose: Defines PrefixCtx, the per-invocation context passed to prefix command handlers. Embeds *MsgCtx to inherit Reply/Send/React helpers, and adds typed accessors (String/Int/Bool) plus raw-token escape hatches (Args/Arg/Rest) so handlers can mix builder validation with manual parsing.
Key Components:
- PrefixCtx: per-invocation handler context, wraps MsgCtx
- String/Int/Bool: typed access to declared builder args
- Args/Arg/Rest: raw-token access for free-form parsing
- Name(): the resolved command name (not the alias)
Note: Reply* methods come from the embedded *MsgCtx; the prefix dispatcher is the only construction site, so these accessors are guaranteed non-nil for the user.
Package sikasa: ratelimit.go Purpose: Provides an in-memory sliding window rate limiter for commands and keywords.
Package sikasa: voice.go Purpose: High-level API for joining Discord voice channels and playing audio sources (local files, YouTube URLs) on top of disgo's voice manager.
Key Components:
- VoiceManager: factory accessed via bot.Voice(); holds per-guild contexts
- VoiceCtx: handle returned by Join(); supports PlayFile, PlayYouTube, Pause, Resume, Stop, Leave
- PlaybackState: enum for the current state machine
Dependencies:
- github.com/disgoorg/disgo/voice: Conn, OpusFrameProvider, SpeakingFlags
- voice_ffmpeg.go, voice_ogg.go, voice_youtube.go: pipeline pieces
- voice_provider.go: bridges the pipeline to disgo
Note: Pause/Resume now happen at the OpusFrameProvider layer. The provider returns voice.SilenceAudioFrame while paused, so disgo's AudioSender keeps ticking and Discord does not drop the speaking session.
Package sikasa: voice_check.go Purpose: Shared "is the user allowed to control this voice session" guard used by prefix commands tagged with .RequireSameVoice() and by anyone who needs the same check from a slash handler.
Key Components:
- Bot.checkSameVoice(): inspects the cached voice states for the guild and returns ErrNotSameChannel when the invoker is not co-located with the bot
Note: When the bot is not connected to any voice channel in the guild the check passes. This lets initial-join commands like "play" still work; the command itself decides whether to join. Use this helper only after the caller has already checked for a guild context (DMs have no voice state).
Package sikasa: voice_ffmpeg.go Purpose: Manages FFmpeg subprocesses that produce Ogg-Opus output suitable for direct streaming into a Discord voice connection.
Key Components:
- ffmpegProcess: wraps an *exec.Cmd plus its stdout pipe
- spawnTranscode(): decode any input format and re-encode to Opus
- spawnPassthrough(): stream-copy already-Opus input (zero CPU cost)
- spawnFromStdin(): accept piped input from another process (yt-dlp)
Dependencies:
- os/exec: subprocess lifecycle
Note: All spawn functions configure FFmpeg to write Ogg-Opus to stdout. Stderr is silenced to avoid noise; for debugging, route it to os.Stderr.
Package sikasa: voice_ogg.go Purpose: Parses an Ogg-Opus byte stream from FFmpeg's stdout into individual Opus frames suitable for direct injection into discordgo's OpusSend channel.
Key Components:
- oggPageParser: reads Ogg pages from an io.Reader and returns segments
- NextFrame(): returns the next Opus packet (one 20ms frame)
Dependencies:
- encoding/binary: little-endian header parsing
- io: EOF handling
Note: Ogg page format is fixed-27-byte header followed by a segment table. Segments < 255 bytes mark a packet boundary; consecutive 255-byte segments are concatenated until a sub-255 segment ends the packet. The first two pages of an Opus stream are headers (OpusHead, OpusTags) and are skipped.
Package sikasa: voice_provider.go Purpose: Bridges the FFmpeg-driven Ogg-Opus pipeline (library-agnostic) into disgo's voice.OpusFrameProvider interface. The provider is owned by a VoiceCtx and replaced on every PlayFile / PlayYouTube call.
Key Components:
- streamProvider: implements voice.OpusFrameProvider for one ffmpeg run
Dependencies:
- github.com/disgoorg/disgo/voice: SilenceAudioFrame, OpusFrameProvider
Note: disgo's internal AudioSender pulls a frame every 20ms. While paused we return SilenceAudioFrame so the sender keeps ticking and Discord does not drop the speaking session. On EOF we return io.EOF and the AudioSender stops calling us; the next PlayX swap re-arms a fresh provider.
Package sikasa: voice_queue.go Purpose: Per-guild playback queue. Owned by VoiceCtx so each guild session has its own track list and cursor; multi-guild concurrency falls out of the existing voices map[snowflake.ID]*VoiceCtx.
Key Components:
- TrackKind: enum distinguishing local files from streamed URLs
- Track: a single queue entry (kind + source descriptor)
- queue: slice + cursor; supports advance, rewind, jump, clear
Note: queue is not exported. Callers manipulate the queue through VoiceCtx methods (Enqueue, Next, Prev, Skip, Queue, ClearQueue), which serialize access through VoiceCtx.mu. The queue itself is therefore not internally locked.
Package sikasa: voice_recovery.go Purpose: Watches the slog stream for DAVE/voice errors that indicate the session has desynchronized ("no active epoch") and triggers a Reconnect() on the affected VoiceCtx. The watcher is layered on top of the user's own slog handler so log output is unchanged.
Key Components:
- recoveryHandler: slog.Handler that wraps another handler, sniffs error records, and dispatches recoveries
- installRecovery: installs the wrapper around the bot's logger
Note: Reconnect is rate-limited per guild (one attempt every 30s) so a flood of "no active epoch" errors does not stack reconnect attempts on top of each other.
Package sikasa: voice_youtube.go Purpose: Spawns yt-dlp to extract a YouTube (or other supported site) audio stream and pipes it into FFmpeg. Also provides metadata + search probes so callers can show "Title by Uploader" instead of raw URLs and let users search by free-text query.
Key Components:
- spawnYouTube(): chains yt-dlp -> FFmpeg, returning a single ffmpegProcess whose stdout yields Ogg-Opus
- probeYouTubeEntries(): expands a URL into Track lists (single video, playlist, or channel)
- SearchYouTube(): runs `ytsearch<n>:<query>` to return top-N candidate Tracks for an interactive picker
- IsHTTPURL(): cheap heuristic to decide whether user input is a URL or a free-text search query
Dependencies:
- os/exec: subprocess lifecycle
Note: We *prefer* Opus-only formats from yt-dlp (itag 251, 250, 249) so FFmpeg can stream-copy them into the Ogg container with zero re-encoding. Pure remux is ~99% cheaper than transcoding and preserves source quality. When yt-dlp falls back to a non-Opus codec (rare on YouTube, common on other sites), we transcode through libopus as before.
Package sikasa: ytsearch.go Purpose: Holds the per-bot YouTube search session map. Each session captures the candidate tracks shown to one user via an interactive picker and the metadata (invoker, expiry) needed to authorize a button click. Bound to the bot lifetime; sessions auto-expire after ytSearchTTL.
Key Components:
- ytSearchSession: per-picker state: candidates, invoker, expiry
- Bot.NewYTSearchSession: registers a fresh session and returns its id
- Bot.YTSearchSession: looks up an active session by id
- Bot.consumeYTSearch: atomic lookup-and-delete for click handlers
- BuildYTSearchEmbed: renders the picker embed and button row
Note: customID layout: "sikasa:ytsearch:<sessionID>:<idx>". The session id is a 12-char base32 random string; collisions are vanishingly small for the active window. Sessions live in memory only; restarting the bot invalidates outstanding pickers (acceptable trade since the parent message visually loses its interactive state on restart anyway).
Index ¶
- Constants
- Variables
- func IsHTTPURL(s string) bool
- func Truncate(s string, n int) string
- type Bot
- func (b *Bot) Command(name, description string) *CommandBuilder
- func (b *Bot) ConsumeYTSearch(id string) *ytSearchSession
- func (b *Bot) Disgo() *bot.Client
- func (b *Bot) NewYTSearchSession(invoker, guildID snowflake.ID, tracks []Track) string
- func (b *Bot) NewYTSearchSessionMode(invoker, guildID snowflake.ID, tracks []Track, mode YTSearchMode) string
- func (b *Bot) OnButton(pattern string, h ButtonHandler) *Bot
- func (b *Bot) OnKeyword(terms ...string) *KeywordBuilder
- func (b *Bot) OnPrefix(name, description string) *PrefixBuilder
- func (b *Bot) OnRegex(pattern string) *KeywordBuilder
- func (b *Bot) SetPrefix(p string) *Bot
- func (b *Bot) Start() error
- func (b *Bot) Stop() error
- func (b *Bot) Voice() *VoiceManager
- func (b *Bot) WithGuild(guildID string) *Bot
- func (b *Bot) WithIntents(intents gateway.Intents) *Bot
- func (b *Bot) WithLogger(l *log.Logger) *Bot
- func (b *Bot) WithSlog(l *slog.Logger) *Bot
- func (b *Bot) WithVerbose() *Bot
- func (b *Bot) YTSearchSession(id string) *ytSearchSession
- type ButtonCtx
- func (c *ButtonCtx) Author() discord.User
- func (c *ButtonCtx) AuthorID() string
- func (c *ButtonCtx) Bot() *Bot
- func (c *ButtonCtx) ChannelID() string
- func (c *ButtonCtx) ClearComponents() error
- func (c *ButtonCtx) CustomID() string
- func (c *ButtonCtx) Data() discord.ButtonInteractionData
- func (c *ButtonCtx) Defer(ephemeral bool) error
- func (c *ButtonCtx) DeferUpdate() error
- func (c *ButtonCtx) Event() *handler.ComponentEvent
- func (c *ButtonCtx) GuildID() string
- func (c *ButtonCtx) Reply(text string) error
- func (c *ButtonCtx) ReplyEmbed(embed any) error
- func (c *ButtonCtx) ReplyPublic(text string) error
- func (c *ButtonCtx) Update(text string) error
- func (c *ButtonCtx) UpdateEmbed(embed any) error
- func (c *ButtonCtx) Var(name string) string
- func (c *ButtonCtx) Vars() map[string]string
- type ButtonHandler
- type CmdCtx
- func (c *CmdCtx) Attachment(name string) discord.Attachment
- func (c *CmdCtx) Author() discord.User
- func (c *CmdCtx) Bool(name string) bool
- func (c *CmdCtx) Bot() *Bot
- func (c *CmdCtx) Channel(name string) discord.ResolvedChannel
- func (c *CmdCtx) ChannelID() string
- func (c *CmdCtx) Data() discord.SlashCommandInteractionData
- func (c *CmdCtx) Defer(ephemeral bool) error
- func (c *CmdCtx) Event() *handler.CommandEvent
- func (c *CmdCtx) Followup(text string) error
- func (c *CmdCtx) GuildID() string
- func (c *CmdCtx) Int(name string) int64
- func (c *CmdCtx) Reply(text string) error
- func (c *CmdCtx) ReplyEmbed(embed discord.Embed) error
- func (c *CmdCtx) ReplyEphemeral(text string) error
- func (c *CmdCtx) ReplyFile(content, filePath string) error
- func (c *CmdCtx) ReplyURL(content, url, fileName string) error
- func (c *CmdCtx) String(name string) string
- func (c *CmdCtx) User(name string) discord.User
- type CmdHandler
- type CommandBuilder
- func (c *CommandBuilder) AttachmentArg(name, description string, required bool) *CommandBuilder
- func (c *CommandBuilder) BoolArg(name, description string, required bool) *CommandBuilder
- func (c *CommandBuilder) ChannelArg(name, description string, required bool) *CommandBuilder
- func (c *CommandBuilder) Handle(h CmdHandler) *CommandBuilder
- func (c *CommandBuilder) IntArg(name, description string, required bool) *CommandBuilder
- func (c *CommandBuilder) StringArg(name, description string, required bool) *CommandBuilder
- func (c *CommandBuilder) UserArg(name, description string, required bool) *CommandBuilder
- type EmbedBuilder
- func (b *EmbedBuilder) Author(name, iconURL, url string) *EmbedBuilder
- func (b *EmbedBuilder) Build() discord.Embed
- func (b *EmbedBuilder) ClearFields() *EmbedBuilder
- func (b *EmbedBuilder) Color(c int) *EmbedBuilder
- func (b *EmbedBuilder) Description(s string) *EmbedBuilder
- func (b *EmbedBuilder) Field(name, value string, inline bool) *EmbedBuilder
- func (b *EmbedBuilder) Footer(text, iconURL string) *EmbedBuilder
- func (b *EmbedBuilder) Image(url string) *EmbedBuilder
- func (b *EmbedBuilder) Now() *EmbedBuilder
- func (b *EmbedBuilder) Thumbnail(url string) *EmbedBuilder
- func (b *EmbedBuilder) Timestamp(t time.Time) *EmbedBuilder
- func (b *EmbedBuilder) Title(s string) *EmbedBuilder
- func (b *EmbedBuilder) URL(u string) *EmbedBuilder
- type Intents
- type KeywordBuilder
- type MsgCtx
- func (c *MsgCtx) Author() discord.User
- func (c *MsgCtx) AuthorMention() string
- func (c *MsgCtx) Bot() *Bot
- func (c *MsgCtx) ChannelID() string
- func (c *MsgCtx) Content() string
- func (c *MsgCtx) Event() *events.MessageCreate
- func (c *MsgCtx) GuildID() string
- func (c *MsgCtx) Message() discord.Message
- func (c *MsgCtx) NewEmbed() *EmbedBuilder
- func (c *MsgCtx) React(emoji string) error
- func (c *MsgCtx) Reply(text string) error
- func (c *MsgCtx) ReplyEmbed(embed any) error
- func (c *MsgCtx) ReplyFile(content, filePath string) error
- func (c *MsgCtx) ReplyURL(content, url, fileName string) error
- func (c *MsgCtx) Send(text string) error
- func (c *MsgCtx) SendEmbed(embed any) error
- func (c *MsgCtx) SendEmbedWithButtons(embed any, components ...discord.InteractiveComponent) error
- type MsgHandler
- type PlaybackState
- type PrefixBuilder
- func (p *PrefixBuilder) Aliases(names ...string) *PrefixBuilder
- func (p *PrefixBuilder) BoolArg(name, description string, required bool) *PrefixBuilder
- func (p *PrefixBuilder) Handle(h PrefixHandler) *PrefixBuilder
- func (p *PrefixBuilder) IntArg(name, description string, required bool) *PrefixBuilder
- func (p *PrefixBuilder) RequireSameVoice() *PrefixBuilder
- func (p *PrefixBuilder) StringArg(name, description string, required bool) *PrefixBuilder
- type PrefixCtx
- type PrefixHandler
- type RateLimitConfig
- type Track
- type TrackKind
- type VoiceCtx
- func (v *VoiceCtx) Bot() *Bot
- func (v *VoiceCtx) ClearQueue()
- func (v *VoiceCtx) Cursor() int
- func (v *VoiceCtx) Disgo() voice.Conn
- func (v *VoiceCtx) Enqueue(t Track) (position int, started bool, err error)
- func (v *VoiceCtx) InsertNext(t Track) (position int, started bool, err error)
- func (v *VoiceCtx) InsertNextYouTube(url string) (firstPos, added int, started bool, err error)
- func (v *VoiceCtx) IsPlaying() bool
- func (v *VoiceCtx) JumpTo(index int) (Track, error)
- func (v *VoiceCtx) Leave() error
- func (v *VoiceCtx) Next() (Track, bool, error)
- func (v *VoiceCtx) Now() (Track, bool)
- func (v *VoiceCtx) Pause() error
- func (v *VoiceCtx) PlayFile(path string) (position int, started bool, err error)
- func (v *VoiceCtx) PlayYouTube(url string) (firstPos, added int, started bool, err error)
- func (v *VoiceCtx) Prev() (Track, error)
- func (v *VoiceCtx) Queue() []Track
- func (v *VoiceCtx) Reconnect() error
- func (v *VoiceCtx) Resume() error
- func (v *VoiceCtx) SetAnnounceChannel(channelID string) *VoiceCtx
- func (v *VoiceCtx) Skip() (Track, bool, error)
- func (v *VoiceCtx) Source() string
- func (v *VoiceCtx) State() PlaybackState
- func (v *VoiceCtx) Stop() error
- type VoiceManager
- type YTSearchMode
Constants ¶
const ( IntentsAll = gateway.IntentsAll IntentsNonPrivileged = gateway.IntentsNonPrivileged IntentsPrivileged = gateway.IntentsPrivileged IntentsNone = gateway.IntentsNone )
Re-exported intent bundles. Pass these to WithIntents() to control which gateway events the bot subscribes to.
Note: disgo splits intents into privileged (members, presences, message content) and non-privileged groups. IntentsAll OR's both together so the bot subscribes to everything; this requires the privileged intents to be enabled in the Developer Portal.
const ( EmbedTitleMaxLen = 256 EmbedDescriptionMaxLen = 4096 EmbedFieldNameMaxLen = 256 EmbedFieldValueMaxLen = 1024 EmbedAuthorNameMaxLen = 256 EmbedFieldsMax = 25 )
Discord embed limits, surfaced as constants so callers can clamp values before hitting the API. https://discord.com/developers/docs/resources/message
Variables ¶
var ( // ErrBotNotStarted is returned by APIs that require a live gateway // connection (most voice operations) when invoked before Bot.Start(). ErrBotNotStarted = errors.New("sikasa: bot not started yet") // ErrInvalidGuildID is returned when a guild snowflake string fails // to parse. Wrapped with the offending value for context. ErrInvalidGuildID = errors.New("sikasa: invalid guild id") // ErrInvalidChannelID is returned when a channel snowflake string // fails to parse. Wrapped with the offending value for context. ErrInvalidChannelID = errors.New("sikasa: invalid channel id") // ErrNotInVoice is returned by voice operations that require an // active voice connection for the guild but find none. ErrNotInVoice = errors.New("sikasa: not connected to a voice channel") // ErrNoAudio is returned when a playback control method (Pause) // runs while nothing is playing. ErrNoAudio = errors.New("sikasa: no audio playing") // ErrNotPaused is returned by Resume() when the stream is not in // the paused state. ErrNotPaused = errors.New("sikasa: not paused") // ErrEmptyToken is returned by New() when the supplied token is "". ErrEmptyToken = errors.New("sikasa: empty token") // ErrUnknownCommand is the sentinel for prefix-dispatch lookups that // fail. The dispatcher renders it to the user as a reply. ErrUnknownCommand = errors.New("sikasa: unknown prefix command") // ErrMissingArg signals that a required builder argument was not // supplied in the user's message. ErrMissingArg = errors.New("sikasa: missing required argument") // ErrInvalidArg signals that a supplied argument failed type parsing // (e.g. IntArg given non-numeric text). ErrInvalidArg = errors.New("sikasa: invalid argument value") // ErrQueueEmpty is returned when a queue navigation method (Next, Prev, // Skip) has nothing more to advance to. ErrQueueEmpty = errors.New("sikasa: queue is empty") // ErrNoPrevious is returned by Prev() when the queue cursor is already // at the first track. ErrNoPrevious = errors.New("sikasa: no previous track") // ErrNotSameChannel is returned by guards that require the invoking user // to be in the same voice channel as the bot. ErrNotSameChannel = errors.New("sikasa: you must be in the same voice channel as the bot") )
Functions ¶
func IsHTTPURL ¶
IsHTTPURL is a cheap heuristic that decides whether a user-supplied string should be treated as a URL or a search query. Anything starting with "http://" or "https://" is considered a URL; everything else is search text.
params:
s: the raw user input
returns:
bool: true when s looks like an HTTP(S) URL
Types ¶
type Bot ¶
type Bot struct {
// contains filtered or unexported fields
}
Bot is the high-level wrapper around disgo's bot.Client.
Key Fields:
- token: raw bot token; consumed only at Start()
- intents: gateway intents bitmask
- guildID: optional dev-guild for instant command sync; zero means global
- cmds: registered command builders, flushed to Discord on Start()
- kws: registered keyword matchers, evaluated on every MessageCreate
- prefix: global text prefix that triggers PrefixBuilder dispatch; empty string disables prefix routing entirely
- prefixes: registered prefix command builders
- prefixIndex: lookup table built at Start(); keys include both names and aliases (lower-cased)
- client: the live disgo client; nil until Start() succeeds
- voices: per-guild voice contexts, keyed by guild ID
- slog: structured logger handed to disgo (gateway, voice, REST)
Note: Not safe for concurrent registration; build all commands and keywords before calling Start(). After Start, the underlying client is goroutine-safe.
func New ¶
New constructs a Bot with the given token. The "Bot " prefix is added by disgo internally, so pass the raw token from the Developer Portal.
params:
token: the Discord bot token from the Developer Portal
returns:
*Bot: a configured Bot ready for command/keyword registration
error: reserved for future validation; currently always nil
func (*Bot) Command ¶
func (b *Bot) Command(name, description string) *CommandBuilder
Command registers a new slash command and returns its builder. Chain arg helpers and Handle() to finish the definition.
params:
name: the command name as it appears after the slash
description: the user-visible description
returns:
*CommandBuilder: builder for further configuration
func (*Bot) ConsumeYTSearch ¶
ConsumeYTSearch atomically looks up a session and removes it from the registry. Picker handlers should consume on success or cancel so the same button cannot fire twice.
params:
id: session id
returns:
*ytSearchSession: the session, or nil if missing/expired
func (*Bot) Disgo ¶
Disgo returns the underlying *bot.Client as an escape hatch for features the wrapper does not cover (sharding, manual REST calls, advanced events, etc). Returns nil before Start() has been called.
returns:
*bot.Client: the live disgo client, or nil if the bot has not started
func (*Bot) NewYTSearchSession ¶
NewYTSearchSession registers a picker session and returns its id. Use the returned id to build button customIDs in the form "sikasa:ytsearch:<id>:<idx>".
params:
invoker: user who triggered the search; only this user may click
guildID: guild where the search was issued (must be non-zero)
tracks: candidate tracks to display
returns:
string: session id
NewYTSearchSession registers a picker session and returns its id. Use the returned id to build button customIDs in the form "sikasa:ytsearch:<id>:<idx>". Mode defaults to YTSearchModeEnqueue.
params:
invoker: user who triggered the search; only this user may click
guildID: guild where the search was issued (must be non-zero)
tracks: candidate tracks to display
returns:
string: session id
func (*Bot) NewYTSearchSessionMode ¶
func (b *Bot) NewYTSearchSessionMode(invoker, guildID snowflake.ID, tracks []Track, mode YTSearchMode) string
NewYTSearchSessionMode is NewYTSearchSession with an explicit hand-off mode. Use YTSearchModeInsertNext to wire a search picker into a "play next" prefix command without rebuilding the dispatcher.
params:
invoker, guildID, tracks: see NewYTSearchSession
mode: how the click handler should hand the choice off to voice
returns:
string: session id
func (*Bot) OnButton ¶
func (b *Bot) OnButton(pattern string, h ButtonHandler) *Bot
OnButton registers a handler for button clicks whose customID matches the given chi-style pattern (e.g. "/sikasa/ytsearch/{session}/{idx}"). Path variables are accessible from the ButtonCtx via Var(name) or Vars().
params:
pattern: customID pattern, supports {var} placeholders
h: handler invoked with a *ButtonCtx
returns:
*Bot: receiver, for chaining
Note: Must be called before Bot.Start(). Handlers registered after Start() will not be wired into the live router.
func (*Bot) OnKeyword ¶
func (b *Bot) OnKeyword(terms ...string) *KeywordBuilder
OnKeyword registers a substring-based keyword rule. Matching is case-insensitive and uses Contains semantics, so "hello" matches "hello world" and "Well, Hello!" alike.
params:
terms: one or more substrings; the rule fires if ANY are present
returns:
*KeywordBuilder: builder for attaching a handler via Reply()
func (*Bot) OnPrefix ¶
func (b *Bot) OnPrefix(name, description string) *PrefixBuilder
OnPrefix registers a new prefix command and returns its builder. Chain arg helpers and Handle() to finish the definition.
params:
name: the command name (without the prefix)
description: a short user-visible description
returns:
*PrefixBuilder: builder for further configuration
func (*Bot) OnRegex ¶
func (b *Bot) OnRegex(pattern string) *KeywordBuilder
OnRegex registers a regex-based message rule. The pattern is compiled once at registration; an invalid pattern panics here rather than later inside the message dispatcher.
params:
pattern: a Go regexp/RE2 pattern
returns:
*KeywordBuilder: builder for attaching a handler via Reply()
func (*Bot) SetPrefix ¶
SetPrefix sets the global text prefix that triggers PrefixBuilder dispatch. An empty string disables prefix dispatch entirely (the default).
params:
p: the prefix string (e.g. "!", "?", "k!")
returns:
*Bot: receiver, for chaining
func (*Bot) Start ¶
Start builds the disgo client, opens the gateway, syncs slash commands, and wires up keyword/message dispatchers. Returns once the gateway handshake is complete; events run in disgo-managed goroutines from there.
returns:
error: if client construction, gateway open, or command sync fails
func (*Bot) Stop ¶
Stop closes voice connections and the gateway. Always call this via defer after Start().
returns:
error: reserved for future error paths; currently always nil
func (*Bot) Voice ¶
func (b *Bot) Voice() *VoiceManager
Voice returns the bot's VoiceManager, the entry point for joining voice channels and starting playback.
returns:
*VoiceManager: helper for voice channel operations
func (*Bot) WithGuild ¶
WithGuild scopes slash command registration to a single guild. Per-guild commands sync instantly, which is ideal during development. Leave unset for global commands (which can take up to an hour to propagate).
params:
guildID: the Discord guild snowflake; accepts the same string form
that the Developer Portal and Discord client display
returns:
*Bot: receiver, for chaining
func (*Bot) WithIntents ¶
WithIntents sets the gateway intents. Must be called before Start().
params:
intents: bitmask of gateway.Intent values
returns:
*Bot: receiver, for chaining
func (*Bot) WithLogger ¶
WithLogger swaps the default logger. Pass nil to silence output.
params:
l: standard library logger
returns:
*Bot: receiver, for chaining
func (*Bot) WithSlog ¶
WithSlog sets the structured logger that gets handed to disgo (gateway, voice, REST). Use this to surface heartbeat warnings, voice state changes, and DAVE/E2EE handshake details. Pass nil to disable.
params:
l: a *slog.Logger; level controls verbosity
returns:
*Bot: receiver, for chaining
func (*Bot) WithVerbose ¶
WithVerbose enables debug-level structured logging on stderr for both the sikasa wrapper and the underlying disgo client. Useful when diagnosing voice-region issues, slow interaction acks, or gateway zombies.
returns:
*Bot: receiver, for chaining
func (*Bot) YTSearchSession ¶
YTSearchSession looks up an active session by id without removing it. Returns nil when the id is unknown or the session has expired.
params:
id: session id from NewYTSearchSession
returns:
*ytSearchSession: live session, or nil
type ButtonCtx ¶
type ButtonCtx struct {
// contains filtered or unexported fields
}
ButtonCtx is the context passed to a Bot.OnButton handler.
Key Fields:
- bot: parent Bot, exposed via Bot()
- event: the disgo *handler.ComponentEvent, exposed via Event()
- data: the button interaction data
- vars: path variables extracted from the customID pattern
- deferred: tracks whether the handler has called DeferUpdate, so subsequent Update/UpdateEmbed/ClearComponents go through the followup-style UpdateInteractionResponse instead of trying to respond a second time
Note: ButtonCtx values are constructed by the dispatcher; do not build them yourself. The embedded *handler.ComponentEvent is exposed as an escape hatch for advanced use (followups, file uploads, modals).
func (*ButtonCtx) AuthorID ¶
AuthorID returns the snowflake of the user who clicked the button. Convenient for permission checks against a stored invokerID.
func (*ButtonCtx) ClearComponents ¶
ClearComponents edits the message to drop every action row, leaving the content and embeds untouched. Use it when the buttons should disappear but the surrounding context stays visible.
returns:
error: from disgo
func (*ButtonCtx) Data ¶
func (c *ButtonCtx) Data() discord.ButtonInteractionData
Data returns the underlying button interaction data.
func (*ButtonCtx) Defer ¶
Defer acks the interaction without sending a visible reply. Use this when the handler will follow up later (more than 3 seconds out).
params:
ephemeral: when true, any later followup is only shown to the clicker
returns:
error: from disgo
func (*ButtonCtx) DeferUpdate ¶
DeferUpdate acks the click within Discord's 3 second window without visibly changing the message. Use it before doing slow work (yt-dlp probe, voice handshake, etc.) that would otherwise leave the user staring at "this interaction failed". After this call, Update / UpdateEmbed / ClearComponents transparently target the deferred response.
returns:
error: from disgo
func (*ButtonCtx) Event ¶
func (c *ButtonCtx) Event() *handler.ComponentEvent
Event returns the underlying *handler.ComponentEvent for advanced flows.
func (*ButtonCtx) Reply ¶
Reply sends a new message in response to the button click. The message is ephemeral (only visible to the clicker) by default, since most button flows already have visible context in the parent message.
params:
text: the message body
returns:
error: from disgo
func (*ButtonCtx) ReplyEmbed ¶
ReplyEmbed sends an ephemeral embed reply. Accepts a discord.Embed or a *EmbedBuilder; pass the builder directly without calling .Build().
params:
embed: a discord.Embed or *EmbedBuilder
returns:
error: from disgo or an unsupported-type error
func (*ButtonCtx) ReplyPublic ¶
ReplyPublic sends a non-ephemeral message in response to the click.
params:
text: the message body
returns:
error: from disgo
func (*ButtonCtx) Update ¶
Update edits the message that owns the clicked button. Useful for swapping a picker into a confirmation. Pass an empty string to clear the content; existing embeds and components are preserved unless cleared explicitly via Event().UpdateMessage with the appropriate ClearX calls. After DeferUpdate has been called, this method edits the deferred response instead of issuing a fresh one.
params:
text: new message body
returns:
error: from disgo
func (*ButtonCtx) UpdateEmbed ¶
UpdateEmbed edits the message to display the given embed and clears all component rows so the picker disappears once a choice is made. Accepts a discord.Embed or a *EmbedBuilder. After DeferUpdate has been called, the edit is dispatched against the deferred response.
params:
embed: a discord.Embed or *EmbedBuilder
returns:
error: from disgo or an unsupported-type error
type ButtonHandler ¶
ButtonHandler is the signature every button handler must satisfy.
type CmdCtx ¶
type CmdCtx struct {
// contains filtered or unexported fields
}
CmdCtx is the context passed to a slash command handler.
Key Fields:
- bot: the parent Bot, exposed via Bot()
- event: the disgo CommandEvent, exposed via Event()
- data: the parsed slash command interaction data
func (*CmdCtx) Attachment ¶
func (c *CmdCtx) Attachment(name string) discord.Attachment
Attachment returns the uploaded attachment for the named option, or the zero Attachment if missing.
func (*CmdCtx) Author ¶
Author returns the user who invoked the command, regardless of whether the invocation happened in a guild or in DMs.
func (*CmdCtx) Channel ¶
func (c *CmdCtx) Channel(name string) discord.ResolvedChannel
Channel returns the picked channel for the named option, or the zero ResolvedChannel if missing.
func (*CmdCtx) Data ¶
func (c *CmdCtx) Data() discord.SlashCommandInteractionData
Data returns the parsed slash command interaction data, useful when the option helpers below are insufficient (e.g. iterating Resolved entities).
func (*CmdCtx) Defer ¶
Defer acknowledges the interaction with a "Bot is thinking…" placeholder. Use this when the handler needs more than 3 seconds to produce a result. After deferring, finish the response with Followup or by editing the interaction response directly.
params:
ephemeral: if true, the eventual response is visible only to the invoker
returns:
error: from disgo
func (*CmdCtx) Event ¶
func (c *CmdCtx) Event() *handler.CommandEvent
Event returns the underlying disgo *handler.CommandEvent as an escape hatch for advanced response patterns (UpdateInteractionResponse, file followups, etc).
func (*CmdCtx) Followup ¶
Followup sends a follow-up message after a Defer. Discord allows follow-ups for up to 15 minutes after the original interaction.
params:
text: the follow-up body
returns:
error: from disgo
func (*CmdCtx) GuildID ¶
GuildID returns the guild snowflake as a string, or empty for DM invocations.
func (*CmdCtx) Reply ¶
Reply sends a plain-text response to the interaction. Must run within 3 seconds of the command firing; otherwise use Defer + Followup.
params:
text: the message body
returns:
error: from disgo
func (*CmdCtx) ReplyEmbed ¶
ReplyEmbed sends an embed response.
params:
embed: a fully-built Embed
returns:
error: from disgo
func (*CmdCtx) ReplyEphemeral ¶
ReplyEphemeral is like Reply but the response is visible only to the invoker.
params:
text: the message body
returns:
error: from disgo
func (*CmdCtx) ReplyFile ¶
ReplyFile sends a local file as a response. The file is opened, attached, and closed by Discord after upload.
params:
content: optional message body
filePath: path on disk to the file to upload
returns:
error: if the file cannot be opened or the response cannot be sent
func (*CmdCtx) ReplyURL ¶
ReplyURL streams a remote file directly into the response without writing to disk. Useful for image/media APIs.
params:
content: optional message body
url: remote file URL to fetch via HTTP GET
fileName: name shown to the recipient
returns:
error: if the fetch fails or the upstream returns non-200
type CmdHandler ¶
CmdHandler is the signature every slash command handler must satisfy. Returning an error logs it via the bot's logger; it does not propagate further, since by the time the handler runs the user has already been shown the bot's response.
type CommandBuilder ¶
type CommandBuilder struct {
// contains filtered or unexported fields
}
CommandBuilder accumulates a slash command's definition and handler.
Key Fields:
- name, desc: surfaced to Discord
- opts: command options in the order added
- handler: invoked on InteractionApplicationCommand
Note: Builders are not thread-safe; finish configuration before Start().
func (*CommandBuilder) AttachmentArg ¶
func (c *CommandBuilder) AttachmentArg(name, description string, required bool) *CommandBuilder
AttachmentArg adds a file attachment option to the command. The uploaded file is resolvable via ctx.Attachment(name).
params:
name, description, required: see StringArg
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) BoolArg ¶
func (c *CommandBuilder) BoolArg(name, description string, required bool) *CommandBuilder
BoolArg adds a boolean option to the command.
params:
name, description, required: see StringArg
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) ChannelArg ¶
func (c *CommandBuilder) ChannelArg(name, description string, required bool) *CommandBuilder
ChannelArg adds a channel-picker option to the command.
params:
name, description, required: see StringArg
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) Handle ¶
func (c *CommandBuilder) Handle(h CmdHandler) *CommandBuilder
Handle attaches the handler that runs when the command is invoked. This finalizes the builder.
params:
h: the handler invoked with a *CmdCtx
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) IntArg ¶
func (c *CommandBuilder) IntArg(name, description string, required bool) *CommandBuilder
IntArg adds an integer option to the command.
params:
name, description, required: see StringArg
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) StringArg ¶
func (c *CommandBuilder) StringArg(name, description string, required bool) *CommandBuilder
StringArg adds a string option to the command.
params:
name: option name shown in the command picker
description: option description
required: if true, Discord rejects invocations missing this arg
returns:
*CommandBuilder: receiver, for chaining
func (*CommandBuilder) UserArg ¶
func (c *CommandBuilder) UserArg(name, description string, required bool) *CommandBuilder
UserArg adds a user-picker option to the command. The picked user is resolvable via ctx.User(name).
params:
name, description, required: see StringArg
returns:
*CommandBuilder: receiver, for chaining
type EmbedBuilder ¶
type EmbedBuilder struct {
// contains filtered or unexported fields
}
EmbedBuilder is a fluent wrapper over discord.Embed. Methods mutate the receiver and return it, so chains compile to a single Embed value.
Note: Constructor functions return *EmbedBuilder so chains stay nil-safe even if zero-value method calls are accidentally inlined. Treat the builder as scoped to a single message; do not reuse a builder across concurrent goroutines.
func BuildYTSearchEmbed ¶
func BuildYTSearchEmbed(query, sessionID string, tracks []Track) (*EmbedBuilder, []discord.InteractiveComponent)
BuildYTSearchEmbed renders a picker embed plus a button ActionRow for the given candidate tracks. The customIDs encode the session id and a 0-based index, matching the route Bot.OnButton handlers should listen on.
params:
query: the original search string, shown in the embed title
sessionID: id returned by NewYTSearchSession
tracks: same slice passed into the session
returns:
*EmbedBuilder: ready to pass to ReplyEmbed/SendEmbed
[]discord.ButtonComponent: one button per track, plus a Cancel button
func NewEmbed ¶
func NewEmbed() *EmbedBuilder
NewEmbed returns a fresh builder. Identical to MsgCtx.NewEmbed() and CmdCtx.NewEmbed(); exposed at package level so callers can build embeds outside of a context.
func (*EmbedBuilder) Author ¶
func (b *EmbedBuilder) Author(name, iconURL, url string) *EmbedBuilder
Author sets the small author block at the top of the embed.
params:
name: display name (max 256 chars; required for the block to render)
iconURL: optional thumbnail beside the name; pass "" to omit
url: optional clickable link on the name; pass "" to omit
func (*EmbedBuilder) Build ¶
func (b *EmbedBuilder) Build() discord.Embed
Build returns the underlying discord.Embed. Most callers do not need this directly: ReplyEmbed / SendEmbed accept an EmbedBuilder via Build() internally. Exposed for callers that need to mix sikasa embeds with raw disgo APIs.
func (*EmbedBuilder) ClearFields ¶
func (b *EmbedBuilder) ClearFields() *EmbedBuilder
ClearFields removes any previously-added fields. Useful when reusing a builder template across multiple embeds.
func (*EmbedBuilder) Color ¶
func (b *EmbedBuilder) Color(c int) *EmbedBuilder
Color sets the left-hand stripe color. Accepts a hex int like 0x5865F2.
func (*EmbedBuilder) Description ¶
func (b *EmbedBuilder) Description(s string) *EmbedBuilder
Description sets the embed description (max 4096 chars).
func (*EmbedBuilder) Field ¶
func (b *EmbedBuilder) Field(name, value string, inline bool) *EmbedBuilder
Field appends a name/value field. Discord caps at 25 fields per embed; extra fields beyond the cap are silently dropped on the API side.
params:
name: field heading (max 256 chars)
value: field body (max 1024 chars)
inline: render side-by-side with adjacent inline fields
func (*EmbedBuilder) Footer ¶
func (b *EmbedBuilder) Footer(text, iconURL string) *EmbedBuilder
Footer sets the small footer text below the body.
params:
text: footer text (max 2048 chars; required for the block to render)
iconURL: optional icon beside the footer; pass "" to omit
func (*EmbedBuilder) Image ¶
func (b *EmbedBuilder) Image(url string) *EmbedBuilder
Image sets the large image rendered below the body.
func (*EmbedBuilder) Now ¶
func (b *EmbedBuilder) Now() *EmbedBuilder
Now sets the timestamp to the current wall-clock time.
func (*EmbedBuilder) Thumbnail ¶
func (b *EmbedBuilder) Thumbnail(url string) *EmbedBuilder
Thumbnail sets the small image rendered in the top-right corner.
func (*EmbedBuilder) Timestamp ¶
func (b *EmbedBuilder) Timestamp(t time.Time) *EmbedBuilder
Timestamp sets the small timestamp shown in the footer.
func (*EmbedBuilder) Title ¶
func (b *EmbedBuilder) Title(s string) *EmbedBuilder
Title sets the embed title (max 256 chars).
func (*EmbedBuilder) URL ¶
func (b *EmbedBuilder) URL(u string) *EmbedBuilder
URL makes the title clickable, pointing to the given URL.
type Intents ¶
Intents is an alias of disgo's gateway.Intents so callers do not need to import the gateway package directly for common cases.
type KeywordBuilder ¶
type KeywordBuilder struct {
// contains filtered or unexported fields
}
KeywordBuilder accumulates a keyword rule and its handler.
Key Fields:
- terms: substrings to match against (lower-cased on the message side)
- regex: compiled regex; mutually exclusive with terms in practice
- handler: invoked when a match is found
func (*KeywordBuilder) Reply ¶
func (k *KeywordBuilder) Reply(h MsgHandler, limits ...RateLimitConfig) *KeywordBuilder
Reply attaches the handler that runs when this rule matches.
params:
h: the handler invoked with a *MsgCtx
limits: optional rate limit config (e.g. sikasa.RateLimitInterval(3, time.Minute))
returns:
*KeywordBuilder: receiver, for chaining
func (*KeywordBuilder) ReplyFile ¶
func (k *KeywordBuilder) ReplyFile(content, filePath string, limits ...RateLimitConfig) *KeywordBuilder
ReplyFile is a shortcut for replying with a local file attachment.
params:
content: optional message body sent alongside the file
filePath: path to a file on disk
limits: optional rate limit config
returns:
*KeywordBuilder: receiver, for chaining
func (*KeywordBuilder) ReplyText ¶
func (k *KeywordBuilder) ReplyText(text string, limits ...RateLimitConfig) *KeywordBuilder
ReplyText is a shortcut for fixed text replies. The reply uses Discord's inline reply (message reference) so it links back to the user's message.
params:
text: the message body to send
limits: optional rate limit config
returns:
*KeywordBuilder: receiver, for chaining
type MsgCtx ¶
type MsgCtx struct {
// contains filtered or unexported fields
}
MsgCtx is the context passed to a keyword or regex handler.
Key Fields:
- bot: the parent Bot, exposed via Bot()
- event: the disgo MessageCreate event, exposed via Event()
func (*MsgCtx) AuthorMention ¶
AuthorMention returns the formatted mention string for the author.
func (*MsgCtx) Event ¶
func (c *MsgCtx) Event() *events.MessageCreate
Event returns the underlying disgo *events.MessageCreate as an escape hatch.
func (*MsgCtx) NewEmbed ¶
func (c *MsgCtx) NewEmbed() *EmbedBuilder
NewEmbed returns a fluent EmbedBuilder. Chain its setters and pass the result to ReplyEmbed / SendEmbed; you do not need to call .Build().
returns:
*EmbedBuilder: ready for .Title()/.Description()/.Field()/...
func (*MsgCtx) React ¶
React adds a reaction emoji to the user's message.
params:
emoji: a unicode emoji or a custom emoji in "name:id" format
returns:
error: from disgo
func (*MsgCtx) Reply ¶
Reply sends a plain-text inline reply to the user's message.
params:
text: the message body
returns:
error: from disgo
func (*MsgCtx) ReplyEmbed ¶
ReplyEmbed sends an embed as an inline reply. Accepts either a built discord.Embed or a sikasa *EmbedBuilder; pass the builder directly without calling .Build() yourself.
params:
embed: a discord.Embed or *EmbedBuilder
returns:
error: from disgo, or an error if the embed type is unsupported
func (*MsgCtx) ReplyFile ¶
ReplyFile sends a local file as an inline reply. The file is opened, attached, and closed automatically.
params:
content: optional message body sent alongside the file
filePath: path on disk to the file to upload
returns:
error: if the file cannot be opened or the message cannot be sent
func (*MsgCtx) ReplyURL ¶
ReplyURL streams a remote file directly into the reply without writing to disk.
params:
content: optional message body
url: remote file URL to fetch via HTTP GET
fileName: name shown to the recipient
returns:
error: if the fetch fails or the upstream returns non-200
func (*MsgCtx) Send ¶
Send posts a plain-text message to the same channel without referencing the user's message. Use this when the response is informational rather than a direct reply.
params:
text: the message body
returns:
error: from disgo
func (*MsgCtx) SendEmbed ¶
SendEmbed posts an embed to the same channel without referencing the user's message. Use it for informational announcements (queue listing, status panels) where an inline reply marker would be visual noise. Accepts either a discord.Embed or a *EmbedBuilder.
params:
embed: a discord.Embed or *EmbedBuilder
returns:
error: from disgo, or an error if the embed type is unsupported
func (*MsgCtx) SendEmbedWithButtons ¶
func (c *MsgCtx) SendEmbedWithButtons(embed any, components ...discord.InteractiveComponent) error
SendEmbedWithButtons posts an embed plus a single ActionRow of interactive components (typically buttons returned from BuildYTSearchEmbed or any custom picker). For multi-row component layouts, use Event() and build the message manually.
params:
embed: a discord.Embed or *EmbedBuilder
components: interactive components (max 5 per Discord ActionRow)
returns:
error: from disgo or an unsupported-embed-type error
type MsgHandler ¶
MsgHandler is the signature every message handler must satisfy.
type PlaybackState ¶
type PlaybackState int32
PlaybackState describes the current state of a VoiceCtx.
const ( StateIdle PlaybackState = iota StatePlaying StatePaused StateStopped )
type PrefixBuilder ¶
type PrefixBuilder struct {
// contains filtered or unexported fields
}
PrefixBuilder accumulates a prefix command's definition and handler.
Key Fields:
- name, desc: user-visible name and description (description currently unused but reserved for a future help generator)
- aliases: extra names that resolve to this builder
- args: option list, parsed in registration order
- handler: invoked when a message matches the command
- requireSameVoice: when true, dispatch rejects callers who are not in the same voice channel as the bot for the guild
Note: Builders are not thread-safe; finish configuration before Start().
func (*PrefixBuilder) Aliases ¶
func (p *PrefixBuilder) Aliases(names ...string) *PrefixBuilder
Aliases attaches alternative names that dispatch to the same handler. For example, .Aliases("p", "pl") on a "play" command lets users type "!p" or "!pl" interchangeably.
params:
names: one or more alias strings
returns:
*PrefixBuilder: receiver, for chaining
func (*PrefixBuilder) BoolArg ¶
func (p *PrefixBuilder) BoolArg(name, description string, required bool) *PrefixBuilder
BoolArg adds a boolean option. Accepted truthy values: "true", "1", "yes", "y", "on". Falsy: "false", "0", "no", "n", "off". Anything else triggers ErrInvalidArg.
params:
name, description, required: see CommandBuilder.BoolArg
returns:
*PrefixBuilder: receiver, for chaining
func (*PrefixBuilder) Handle ¶
func (p *PrefixBuilder) Handle(h PrefixHandler) *PrefixBuilder
Handle attaches the handler that runs when this command is invoked.
params:
h: the handler invoked with a *PrefixCtx
returns:
*PrefixBuilder: receiver, for chaining
func (*PrefixBuilder) IntArg ¶
func (p *PrefixBuilder) IntArg(name, description string, required bool) *PrefixBuilder
IntArg adds an integer option to the command. Values are parsed via strconv.ParseInt; invalid values trigger ErrInvalidArg.
params:
name, description, required: see CommandBuilder.IntArg
returns:
*PrefixBuilder: receiver, for chaining
func (*PrefixBuilder) RequireSameVoice ¶
func (p *PrefixBuilder) RequireSameVoice() *PrefixBuilder
RequireSameVoice gates the command on the invoking user being in the same voice channel as the bot for that guild. When the bot is not in any voice channel, the command is allowed (so initial join commands like "play" can still run).
returns:
*PrefixBuilder: receiver, for chaining
func (*PrefixBuilder) StringArg ¶
func (p *PrefixBuilder) StringArg(name, description string, required bool) *PrefixBuilder
StringArg adds a string option to the command. The last StringArg in the declaration order consumes the entire remaining message tail, so a single trailing StringArg captures free-form text like "echo hello world".
params:
name, description, required: see CommandBuilder.StringArg
returns:
*PrefixBuilder: receiver, for chaining
type PrefixCtx ¶
type PrefixCtx struct {
*MsgCtx
// contains filtered or unexported fields
}
PrefixCtx is the context passed to a prefix command handler.
Key Fields:
- MsgCtx: embedded; provides Bot(), Event(), Reply(), Send(), etc.
- name: resolved canonical command name (Aliases are normalized away)
- args: parsed builder args, keyed by the names declared in StringArg / IntArg / BoolArg
- raw: every whitespace-separated token after the command name
- rest: the entire message tail after the command name, with original whitespace preserved
func (*PrefixCtx) Arg ¶
Arg returns the i-th token after the command name, or "" if i is out of range. Convenient when you only care about positions.
func (*PrefixCtx) Args ¶
Args returns every whitespace-separated token after the command name. Use this when builder args do not fit the parsing shape you need (e.g. a variadic list of user IDs).
func (*PrefixCtx) Bool ¶
Bool returns the value of a named BoolArg.
params:
name: the arg name as declared via BoolArg
returns:
bool: the parsed value, or false if absent
func (*PrefixCtx) Int ¶
Int returns the value of a named IntArg.
params:
name: the arg name as declared via IntArg
returns:
int64: the parsed value, or 0 if absent
func (*PrefixCtx) Name ¶
Name returns the canonical command name that matched, even if the user invoked the command via an alias.
type PrefixHandler ¶
PrefixHandler is the signature every prefix command handler must satisfy.
type RateLimitConfig ¶
RateLimitConfig represents the configuration for a rate limit. It specifies how many times an action can be performed within a given duration.
func RateLimitInterval ¶
func RateLimitInterval(count int, interval time.Duration) RateLimitConfig
RateLimitInterval is a helper to construct a RateLimitConfig.
params:
count: maximum number of allowed requests
interval: time window in which requests are counted
returns:
RateLimitConfig
type Track ¶
Track is a single queue entry.
Key Fields:
- Kind: selects the spawn strategy when this track plays
- Source: file path (TrackFile) or URL (TrackYouTube); used as the fallback label when Title is not populated
- Title: optional human-readable title; populated by metadata probes for TrackYouTube. Empty for TrackFile by default
- Author: optional uploader / channel / artist; populated alongside Title
func SearchYouTube ¶
SearchYouTube runs `ytsearch<n>:<query>` through yt-dlp and returns the top-N results as Tracks. Useful for an interactive picker; for direct enqueue use PlayYouTube which expands either a URL or a search.
params:
query: free-text search string (yt-dlp escapes it internally)
n: number of results to return; clamped to [1, 25]
returns:
[]Track: candidate tracks in yt-dlp's relevance order
error: yt-dlp invocation error (timeout, missing binary, etc.)
Note: 15s timeout is enough for top-25 search; raise via env if you need deeper results. Search uses --flat-playlist so we get URL+title+uploader quickly without fetching each video's full metadata.
type VoiceCtx ¶
type VoiceCtx struct {
// contains filtered or unexported fields
}
VoiceCtx is the per-guild voice handle. All playback control flows through it.
Key Fields:
- conn: the underlying disgo voice.Conn
- state: atomic state (Idle / Playing / Paused / Stopped)
- provider: the active OpusFrameProvider; swapped on track change
- source: human-readable description of what is currently loaded
- queue: per-guild track list and cursor; auto-advances on EOF
- announceChannelID: optional channel where auto-advance announcements go; zero means announcements are disabled
Note: Methods are safe to call concurrently. The provider, source, queue, and announceChannelID fields are guarded by the mutex; state is atomic so quick reads (IsPlaying, State) are lock-free. Each guild gets its own VoiceCtx via Bot.voices, so queues are naturally isolated per session.
func (*VoiceCtx) ClearQueue ¶
func (v *VoiceCtx) ClearQueue()
ClearQueue empties the queue without affecting the currently playing track. Combine with Stop() to halt playback and reset state in one shot.
func (*VoiceCtx) Cursor ¶
Cursor returns the index of the currently playing track, or -1 if nothing has started yet.
func (*VoiceCtx) Disgo ¶
Disgo returns the underlying voice.Conn as an escape hatch for advanced operations (custom OpusFrameReceiver, raw UDP writes, etc).
func (*VoiceCtx) Enqueue ¶
Enqueue adds a Track to the queue. If nothing is currently playing the queue is advanced to this track and playback begins. Otherwise the track simply waits its turn.
params:
t: the track to enqueue
returns:
position: 0-based index of the new track in the queue
started: true if this call kicked off playback
error: spawn error if started==true and the pipeline fails
func (*VoiceCtx) InsertNext ¶
InsertNext inserts a track immediately after the currently playing one, shifting any later tracks down by one. When nothing is playing yet, the track lands at index 0 and playback starts. Useful for "play next" UX without disturbing what is currently audible.
params:
t: track to insert
returns:
position: 0-based index of the newly inserted track
started: true if this call kicked off playback (queue was idle)
error: spawn error when started==true and the pipeline fails
func (*VoiceCtx) InsertNextYouTube ¶
InsertNextYouTube probes a yt-dlp-resolvable URL (single video, playlist, channel) and inserts every resulting track immediately after the currently playing one in playlist order. Mirror of PlayYouTube's expand-then-enqueue flow but inserts instead of appending. Probe failure falls back to inserting the raw URL as a single track.
params:
url: any URL yt-dlp can resolve
returns:
firstPos: index of the first inserted track
added: number of tracks inserted (>=1)
started: true if the call kicked off playback (queue was idle)
error: spawn error when started==true and the pipeline fails
func (*VoiceCtx) JumpTo ¶
JumpTo moves the queue cursor to a specific 0-based index and plays that track. Out-of-range indices return ErrQueueEmpty without changing state.
params:
index: 0-based queue position to jump to
returns:
Track: the track that started playing
error: ErrQueueEmpty if index is out of range, or a spawn error
func (*VoiceCtx) Leave ¶
Leave closes the voice connection and removes this context from the bot's registry. After calling Leave, this VoiceCtx must not be reused.
returns:
error: always nil currently; reserved for future error paths
func (*VoiceCtx) Now ¶
Now returns the currently loaded track and true, or a zero Track and false if the queue has not started yet.
func (*VoiceCtx) Pause ¶
Pause stops sending audio frames. The FFmpeg process and parser stay alive, so Resume() picks up exactly where playback left off.
returns:
error: if no audio is currently playing
func (*VoiceCtx) PlayFile ¶
PlayFile enqueues a local audio file. If nothing is currently playing the track starts immediately; otherwise it lands at the tail of the queue and auto-plays when the current track ends. Codec detection (.opus / .ogg passthrough vs. transcode) happens at spawn time, not enqueue time.
params:
path: filesystem path to the audio file
returns:
position: 0-based index of the track in the queue
started: true if this call began playback (queue was idle)
error: only if the queue manipulation fails (currently never)
func (*VoiceCtx) PlayYouTube ¶
PlayYouTube enqueues a yt-dlp-resolvable URL. Same enqueue-or-play semantics as PlayFile, with one extra wrinkle: playlist URLs are expanded into N tracks via yt-dlp --flat-playlist, so a single PlayYouTube call can append many queue entries at once. Probe failure is non-fatal; on error the raw URL is enqueued as a single track and the caller will see it as the bare link.
params:
url: any URL yt-dlp can resolve (single video, playlist, channel, ...)
returns:
firstPos: 0-based queue index of the first track added
added: number of tracks appended (1 for a single video, N for
a playlist)
started: true if this call began playback (queue was idle and the
first new track is now playing)
error: only if the queue manipulation fails (currently never)
func (*VoiceCtx) Prev ¶
Prev rewinds the queue cursor by one and plays that track. If the queue has been exhausted (cursor sits past the last track because the last track finished naturally), Prev jumps back to the last track instead of going all the way to the first. Returns ErrNoPrevious only when the queue is empty or the cursor is genuinely at index 0.
returns:
Track: the track that started playing
error: ErrNoPrevious or a spawn error
func (*VoiceCtx) Queue ¶
Queue returns a snapshot of the queued tracks. The current track (if any) is included at index Cursor().
func (*VoiceCtx) Reconnect ¶
Reconnect tears down the current voice connection and reopens one to the same channel, then resumes playback from the current track. Used to recover from DAVE epoch desync ("no active epoch" errors) where the connection remains physically open but encryption fails on every packet. The queue and cursor are preserved; only the audio handshake gets a fresh start.
returns:
error: if the channel cannot be re-resolved or the new handshake fails
Note: Resume from mid-track is not supported. The current track restarts from the beginning because FFmpeg has been torn down with the connection.
func (*VoiceCtx) Resume ¶
Resume continues a paused stream.
returns:
error: if the state is not Paused
func (*VoiceCtx) SetAnnounceChannel ¶
SetAnnounceChannel routes auto-advance announcements ("next track: ...", "queue ended") to the given Discord channel ID. Pass "" to disable. Manual Skip/Prev/Jump do not announce here, since the prefix handler that triggered them already replies in-place.
params:
channelID: snowflake of the text channel to post to, or "" to disable
returns:
*VoiceCtx: receiver, for chaining
func (*VoiceCtx) Skip ¶
Skip stops the current track and plays the next one in the queue. If the queue has no further tracks the connection stays open but playback ends.
returns:
Track: the track that started playing (zero value if queue exhausted)
bool: true if a new track started, false if the queue is exhausted
error: spawn error if the next track fails to start
func (*VoiceCtx) Source ¶
Source returns a human-readable description of the current source (file path or URL); empty if nothing has been played yet.
func (*VoiceCtx) State ¶
func (v *VoiceCtx) State() PlaybackState
State returns the current playback state.
func (*VoiceCtx) Stop ¶
Stop halts playback and tears down the FFmpeg pipeline. The voice connection itself stays open so the next Play* call can reuse it. The queue is left intact; call ClearQueue() too if you want a fresh session.
returns:
error: always nil currently; reserved for future error paths
type VoiceManager ¶
type VoiceManager struct {
// contains filtered or unexported fields
}
VoiceManager is returned by Bot.Voice(). It is the entry point for joining voice channels and tracks active VoiceCtx values per guild.
func (*VoiceManager) Get ¶
func (m *VoiceManager) Get(guildID string) *VoiceCtx
Get returns the active VoiceCtx for a guild, or nil if the bot is not connected to any voice channel there.
func (*VoiceManager) Join ¶
func (m *VoiceManager) Join(guildID, channelID string) (*VoiceCtx, error)
Join connects to a voice channel and returns a VoiceCtx for playback control. If the bot is already in a voice channel for the same guild, that connection is moved to the new channel via UpdateVoiceState instead of opening a second one.
params:
guildID: the guild snowflake the voice channel belongs to
channelID: the voice channel to join
returns:
*VoiceCtx: a handle for playback control
error: if the gateway voice handshake fails
type YTSearchMode ¶
type YTSearchMode int
YTSearchMode tags how the picker should hand off to the voice queue when a candidate is selected. Embedded in the session so a single OnButton route can drive both "enqueue" (k!yt) and "insert next" (k!insert) flows without separate customID namespaces.
const ( // YTSearchModeEnqueue appends the chosen track to the queue tail. YTSearchModeEnqueue YTSearchMode = iota // YTSearchModeInsertNext inserts the chosen track right after the // currently playing one, shifting later tracks down. YTSearchModeInsertNext )