sikasa

package module
v0.0.0-...-e994150 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: MIT Imports: 29 Imported by: 0

README

Sikasa

Sikasa (see-KAH-sah) is a high-level, fluent wrapper around the modern disgoorg/disgo library. It eliminates boilerplate and simplifies common bot-building tasks while keeping disgo's full power one method call away.

Why Sikasa?

Writing a bot in raw disgo still involves repetitive boilerplate for things that should be simple:

  • Wiring slash command definitions, the handler.Router, and the option parser separately.
  • Building discord.MessageCreate chains for every reply variant.
  • Manually constructing MessageReference for inline replies.
  • The 15-line os.Open + defer Close() + AddFile dance just to send an image.

Sikasa solves this by providing a Builder pattern, context helpers (CmdCtx and MsgCtx), and automatic command registration on top of disgo.

Migration note: Sikasa originally wrapped bwmarrin/discordgo. It has been rewritten on top of disgo because Discord now enforces the DAVE (E2EE) protocol in many voice regions, which discordgo does not support (close code 4017). disgo ships native DAVE support via thomas-vilte/dave-go. The escape hatch was renamed from .DiscordGo() to .Disgo() and now returns a *bot.Client. Everything else in the public API is unchanged.

Features

  • Fluent Command Builder: Define slash commands, their arguments, and their handler in one place.
  • Auto-Sync: Atomically bulk-overwrites slash commands on startup via handler.SyncCommands.
  • Prefix Commands: Classic text-prefix routing (!play, !ping) with the same builder feel as slash commands.
  • Keyword & Regex Router: Easily respond to specific words or regex patterns in messages.
  • Context Helpers: One-liners for replying with text, embeds, local files, or fetching files from URLs.
  • Voice / Music with DAVE: Join voice channels and stream local files or YouTube URLs with a single call. End-to-end encryption is wired in for you.
  • Per-guild Queue: Built-in track queue with auto-advance, skip, prev, and clear. Each guild gets its own session, so multi-server playback is isolated by default.
  • Rate Limiting: Optional sliding-window rate limit on keyword replies.
  • Sentinel Errors: Exported Err* values so callers can branch on errors.Is(err, sikasa.ErrXxx).
  • Escape Hatches: Drop down to disgo via .Disgo(), .Event(), or .Data() if Sikasa doesn't cover your specific need.

Requirements

  • Go 1.22+
  • ffmpeg on PATH (only required for PlayFile / PlayYouTube)
  • yt-dlp on PATH (only required for PlayYouTube)

DAVE (E2EE voice) is built in via the pure-Go dave-go backend; no extra setup needed.

Install on common platforms:

# Linux (Debian/Ubuntu)
sudo apt install ffmpeg && pipx install yt-dlp

# macOS
brew install ffmpeg yt-dlp

# Windows
winget install Gyan.FFmpeg
winget install yt-dlp.yt-dlp

Installation

go get github.com/dlcuy22/sikasa

Quick Start

package main

import (
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/dlcuy22/sikasa"
)

func main() {
	// 1. Initialize Bot
	bot, err := sikasa.New(os.Getenv("DISCORD_TOKEN"))
	if err != nil {
		log.Fatal(err)
	}
	bot.WithIntents(sikasa.IntentsAll)

	// 2. Slash Command
	bot.Command("echo", "Echoes back the text you provide").
		StringArg("text", "The text to echo", true).
		Handle(func(ctx *sikasa.CmdCtx) error {
			return ctx.Reply(ctx.String("text"))
		})

	// 3. Keyword Detection
	bot.OnKeyword("hello", "hi").
		Reply(func(ctx *sikasa.MsgCtx) error {
			return ctx.Reply("Hello there, " + ctx.AuthorMention() + "!")
		})

	// 4. One-liner File Reply
	bot.OnKeyword("sikasa").
		ReplyFile("ongo", "media/pp.jpg")

	// 5. Start Bot
	if err := bot.Start(); err != nil {
		log.Fatal(err)
	}
	defer bot.Stop()

	log.Println("bot is running, ctrl-c to exit")
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
	<-sc
}

Core Concepts

Slash Commands (CmdCtx)

Sikasa provides CmdCtx which holds the interaction context.

bot.Command("avatar", "Get user avatar").
    UserArg("target", "User to get avatar for", false).
    Handle(func(ctx *sikasa.CmdCtx) error {
        user := ctx.User("target")
        if user.ID == 0 {
            user = ctx.Author() // Fallback to invoker
        }
        return ctx.ReplyURL("", user.EffectiveAvatarURL(), "avatar.png")
    })

Context Helpers:

  • ctx.Reply("text")
  • ctx.ReplyEphemeral("secret text")
  • ctx.ReplyFile("text", "path.png")
  • ctx.ReplyURL("text", "https://...", "image.png")
  • ctx.Defer(ephemeral) and ctx.Followup("text") for slow operations.
Keyword & Regex Routing (MsgCtx)

Instead of writing massive switch chains on every MessageCreate, register rules:

// Matches any message containing "help" (case-insensitive)
bot.OnKeyword("help").
    ReplyText("I am a simple bot. Try `/echo`")

// Matches complex patterns
bot.OnRegex(`(?i)^ping\s+\d+$`).
    Reply(func(ctx *sikasa.MsgCtx) error {
        return ctx.React("🏓")
    })

Context Helpers:

  • ctx.Reply("text") (Sends as an inline reply to the user)
  • ctx.Send("text") (Sends a normal message to the channel)
  • ctx.ReplyFile(), ctx.ReplyURL(), ctx.React("👍")
  • ctx.NewEmbed() returns a fluent *EmbedBuilder; pass it to ctx.ReplyEmbed() or ctx.SendEmbed() directly (no .Build() needed)
bot.OnKeyword("status").Reply(func(ctx *sikasa.MsgCtx) error {
    embed := ctx.NewEmbed().
        Title("Status").
        Color(0x57F287).
        Description("All systems nominal").
        Field("Region", "us-west", true).
        Field("Latency", "42ms", true).
        Footer("updated just now", "").
        Now()
    return ctx.SendEmbed(embed)
})

The builder mirrors Discord's embed shape: Title / Description / URL / Color / Author / Footer / Thumbnail / Image / Field / Timestamp. For long content, clamp with sikasa.Truncate(s, sikasa.EmbedDescriptionMaxLen) (or EmbedFieldValueMaxLen etc.) so a single overflow does not 400 the whole reply.

Prefix Commands (PrefixCtx)

For classic text-trigger commands (!play, !ping, ?help), set a global prefix and register builders. The API mirrors bot.Command, so the muscle memory carries over.

bot.SetPrefix("!")

bot.OnPrefix("ping", "Replies pong").
    Handle(func(ctx *sikasa.PrefixCtx) error {
        return ctx.Reply("pong")
    })

bot.OnPrefix("echo", "Echoes back text").
    Aliases("e", "say").
    StringArg("text", "text to echo", true).
    Handle(func(ctx *sikasa.PrefixCtx) error {
        return ctx.Reply(ctx.String("text"))
    })

bot.OnPrefix("add", "Adds two integers").
    IntArg("a", "first number", true).
    IntArg("b", "second number", true).
    Handle(func(ctx *sikasa.PrefixCtx) error {
        sum := ctx.Int("a") + ctx.Int("b")
        return ctx.Reply(strconv.FormatInt(sum, 10))
    })

Behavior:

  • The last StringArg consumes the entire remaining message tail, so !echo hello world puts "hello world" into the text arg.
  • Aliases dispatch to the same handler. !e and !say both run the echo builder.
  • Command name lookup is case-insensitive (!Echo works); the prefix itself is case-sensitive.
  • When a message starts with the prefix, keyword matchers do not fire for that message, preventing double-responses.
  • Unknown commands reply with sikasa: unknown prefix command: !xxx. Missing required args reply with sikasa: missing required argument: name.
  • RequireSameVoice() gates a command on the invoker being in the same voice channel as the bot. When the bot is not in any voice channel for the guild the gate passes (so initial-join commands still work). On rejection the user gets sikasa: you must be in the same voice channel as the bot.
bot.OnPrefix("skip", "Skip to the next track").
    RequireSameVoice().
    Handle(func(ctx *sikasa.PrefixCtx) error {
        // only reachable when the user is in the bot's voice channel
        return nil
    })

Hybrid Argument Access:

Builder validation gives you typed access via ctx.String/Int/Bool. For free-form parsing (variadic args, custom syntax) drop to the raw escape hatches:

bot.OnPrefix("tag", "Tag multiple users").
    Handle(func(ctx *sikasa.PrefixCtx) error {
        // ctx.Args() every whitespace-separated token after the command name
        // ctx.Arg(i) i-th token, or ""
        // ctx.Rest() message tail with original whitespace preserved
        // ctx.Name() canonical command name (alias-resolved)
        return ctx.Reply("tagged: " + strings.Join(ctx.Args(), ", "))
    })
Voice & Music (VoiceCtx)

Join a voice channel and stream audio in a few lines. DAVE encryption is set up automatically.

bot.Command("play", "Play a song").
    StringArg("file", "path to audio", true).
    Handle(func(ctx *sikasa.CmdCtx) error {
        guildID := ctx.Event().GuildID()
        if guildID == nil {
            return ctx.Reply("voice commands only work in a server")
        }
        state, ok := ctx.Bot().Disgo().Caches.VoiceState(*guildID, ctx.Author().ID)
        if !ok || state.ChannelID == nil {
            return ctx.Reply("you must be in a voice channel first")
        }

        vctx, err := ctx.Bot().Voice().Join(guildID.String(), state.ChannelID.String())
        if err != nil {
            return ctx.Reply("join error: " + err.Error())
        }

        // Auto-detects file type. .opus and .ogg use stream-copy
        // passthrough (zero CPU); other formats are transcoded via FFmpeg.
        return vctx.PlayFile(ctx.String("file"))
    })

YouTube playback (requires yt-dlp):

vctx.PlayYouTube("https://youtu.be/dQw4w9WgXcQ")

Playlists, channels, and any other multi-entry yt-dlp URL are expanded automatically. A single PlayYouTube call appends every entry to the queue in order, so passing youtube.com/playlist?list=... enqueues the whole list at once.

Free-text search via SearchYouTube(query, n) returns top-N candidate Tracks (ytsearch<n>:<query> under the hood). Pair it with BuildYTSearchEmbed and Bot.OnButton to give users an interactive picker:

// In your prefix handler:
results, _ := sikasa.SearchYouTube(query, 3)
sessionID := bot.NewYTSearchSession(invokerID, guildID, results)
embed, buttons := sikasa.BuildYTSearchEmbed(query, sessionID, results)
ctx.SendEmbedWithButtons(embed, buttons...)

// Wire the click handler once at startup:
bot.OnButton("/sikasa/ytsearch/{session}/{idx}", func(ctx *sikasa.ButtonCtx) error {
    s := ctx.Bot().YTSearchSession(ctx.Var("session"))
    if s == nil || ctx.Author().ID != s.InvokerID() {
        return ctx.Reply("not allowed")
    }
    // s.Tracks()[idx], join voice, PlayYouTube, etc.
    return nil
})

Sessions live in memory for 5 minutes; only the original invoker can click results.

firstPos, added, started, err := vctx.PlayYouTube(playlistURL)
// added = number of tracks appended (1 for a single video)
// started = true if the first track is now playing
// firstPos = index of the first appended track

Each Track in the queue carries Title and Author (uploader/channel) populated from yt-dlp, so reach for track.Label() ("Title by Author") when displaying the queue. This keeps replies clean and avoids Discord's auto-embed spam on raw URLs.

PlayFile enqueues a local file and returns (pos, started, err). Tracks auto-advance on natural EOF, so a multi-track queue plays straight through without manual prompting.

pos, started, err := vctx.PlayFile("song.mp3")
if err != nil { /* ... */ }
if started {
    fmt.Println("playing now")
} else {
    fmt.Printf("queued at #%d\n", pos+1)
}

Queue control:

vctx.Skip()         // advance to the next track (alias: Next)
vctx.Prev()         // rewind cursor by one and play that track
vctx.Now()          // (Track, ok) currently loaded track
vctx.Queue()        // []Track snapshot of the entire list
vctx.Cursor()       // index of the current track, -1 if none started
vctx.ClearQueue()   // empty the queue (current track keeps playing)

Per-track control:

vctx.Pause()
vctx.Resume()
vctx.Stop()    // halts current track; queue is preserved
vctx.Leave()   // disconnects entirely (clears queue too)
vctx.Reconnect() // tear down and reopen, then resume current track

The bot watches its own log stream for DAVE/voice errors ("no active epoch", "failed to encrypt packet") and triggers Reconnect() automatically with a 30-second per-guild debounce. The current track is restarted from the beginning because FFmpeg is torn down with the connection. Hook your own slog handler via WithSlog to see when this happens.

Multi-guild sessions are automatic: each guild gets its own VoiceCtx (and therefore its own queue, cursor, and FFmpeg pipeline), so the bot can play different music in two servers concurrently without any extra wiring.

Retrieve an existing connection from anywhere:

vctx := bot.Voice().Get(guildID)  // nil if not connected
Rate Limiting

Keyword replies accept an optional rate-limit config to silence spammers:

bot.OnKeyword("sikasa").
    ReplyFile("ongo", "media/pp.jpg",
        sikasa.RateLimitInterval(1, 10*time.Second))

A given user can trigger this rule at most once per 10 seconds; extra messages are silently dropped.

Errors

Sikasa exports sentinel errors so you can branch on failure modes without parsing strings. Wrapped errors (with extra context like the offending guild ID) preserve the chain, so errors.Is works on both bare and wrapped values.

_, err := bot.Voice().Join("garbage", "ids")
if errors.Is(err, sikasa.ErrInvalidGuildID) {
    // safe to recover or show a friendlier message
}
Sentinel Source
ErrEmptyToken sikasa.New("")
ErrBotNotStarted Voice operations called before bot.Start()
ErrInvalidGuildID Guild snowflake fails to parse
ErrInvalidChannelID Channel snowflake fails to parse
ErrNotInVoice Voice operation requires an active connection
ErrNoAudio Pause() called while nothing is playing
ErrNotPaused Resume() called outside the paused state
ErrUnknownCommand Prefix dispatch finds no matching command
ErrMissingArg Required builder argument absent from the message
ErrInvalidArg Argument value fails type parsing (e.g. IntArg got non-numeric)
ErrQueueEmpty Queue navigation called on an empty queue
ErrNoPrevious Prev() called while the cursor is already at the first track
ErrNotSameChannel RequireSameVoice() gate rejected the invoker

Escape Hatches

When Sikasa doesn't cover something, drop down to disgo directly:

client := bot.Disgo()                    // *bot.Client (disgo)
event  := ctx.Event()                    // *handler.CommandEvent
data   := ctx.Data()                     // discord.SlashCommandInteractionData
voice  := vctx.Disgo()                   // voice.Conn

// e.g. send a custom embed via the raw REST surface
client.Rest.CreateMessage(channelID, discord.NewMessageCreate().AddEmbeds(myEmbed))

Roadmap / TODO

Voice features deferred from this iteration:

  • Real-time volume control (currently fixed at 100%; would need PCM-side gain or FFmpeg restart)
  • AudioProvider chaining for custom audio sources (Spotify, custom TTS, raw PCM)
  • Voice receive (decoding user audio; disgo exposes OpusFrameReceiver but Sikasa hasn't surfaced it yet)
  • Lavalink mode for production deployments at scale

Now that disgo is the underlying library, the following also become easy follow-ons:

  • Components (buttons, select menus) via disgo's handler router
  • Modals with the same path-pattern API
  • Threads, polls, business connections that disgo supports natively

License

MIT

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

View Source
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.

View Source
const (
	EmbedTitleMaxLen       = 256
	EmbedDescriptionMaxLen = 4096
	EmbedFieldNameMaxLen   = 256
	EmbedFieldValueMaxLen  = 1024
	EmbedFooterTextMaxLen  = 2048
	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

View Source
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

func IsHTTPURL(s string) bool

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

func Truncate

func Truncate(s string, n int) string

Truncate clips s to at most n characters, appending an ellipsis when truncation occurs. Multi-byte safe for Discord's UTF-8 byte budget if n is given as a byte count.

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

func New(token string) (*Bot, error)

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

func (b *Bot) ConsumeYTSearch(id string) *ytSearchSession

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

func (b *Bot) Disgo() *bot.Client

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

func (b *Bot) NewYTSearchSession(invoker, guildID snowflake.ID, tracks []Track) string

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

func (b *Bot) SetPrefix(p string) *Bot

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

func (b *Bot) Start() error

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

func (b *Bot) Stop() error

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

func (b *Bot) WithGuild(guildID string) *Bot

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

func (b *Bot) WithIntents(intents gateway.Intents) *Bot

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

func (b *Bot) WithLogger(l *log.Logger) *Bot

WithLogger swaps the default logger. Pass nil to silence output.

params:
      l: standard library logger
returns:
      *Bot: receiver, for chaining

func (*Bot) WithSlog

func (b *Bot) WithSlog(l *slog.Logger) *Bot

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

func (b *Bot) WithVerbose() *Bot

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

func (b *Bot) YTSearchSession(id string) *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) Author

func (c *ButtonCtx) Author() discord.User

Author returns the user who clicked the button.

func (*ButtonCtx) AuthorID

func (c *ButtonCtx) AuthorID() string

AuthorID returns the snowflake of the user who clicked the button. Convenient for permission checks against a stored invokerID.

func (*ButtonCtx) Bot

func (c *ButtonCtx) Bot() *Bot

Bot returns the parent Bot.

func (*ButtonCtx) ChannelID

func (c *ButtonCtx) ChannelID() string

ChannelID returns the channel where the button lives.

func (*ButtonCtx) ClearComponents

func (c *ButtonCtx) ClearComponents() error

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) CustomID

func (c *ButtonCtx) CustomID() string

CustomID returns the raw customID string of the clicked button.

func (*ButtonCtx) Data

Data returns the underlying button interaction data.

func (*ButtonCtx) Defer

func (c *ButtonCtx) Defer(ephemeral bool) error

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

func (c *ButtonCtx) DeferUpdate() error

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) GuildID

func (c *ButtonCtx) GuildID() string

GuildID returns the guild snowflake as a string, or empty for DMs.

func (*ButtonCtx) Reply

func (c *ButtonCtx) Reply(text string) error

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

func (c *ButtonCtx) ReplyEmbed(embed any) error

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

func (c *ButtonCtx) ReplyPublic(text string) error

ReplyPublic sends a non-ephemeral message in response to the click.

params:
      text: the message body
returns:
      error: from disgo

func (*ButtonCtx) Update

func (c *ButtonCtx) Update(text string) error

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

func (c *ButtonCtx) UpdateEmbed(embed any) error

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

func (*ButtonCtx) Var

func (c *ButtonCtx) Var(name string) string

Var is a convenience for Vars()[name].

func (*ButtonCtx) Vars

func (c *ButtonCtx) Vars() map[string]string

Vars returns path variables parsed from the customID pattern. For "/sikasa/ytsearch/{session}/{idx}" with customID "/sikasa/ytsearch/abcd/2", Vars()["session"] is "abcd" and Vars()["idx"] is "2".

type ButtonHandler

type ButtonHandler func(ctx *ButtonCtx) error

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

func (c *CmdCtx) Author() discord.User

Author returns the user who invoked the command, regardless of whether the invocation happened in a guild or in DMs.

func (*CmdCtx) Bool

func (c *CmdCtx) Bool(name string) bool

Bool returns the boolean value for the named option, or false if missing.

func (*CmdCtx) Bot

func (c *CmdCtx) Bot() *Bot

Bot returns the parent Bot.

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) ChannelID

func (c *CmdCtx) ChannelID() string

ChannelID returns the channel where the command was invoked.

func (*CmdCtx) Data

Data returns the parsed slash command interaction data, useful when the option helpers below are insufficient (e.g. iterating Resolved entities).

func (*CmdCtx) Defer

func (c *CmdCtx) Defer(ephemeral bool) error

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

func (c *CmdCtx) Followup(text string) error

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

func (c *CmdCtx) GuildID() string

GuildID returns the guild snowflake as a string, or empty for DM invocations.

func (*CmdCtx) Int

func (c *CmdCtx) Int(name string) int64

Int returns the integer value for the named option, or 0 if missing.

func (*CmdCtx) Reply

func (c *CmdCtx) Reply(text string) error

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

func (c *CmdCtx) ReplyEmbed(embed discord.Embed) error

ReplyEmbed sends an embed response.

params:
      embed: a fully-built Embed
returns:
      error: from disgo

func (*CmdCtx) ReplyEphemeral

func (c *CmdCtx) ReplyEphemeral(text string) error

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

func (c *CmdCtx) ReplyFile(content, filePath string) error

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

func (c *CmdCtx) ReplyURL(content, url, fileName string) error

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

func (*CmdCtx) String

func (c *CmdCtx) String(name string) string

String returns the string value for the named option, or "" if missing.

func (*CmdCtx) User

func (c *CmdCtx) User(name string) discord.User

User returns the picked user for the named option, or the zero User if missing.

type CmdHandler

type CmdHandler func(ctx *CmdCtx) error

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

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

type Intents = gateway.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) Author

func (c *MsgCtx) Author() discord.User

Author returns the user who sent the message.

func (*MsgCtx) AuthorMention

func (c *MsgCtx) AuthorMention() string

AuthorMention returns the formatted mention string for the author.

func (*MsgCtx) Bot

func (c *MsgCtx) Bot() *Bot

Bot returns the parent Bot.

func (*MsgCtx) ChannelID

func (c *MsgCtx) ChannelID() string

ChannelID returns the channel where the message was sent.

func (*MsgCtx) Content

func (c *MsgCtx) Content() string

Content returns the raw message text.

func (*MsgCtx) Event

func (c *MsgCtx) Event() *events.MessageCreate

Event returns the underlying disgo *events.MessageCreate as an escape hatch.

func (*MsgCtx) GuildID

func (c *MsgCtx) GuildID() string

GuildID returns the guild snowflake as a string, or empty for DMs.

func (*MsgCtx) Message

func (c *MsgCtx) Message() discord.Message

Message returns the raw discord.Message.

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

func (c *MsgCtx) React(emoji string) error

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

func (c *MsgCtx) Reply(text string) error

Reply sends a plain-text inline reply to the user's message.

params:
      text: the message body
returns:
      error: from disgo

func (*MsgCtx) ReplyEmbed

func (c *MsgCtx) ReplyEmbed(embed any) error

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

func (c *MsgCtx) ReplyFile(content, filePath string) error

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

func (c *MsgCtx) ReplyURL(content, url, fileName string) error

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

func (c *MsgCtx) Send(text string) error

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

func (c *MsgCtx) SendEmbed(embed any) error

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

type MsgHandler func(ctx *MsgCtx) error

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

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

func (c *PrefixCtx) Arg(i int) string

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

func (c *PrefixCtx) Args() []string

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

func (c *PrefixCtx) Bool(name string) 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

func (c *PrefixCtx) Int(name string) int64

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

func (c *PrefixCtx) Name() string

Name returns the canonical command name that matched, even if the user invoked the command via an alias.

func (*PrefixCtx) Rest

func (c *PrefixCtx) Rest() string

Rest returns the message tail after the command name with original whitespace preserved. Use this for raw text capture (e.g. quoting, multi-line input) that strings.Fields would have collapsed.

func (*PrefixCtx) String

func (c *PrefixCtx) String(name string) string

String returns the value of a named StringArg. If the arg was optional and omitted, the zero value "" is returned.

params:
      name: the arg name as declared via StringArg
returns:
      string: the parsed value, or "" if absent

type PrefixHandler

type PrefixHandler func(ctx *PrefixCtx) error

PrefixHandler is the signature every prefix command handler must satisfy.

type RateLimitConfig

type RateLimitConfig struct {
	Count    int
	Duration time.Duration
}

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

type Track struct {
	Kind   TrackKind
	Source string
	Title  string
	Author string
}

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

func SearchYouTube(query string, n int) ([]Track, error)

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.

func (Track) Label

func (t Track) Label() string

Label returns a human-friendly description of the track. If Title is set, formats as "Title by Author" (or just "Title" when Author is empty); otherwise falls back to Source. Use this for any user-facing reply so the bot does not spam raw URLs.

type TrackKind

type TrackKind int

TrackKind tags how a Track should be played back.

const (
	// TrackFile is a local audio file path. Routed to PlayFile semantics.
	TrackFile TrackKind = iota
	// TrackYouTube is any URL yt-dlp can resolve. Routed to PlayYouTube.
	TrackYouTube
)

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) Bot

func (v *VoiceCtx) Bot() *Bot

Bot returns the parent *Bot. Mainly for symmetry with CmdCtx / MsgCtx.

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

func (v *VoiceCtx) Cursor() int

Cursor returns the index of the currently playing track, or -1 if nothing has started yet.

func (*VoiceCtx) Disgo

func (v *VoiceCtx) Disgo() voice.Conn

Disgo returns the underlying voice.Conn as an escape hatch for advanced operations (custom OpusFrameReceiver, raw UDP writes, etc).

func (*VoiceCtx) Enqueue

func (v *VoiceCtx) Enqueue(t Track) (position int, started bool, err error)

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

func (v *VoiceCtx) InsertNext(t Track) (position int, started bool, err error)

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

func (v *VoiceCtx) InsertNextYouTube(url string) (firstPos, added int, started bool, err error)

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) IsPlaying

func (v *VoiceCtx) IsPlaying() bool

IsPlaying reports whether audio is actively being streamed.

func (*VoiceCtx) JumpTo

func (v *VoiceCtx) JumpTo(index int) (Track, error)

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

func (v *VoiceCtx) Leave() error

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) Next

func (v *VoiceCtx) Next() (Track, bool, error)

Next is an alias for Skip provided for callers that prefer the queue verb.

func (*VoiceCtx) Now

func (v *VoiceCtx) Now() (Track, bool)

Now returns the currently loaded track and true, or a zero Track and false if the queue has not started yet.

func (*VoiceCtx) Pause

func (v *VoiceCtx) Pause() error

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

func (v *VoiceCtx) PlayFile(path string) (position int, started bool, err error)

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

func (v *VoiceCtx) PlayYouTube(url string) (firstPos, added int, started bool, err error)

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

func (v *VoiceCtx) Prev() (Track, error)

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

func (v *VoiceCtx) Queue() []Track

Queue returns a snapshot of the queued tracks. The current track (if any) is included at index Cursor().

func (*VoiceCtx) Reconnect

func (v *VoiceCtx) Reconnect() error

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

func (v *VoiceCtx) Resume() error

Resume continues a paused stream.

returns:
      error: if the state is not Paused

func (*VoiceCtx) SetAnnounceChannel

func (v *VoiceCtx) SetAnnounceChannel(channelID string) *VoiceCtx

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

func (v *VoiceCtx) Skip() (Track, bool, error)

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

func (v *VoiceCtx) Source() string

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

func (v *VoiceCtx) Stop() error

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
)

Directories

Path Synopsis
example/main.go Purpose: Demonstrates how to build a Discord bot with the sikasa wrapper.
example/main.go Purpose: Demonstrates how to build a Discord bot with the sikasa wrapper.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL