Chat about this codebase

AI-powered code exploration

Online

Project Overview

DisGo is a Go library for building Discord bots and integrations. It provides full REST and Gateway support, strongly typed data structures, and extensible middleware for high-performance, production-ready applications.

High-Level Overview

DisGo exposes a single, intuitive API to:

  • Authenticate and manage bot clients
  • Interact with Discord’s REST endpoints (channels, messages, guilds, etc.)
  • Connect to Discord’s Gateway for real-time events
  • Register event listeners and middleware
  • Inspect and override library version metadata at build time

Key Features

  • Full REST API coverage
  • Gateway connection with shard management and intent filtering
  • Strongly typed events and request/response structs
  • Built-in rate limit handling and retry logic
  • Middleware and plugin support for custom behavior
  • Version and metadata exposed via disgo.Version and disgo.SemVersion
  • Minimal external dependencies for lightweight deployments

Why DisGo?

  • Type Safety
    Eliminate JSON parsing errors with Go structs that mirror Discord’s schema.
  • Performance
    Optimized for low latency and efficient resource usage.
  • Flexibility
    Configure gateway options (intents, identify payload), swap out middleware, or extend core behavior.
  • Discoverability
    Comprehensive documentation, examples, and community-driven plugins.

Minimal Example

This example shows how to instantiate a DisGo client and log in:

package main

import (
  "context"
  "log"
  "os"

  "github.com/disgoorg/disgo"
  "github.com/disgoorg/disgo/bot"
)

func main() {
  token := os.Getenv("DISGO_BOT_TOKEN")

  // Create a new DisGo client with default gateway options
  client, err := disgo.New(token,
    bot.WithGatewayConfigOpts(),        // uses default intents
    bot.WithEventListenerFunc(func(e *disgo.MessageCreate) {
      if e.Message.Content == "hello" {
        _, _ = e.Client().Rest().CreateMessage(e.ChannelID, disgo.NewMessageCreateRequest().SetContent("Hi there!"))
      }
    }),
  )
  if err != nil {
    log.Fatalf("failed to create client: %v", err)
  }

  // Connect to Discord’s Gateway
  if err := client.OpenGateway(context.Background()); err != nil {
    log.Fatalf("failed to open gateway: %v", err)
  }
}

Value Proposition

By using DisGo, developers gain:

  • A single library to handle all Discord interactions
  • Strongly typed code to reduce runtime errors
  • Built-in best practices for rate limits and gateway management
  • Easy extensibility for custom commands, middleware, and plugins

Explore the full documentation and examples on GitHub to get started with advanced features like slash commands, voice support, and custom middleware.

Getting Started

Get up and running with DisGo: install the library, create your first bot, run it, and respond to messages.

Prerequisites

  • Go 1.24 or later
  • A Discord application with a bot token
  • DISGO_TOKEN environment variable set to your bot token

Adding DisGo as a Dependency

  1. Initialize your module (if needed):
    go mod init github.com/yourusername/yourbot
    
  2. Add DisGo and tidy up:
    go get github.com/disgoorg/disgo@latest
    go mod tidy
    

Writing Your First Bot

Create a file named main.go with the following content:

package main

import (
  "context"
  "log"
  "os"

  "github.com/disgoorg/disgo"
  "github.com/disgoorg/disgo/bot"
  "github.com/disgoorg/disgo/events"
  "github.com/disgoorg/disgo/discord"
  "github.com/disgoorg/disgo/gateway"
)

func main() {
  token := os.Getenv("DISGO_TOKEN")
  if token == "" {
    log.Fatal("DISGO_TOKEN not set")
  }

  client, err := disgo.New(token,
    // Enable message intents to receive message content
    bot.WithGatewayConfigOpts(
      gateway.WithIntents(
        gateway.IntentGuildMessages,
        gateway.IntentMessageContent,
      ),
    ),
    // Register event handlers
    bot.WithEventListeners(&events.ListenerAdapter{
      OnReady: func(event *events.Ready) {
        log.Printf("Logged in as %s#%s", event.User.Username, event.User.Discriminator)
      },
      OnMessageCreate: func(event *events.MessageCreate) {
        // Ignore bots to prevent loops
        if event.Message.Author.Bot {
          return
        }
        // Simple ping-pong response
        if event.Message.Content == "ping" {
          _, err := event.Client().Rest.CreateMessage(event.ChannelID, discord.MessageCreate{
            Content: "pong",
          })
          if err != nil {
            log.Println("failed to send message:", err)
          }
        }
      },
    }),
  )
  if err != nil {
    log.Fatal("failed to create client:", err)
  }

  // Connect to Discord and block until shutdown
  if err = client.Open(context.Background()); err != nil {
    log.Fatal("failed to open connection:", err)
  }
  <-client.Done()
}

Running the Bot

  1. Export your bot token:
    export DISGO_TOKEN=your_bot_token
    
  2. Start the bot:
    go run main.go
    
  3. In any guild channel where your bot is present, send ping to see it reply with pong.

Next Steps

  • Browse the full API reference on pkg.go.dev:
    https://pkg.go.dev/github.com/disgoorg/disgo
  • Explore advanced examples in the examples/ directory
  • Consult the docs/ folder for guides on embeds, components, slash commands, and more

Core Concepts: Subsection Selection

Please specify which subsection you’d like detailed documentation for:

  • SyncCommands utility
  • Mux routing methods (Use, Route, Mount, etc.)
  • handlerHolder matching logic
  • Middleware package (e.g. Defer, Go, Logger)
  • Router interface and handler types

Or let me know any other core concept area in disgoorg/disgo you’d like covered.

Advanced Usage

Dive into Disgo’s power-user features to fine-tune sharding, voice audio handling, HTTP servers, webhook clients, and proxy configurations.

Configuring the ShardManager

Customize shard behavior via functional options passed to sharding.New.

import (
  "context"
  "log"

  "github.com/disgoorg/disgo/sharding"
  "github.com/disgoorg/disgo/gateway"
)

// eventHandler receives gateway events
func eventHandler(evt gateway.Event) { /* … */ }

func main() {
  token := "YOUR_BOT_TOKEN"
  mgr := sharding.New(token, eventHandler,
    // 1. Manage shards 0–2 out of 5 total
    sharding.WithShardIDs(0, 1, 2),
    sharding.WithShardCount(5),
    // 2. Enable auto-resharding: split large shards into 3
    sharding.WithAutoScaling(true),
    sharding.WithShardSplitCount(3),
    // 3. Pass gateway options: set intents, proxy URL
    sharding.WithGatewayConfigOpts(
      gateway.WithIntents(gateway.IntentGuildMessages|gateway.IntentGuildMembers),
      gateway.WithProxyURL("http://proxy:8080"),
    ),
    // 4. Increase login concurrency
    sharding.WithRateLimiterConfigOpt(
      sharding.WithMaxConcurrency(2),
      sharding.WithIdentifyWait(5*1e9), // 5s
    ),
  )

  ctx := context.Background()
  if err := mgr.Open(ctx); err != nil {
    log.Fatalf("failed to open sharding manager: %v", err)
  }
  defer mgr.Close(ctx)
}

Integrating AudioReceiver into Your Voice Connection

Receive per-user Opus frames by implementing voice.OpusFrameReceiver.

1. Implement OpusFrameReceiver

import (
  "log"

  "github.com/disgoorg/disgo/voice"
  "github.com/disgoorg/snowflake/v2"
)

type MyOpusReceiver struct{}

func NewMyOpusReceiver() *MyOpusReceiver { return &MyOpusReceiver{} }

func (r *MyOpusReceiver) ReceiveOpusFrame(userID snowflake.ID, packet *voice.Packet) error {
  log.Printf("Frame from %d: %d bytes", userID, len(packet.Data))
  // decode or buffer packet.Data as needed
  return nil
}

func (r *MyOpusReceiver) CleanupUser(userID snowflake.ID) {
  log.Printf("cleanup resources for user %d", userID)
}

func (r *MyOpusReceiver) Close() {
  log.Println("closing MyOpusReceiver")
}

2. Register and Open Connection

import (
  "context"
  "log"

  "github.com/disgoorg/disgo/voice"
  "github.com/disgoorg/disgo/voice/manager"
)

func main() {
  ctx := context.Background()
  guildID := Snowflake(123)
  channelID := Snowflake(456)

  // Create voice connection
  conn := manager.New().CreateConn(guildID)

  // Set OpusFrameReceiver
  receiver := NewMyOpusReceiver()
  conn.SetOpusFrameReceiver(receiver)

  if err := conn.Open(ctx, channelID, false, false); err != nil {
    log.Fatalf("failed to open voice conn: %v", err)
  }
  defer conn.Close(ctx)
}

3. Practical Tips

  • Frames arrive every 20 ms; aggregate for PCM decoding.
  • CleanupUser triggers on user leave/disconnect.
  • Calling conn.Close or replacing the receiver invokes its Close().

Configuring the HTTP Server

Expose an interaction callback endpoint with httpserver.New and override defaults via options.

import (
  "context"
  "log"
  "net/http"

  "github.com/disgoorg/disgo/httpserver"
  "github.com/disgoorg/disgo/discord"
)

func main() {
  // Your public key from Discord Developer Portal
  publicKey := "YOUR_PUBLIC_KEY_HEX"

  // Define interaction handler
  handler := func(respond httpserver.RespondFunc, event httpserver.EventInteractionCreate) {
    respond(discord.InteractionResponse{
      Type: discord.InteractionResponseTypeChannelMessageWithSource,
      Data: &discord.InteractionResponseData{
        Content: "Hello from Disgo!",
      },
    })
  }

  srv, err := httpserver.New(
    publicKey, handler,
    httpserver.WithAddress(":8080"),
    httpserver.WithURL("/discord/interactions"),
    httpserver.WithTLS("cert.pem", "key.pem"),
  )
  if err != nil {
    log.Fatalf("failed to create server: %v", err)
  }

  go func() {
    if err := srv.Start(); err != nil && err != http.ErrServerClosed {
      log.Fatalf("server error: %v", err)
    }
  }()

  // Shutdown handling omitted for brevity
}

Creating a Webhook Client

Initialize webhook.Client by ID/token or by parsing a URL, and apply client or logger customizations.

import (
  "context"
  "fmt"
  "log"

  "github.com/disgoorg/disgo/rest/webhook"
  "github.com/disgoorg/snowflake/v2"
)

func exampleByID() {
  id, _ := snowflake.Parse("123456789012345678")
  token := "your-webhook-token"
  client := webhook.New(id, token,
    webhook.WithLogger(log.Default()),
    webhook.WithRestClientConfigOpts(
      webhook.WithBaseURL("https://custom.api/"),
    ),
  )
  fmt.Println("Webhook URL:", client.URL())
  client.Close(context.Background())
}

func exampleByURL() {
  rawURL := "https://discord.com/api/webhooks/123456789012345678/your-webhook-token"
  client, err := webhook.NewWithURL(rawURL,
    webhook.WithDefaultAllowedMentions(webhook.AllowedMentions{
      Parse: []webhook.AllowedMentionType{webhook.AllowedMentionTypeUsers},
    }),
  )
  if err != nil {
    log.Fatalf("could not create webhook client: %v", err)
  }
  // Use client...
  client.Close(context.Background())
}

Configuring Disgo with Gateway & REST Proxies

Point Disgo at custom gateway/rest proxies and disable internal rate limiters.

import (
  "log"
  "os"

  "github.com/disgoorg/disgo"
  "github.com/disgoorg/disgo/bot"
  "github.com/disgoorg/disgo/gateway"
  "github.com/disgoorg/disgo/rest"
  "github.com/disgoorg/disgo/sharding"
)

func main() {
  token := os.Getenv("disgo_token")
  gwURL := os.Getenv("disgo_gateway_url")
  restURL := os.Getenv("disgo_rest_url")

  client, err := disgo.New(token,
    bot.WithShardManagerConfigOpts(
      sharding.WithGatewayConfigOpts(
        gateway.WithURL(gwURL),
        gateway.WithCompress(false),
      ),
      sharding.WithRateLimiter(sharding.NewNoopRateLimiter()),
    ),
    bot.WithRestClientConfigOpts(
      rest.WithURL(restURL),
      rest.WithRateLimiter(rest.NewNoopRateLimiter()),
    ),
    // add your event handlers…
  )
  if err != nil {
    log.Fatal("failed to create Disgo client:", err)
  }
  client.Open() // or client.OpenWithContext(ctx)
}

Practical guidance:

  • Use NewNoopRateLimiter when your proxy enforces limits.
  • Disable compression behind low-latency internal proxies.
  • Intents and sharding typically are handled by the proxy.

Examples Cookbook

Curated, runnable examples demonstrating common patterns and specialized features in Disgo.


Slash Commands via Gateway

Register and handle slash commands over the Discord Gateway.

client, err := disgo.New(token,
  bot.WithDefaultGateway(),
  bot.WithEventListenerFunc(func(event *events.ApplicationCommandInteractionCreate) {
    data := event.SlashCommandInteractionData()
    if data.CommandName() == "say" {
      err := event.CreateMessage(discord.NewMessageCreateBuilder().
        SetContent(data.String("message")).
        SetEphemeral(data.Bool("ephemeral")).
        Build(),
      )
      if err != nil {
        log.Println("failed to send message:", err)
      }
    }
  }),
)
if err != nil {
  log.Fatal(err)
}
defer client.Close(context.Background())

// Register slash commands in a guild
_, err = client.Rest.SetGuildCommands(client.ApplicationID, guildID, []discord.ApplicationCommandCreate{
  discord.SlashCommandCreate{
    Name:        "say",
    Description: "says what you say",
    Options: []discord.ApplicationCommandOption{
      discord.ApplicationCommandOptionString{
        Name:        "message",
        Description: "What to say",
        Required:    true,
      },
      discord.ApplicationCommandOptionBool{
        Name:        "ephemeral",
        Description: "Visible only to you",
        Required:    true,
      },
    },
  },
})
if err != nil {
  log.Fatal(err)
}

// Connect to Discord
if err := client.OpenGateway(context.Background()); err != nil {
  log.Fatal(err)
}

Practical tips:

  • Use signal.Notify and block on a channel to gracefully shut down.
  • Log errors returned by event.CreateMessage to catch delivery issues.

Slash Commands via HTTP Server

Receive interactions via an HTTP callback endpoint—ideal for serverless setups.

// Custom ed25519 verifier
type customVerifier struct{}

func (customVerifier) Verify(pk httpserver.PublicKey, msg, sig []byte) bool {
  return ed25519.Verify(pk, msg, sig)
}
func (customVerifier) SignatureSize() int {
  return ed25519.SignatureSize
}

client, err := disgo.New(token,
  bot.WithHTTPServerConfigOpts(
    publicKey,
    httpserver.WithURL("/interactions/callback"),
    httpserver.WithAddress(":8080"),
    httpserver.WithVerifier(customVerifier{}),
  ),
  bot.WithEventListenerFunc(commandListener),
)
if err != nil {
  log.Fatal(err)
}
defer client.Close(context.Background())

// Register commands as in the gateway example
client.Rest.SetGuildCommands(client.ApplicationID, guildID, commands)

// Start the HTTP server
if err := client.OpenHTTPServer(); err != nil {
  log.Fatal(err)
}

Practical tips:

  • Expose Discord’s public key via WithVerifier.
  • Ensure your service is reachable by Discord (reverse proxy or public ingress).

Localizing Slash Commands

Define per-locale names and descriptions to improve UX for multilingual communities.

discord.SlashCommandCreate{
  Name: "say",
  NameLocalizations: map[discord.Locale]string{
    discord.LocaleEnglishGB: "say",
    discord.LocaleGerman:    "sagen",
  },
  Description: "says what you say",
  DescriptionLocalizations: map[discord.Locale]string{
    discord.LocaleEnglishGB: "says what you say",
    discord.LocaleGerman:    "sagt was du sagst",
  },
  Options: []discord.ApplicationCommandOption{
    discord.ApplicationCommandOptionString{
      Name: "message",
      NameLocalizations: map[discord.Locale]string{
        discord.LocaleGerman: "nachricht",
      },
      Description: "What to say",
      DescriptionLocalizations: map[discord.Locale]string{
        discord.LocaleGerman: "Was soll ich sagen?",
      },
      Required: true,
    },
  },
}

Practical tips:

  • Localizations apply at registration time—update via REST when changed.
  • Omit a locale to fall back on the base Name or Description.

Using Components V2 for Rich Message Layouts

Build and send a Discord message using Components V2 (v0.38+).

flags := discord.MessageFlagIsComponentsV2
if ephemeral, _ := data.OptBool("ephemeral"); ephemeral {
  flags = flags.Add(discord.MessageFlagEphemeral)
}

msg := discord.MessageCreate{
  Flags: flags,
  Components: []discord.LayoutComponent{
    discord.NewContainer(
      discord.NewSection(
        discord.NewTextDisplay("**Name: [Seeing Red](https://open.spotify.com/track/65qBr6ToDUjTD1RiE1H4Gl)**"),
        discord.NewTextDisplay("**Artist: [Architects](https://open.spotify.com/artist/3ZztVuWxHzNpl0THurTFCv)**"),
        discord.NewTextDisplay("**Album: [The Sky, The Earth & All Between](https://open.spotify.com/album/2W82VyyIFAXigJEiLm5TT1)**"),
      ).WithAccessory(discord.NewThumbnail("attachment://thumbnail.png")),
      discord.NewTextDisplay("`0:08` / `3:40`"),
      discord.NewTextDisplay("[🔘▬▬▬▬▬▬▬▬▬]"),
      discord.NewSmallSeparator(),
      discord.NewActionRow(
        discord.NewPrimaryButton("", "/player/previous").WithEmoji(discord.ComponentEmoji{Name: "⏮"}),
        discord.NewPrimaryButton("", "/player/pause_play").WithEmoji(discord.ComponentEmoji{Name: "⏯"}),
        discord.NewPrimaryButton("", "/player/next").WithEmoji(discord.ComponentEmoji{Name: "⏭"}),
        discord.NewDangerButton("", "/player/stop").WithEmoji(discord.ComponentEmoji{Name: "⏹"}),
        discord.NewPrimaryButton("", "/player/like").WithEmoji(discord.ComponentEmoji{Name: "❤️"}),
      ),
    ).WithAccentColor(0x5c5fea),
  },
  Files: []*discord.File{
    discord.NewFile("thumbnail.png", "", bytes.NewReader(thumbnail)),
  },
}

// Send in response to an interaction
if err := e.CreateMessage(msg); err != nil {
  slog.Error("error sending Components V2 message", slog.Any("err", err))
}

Practical tips:

  • Always include MessageFlagIsComponentsV2.
  • Reference files with attachment://<filename>.
  • Group buttons in NewActionRow (max 5 per row).
  • Use NewSmallSeparator and multiple NewTextDisplay calls for fine-grained spacing.

AutoModeration Rule Management

Create, update, and delete AutoModeration rules via the REST client.

// Create a rule
rule, err := client.Rest.CreateAutoModerationRule(guildID, discord.AutoModerationRuleCreate{
  Name:      "test-rule",
  EventType: discord.AutoModerationEventTypeMessageSend,
  TriggerType: discord.AutoModerationTriggerTypeKeyword,
  TriggerMetadata: &discord.AutoModerationTriggerMetadata{
    KeywordFilter: []string{"*test*"},
  },
  Actions: []discord.AutoModerationAction{
    {
      Type: discord.AutoModerationActionTypeSendAlertMessage,
      Metadata: &discord.AutoModerationActionMetadata{ChannelID: channelID},
    },
    {Type: discord.AutoModerationActionTypeBlockMessage},
  },
  Enabled: omit.Ptr(true),
})
if err != nil {
  log.Fatal("create rule:", err)
}
fmt.Println("Created rule ID:", rule.ID)

// Update the rule
updated, err := client.Rest.UpdateAutoModerationRule(guildID, rule.ID, discord.AutoModerationRuleUpdate{
  Name: omit.Ptr("test-rule-updated"),
  TriggerMetadata: &discord.AutoModerationTriggerMetadata{
    KeywordFilter: []string{"*test2*"},
  },
  Actions: &[]discord.AutoModerationAction{
    {
      Type: discord.AutoModerationActionTypeSendAlertMessage,
      Metadata: &discord.AutoModerationActionMetadata{ChannelID: channelID},
    },
  },
})
if err != nil {
  log.Fatal("update rule:", err)
}
fmt.Println("Updated rule name to:", *updated.Name)

// Delete the rule
if err := client.Rest.DeleteAutoModerationRule(guildID, rule.ID); err != nil {
  log.Fatal("delete rule:", err)
}
fmt.Println("Deleted rule:", rule.ID)

Practical tips:

  • Use omit.Ptr(...) for optional fields.
  • Updating Actions replaces existing actions; pass nil or omit to keep unchanged.
  • Listen to events.AutoModerationRuleCreate/Update/Delete to track propagation.

Playing a DCA Audio File

Stream a pre-encoded Opus “.dca” file into a voice channel with 20 ms frame timing.

func play(client *bot.Client, closeChan chan os.Signal) {
  conn := client.VoiceManager.CreateConn(guildID)
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  if err := conn.Open(ctx, channelID, false, false); err != nil {
    log.Fatal("voice connect:", err)
  }
  defer func() {
    closeCtx, closeCancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer closeCancel()
    conn.Close(closeCtx)
  }()

  if err := conn.SetSpeaking(ctx, voice.SpeakingFlagMicrophone); err != nil {
    log.Fatal("set speaking:", err)
  }

  writeOpus(conn.UDP())
  closeChan <- syscall.SIGTERM
}

func writeOpus(w io.Writer) {
  file, err := os.Open("nico.dca")
  if err != nil {
    log.Fatal("open dca:", err)
  }
  ticker := time.NewTicker(20 * time.Millisecond)
  defer ticker.Stop()

  var lenBuf [4]byte
  for range ticker.C {
    if _, err := io.ReadFull(file, lenBuf[:]); err != nil {
      if err == io.EOF {
        file.Close()
        return
      }
      log.Fatal("read length:", err)
    }
    frameLen := int64(binary.LittleEndian.Uint32(lenBuf[:]))
    if _, err := io.CopyN(w, file, frameLen); err != nil {
      file.Close()
      return
    }
  }
}

Practical tips:

  • Generate DCA with the dca tool or custom script.
  • Place nico.dca next to your binary or adjust the path.
  • Enable IntentGuildVoiceStates and supply guildID + channelID.

OAuth2 Client Configuration and Authorization Flow

Set up an OAuth2 client, generate auth URLs, handle callbacks, and fetch user data.

1. Initialize the OAuth2 Client

import (
  "net/http"
  "os"

  "github.com/disgoorg/disgo/oauth2"
  "github.com/disgoorg/disgo/rest"
  "github.com/disgoorg/snowflake/v2"
)

var (
  clientID     = snowflake.GetEnv("client_id")
  clientSecret = os.Getenv("client_secret")
  httpClient   = http.DefaultClient
  oauthClient  *oauth2.Client
)

func initOAuth2() {
  oauthClient = oauth2.New(
    clientID,
    clientSecret,
    oauth2.WithRestClientConfigOpts(
      rest.WithHTTPClient(httpClient),
    ),
  )
}

2. Generate the Authorization URL

import "github.com/disgoorg/disgo/discord"

func handleLogin(w http.ResponseWriter, r *http.Request) {
  params := oauth2.AuthorizationURLParams{
    RedirectURI: baseURL + "/callback",
    Scopes: []discord.OAuth2Scope{
      discord.OAuth2ScopeIdentify,
      discord.OAuth2ScopeEmail,
      discord.OAuth2ScopeConnections,
    },
  }
  url := oauthClient.GenerateAuthorizationURL(params)
  http.Redirect(w, r, url, http.StatusSeeOther)
}

3. Handle the OAuth2 Callback

var sessions = make(map[string]oauth2.Session)

func handleCallback(w http.ResponseWriter, r *http.Request) {
  code := r.URL.Query().Get("code")
  state := r.URL.Query().Get("state")
  if code == "" || state == "" {
    http.Error(w, "missing code or state", http.StatusBadRequest)
    return
  }

  session, _, err := oauthClient.StartSession(code, state)
  if err != nil {
    http.Error(w, "start session: "+err.Error(), http.StatusInternalServerError)
    return
  }

  id := randStr(32)
  sessions[id] = session
  http.SetCookie(w, &http.Cookie{Name: "token", Value: id, Path: "/"})
  http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

func randStr(n int) string {
  letters := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
  b := make([]byte, n)
  for i := range b {
    b[i] = letters[rand.Intn(len(letters))]
  }
  return string(b)
}

4. Fetch User Data with the Session

import "github.com/disgoorg/disgo/json/v2"

func handleRoot(w http.ResponseWriter, r *http.Request) {
  cookie, err := r.Cookie("token")
  if err != nil {
    showLogin(w); return
  }
  session, ok := sessions[cookie.Value]
  if !ok {
    showLogin(w); return
  }

  user, err := oauthClient.GetUser(session)
  if err != nil {
    http.Error(w, "GetUser: "+err.Error(), http.StatusInternalServerError)
    return
  }
  connections, err := oauthClient.GetConnections(session)
  if err != nil {
    http.Error(w, "GetConnections: "+err.Error(), http.StatusInternalServerError)
    return
  }

  userJSON, _ := json.MarshalIndent(user, "", "  ")
  connJSON, _ := json.MarshalIndent(connections, "", "  ")
  w.Header().Set("Content-Type", "text/html")
  w.Write([]byte(
    "User:<br><pre>" + string(userJSON) + "</pre>" +
    "Connections:<br><pre>" + string(connJSON) + "</pre>",
  ))
}

Practical tips:

  • Match RedirectURI exactly with your Discord application setting.
  • Store sessions securely (database or cache) in production.
  • Validate state to protect against CSRF.
  • Request only necessary scopes.
  • Implement token refresh for long-lived sessions.

Message Collector Example

Collect a fixed number of user messages with a timeout using bot.NewEventCollector.

func onMessageCreate(event *events.MessageCreate) {
  if event.Message.Author.Bot || event.Message.Author.System {
    return
  }
  if event.Message.Content != "start" {
    return
  }

  go func() {
    ch, closeCollector := bot.NewEventCollector(event.Client(), func(e *events.MessageCreate) bool {
      return e.ChannelID == event.ChannelID &&
             e.Message.Author.ID == event.Message.Author.ID &&
             e.Message.Content != ""
    })
    defer closeCollector()

    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    result := ">>> "
    count := 1
    for {
      select {
      case <-ctx.Done():
        event.Client().Rest.CreateMessage(event.ChannelID, discord.MessageCreate{
          Content: "cancelled",
        })
        return

      case msgEvt := <-ch:
        result += fmt.Sprintf("%d. %s\n\n", count, msgEvt.Message.Content)
        if count == 3 {
          event.Client().Rest.CreateMessage(msgEvt.ChannelID, discord.MessageCreate{
            Content: result,
          })
          return
        }
        count++
      }
    }
  }()
}

Practical tips:

  • Defer the collector’s close function to free resources.
  • Use select on ctx.Done() to handle timeouts.
  • Tailor the filter function for exact collection criteria.
  • Run the collector in its own goroutine to avoid blocking other handlers.

Architecture Overview

This section describes how DisGo’s core packages interact to provide a high-level, extensible Discord bot framework. You’ll learn how the root, bot, gateway, REST, HTTP server, and cache modules fit together and where to extend functionality.

Package Layout

disgo
– Entry point: version metadata and New.
bot
Client: orchestrates gateway, REST, HTTP server, cache, logging.
ConfigOpt: options for intents, shards, caching, rate-limits, HTTP server.
gateway
– WebSocket connection, shard manager, event dispatch, lifecycle hooks.
rest
– Typed wrappers around Discord REST API, built-in rate-limiter, middleware support.
httpserver
– HTTP listener for slash-command interactions, signature verification, router integration.
cache
– Abstract cache interfaces, default in-memory providers, adapters (Redis, BoltDB).
util
– Logging, bitwise flag helpers, backoff, UUID, timing utilities.

Client Initialization

Use disgo.New to wire all subsystems with sensible defaults. You supply your bot token and any number of bot.ConfigOpt to tweak behavior.

import (
  "log"
  "os"

  "github.com/disgoorg/disgo"
  "github.com/disgoorg/disgo/bot"
)

func main() {
  token := os.Getenv("DISCORD_TOKEN")
  if token == "" {
    log.Fatal("missing DISCORD_TOKEN")
  }

  client, err := disgo.New(token,
    // Core gateway intents
    bot.WithIntents(bot.IntentGuilds, bot.IntentGuildMessages),
    // Use default in-memory cache
    bot.WithCacheProvider(bot.CacheWithDefault[bot.UserCache]()),
    // Enable built-in HTTP server on :8080
    bot.WithHTTPServerAddr(":8080"),
    // Debug-level logging
    bot.WithLogLevel(bot.LogDebug),
  )
  if err != nil {
    log.Fatalf("failed to create client: %v", err)
  }
  …
}

Gateway Subsystem

The gateway module manages WebSocket connections and shards.
ShardManager spins up one or more shards based on config.
EventDispatcher invokes AddEventHandler callbacks on typed event structs.
Lifecycle Hooks allow OnConnect, OnDisconnect, OnReconnect handlers.

// Register a message handler
client.Gateway().AddEventHandler(func(e bot.MessageCreate) {
  if e.Message.Content == "!hello" {
    _, _ = e.Sender().SendMessage(e.Message.ChannelID, "Hello, world!")
  }
})

// Start the connection loop (blocks)
if err := client.OpenGateway(); err != nil {
  log.Fatalf("gateway failed: %v", err)
}
defer client.CloseGateway()

REST Module

All Discord REST endpoints live under disgo/rest.
Typed Methods return Go structs for channels, guilds, emojis, etc.
RateLimiter prevents HTTP 429s; you can supply a custom one with bot.WithRESTRateLimiter.
Middleware hooks let you inject headers, logging, or backoff policies.

import "github.com/disgoorg/disgo/rest"

// Replace default rate-limiter with in-memory bucket
client, _ := disgo.New(token,
  bot.WithRESTRateLimiter(rest.NewMemoryRateLimiter()),
)

HTTP Server (Interactions)

DisGo includes a built-in HTTP server for slash command interactions:
• Verifies Discord signatures.
• Dispatches InteractionCreate events via the gateway dispatcher.
• Exposes WithHTTPServerAddr and WithHTTPServerRouter for custom routing.

client, _ := disgo.New(token,
  bot.WithHTTPServerAddr(":3000"),
)
// Interaction handlers use the same AddEventHandler API
client.Gateway().AddEventHandler(func(i bot.InteractionCreate) {
  // respond to slash command
})
go client.OpenHTTPServer()

Caching

bot.CacheProvider abstracts storage of users, channels, guilds, roles, etc.
In‐memory: bot.CacheWithDefault[...]()
Redis: cache.NewRedisProvider(addr, options...)
Custom: implement cache.Provider for your backend.

// Use Redis for user and channel caches
client, _ := disgo.New(token,
  bot.WithCacheProvider(
    bot.CacheWithProvider(cache.NewRedis("localhost:6379"), bot.UserCache, bot.ChannelCache),
  ),
)

Extension Points

DisGo design encourages customization via:
bot.ConfigOpt: add or override any subsystem during New.
Event Handlers: Gateway().AddEventHandler for low-latency callbacks.
REST Middleware: custom auth, metrics, or retry logic.
HTTP Router: mount additional routes (health checks, metrics).
Custom Cache Providers: optimize memory or persistence.

By understanding these layers and their interactions, you can reason about internals and extend DisGo to meet advanced use cases.

Contribution & Development Guide

This guide outlines coding style, branching, issue/PR workflow, CI, and local testing for DisGo.

Coding Style & Formatting

Enforce consistent formatting via .editorconfig at the repo root. Editors with EditorConfig support will apply these rules automatically.

Key settings:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.go]
indent_style = tab
tab_width = 4

[*.md]
trim_trailing_whitespace = false

[*.{yaml,yml}]
indent_style = space
indent_size = 2

Before each commit:

go fmt ./...
goimports -w .

Branching & Commit Conventions

Use descriptive prefixes to categorize work:

  • feat/ – new features
  • fix/ or bugfix/ – bug fixes
  • docs/ – documentation
  • refactor/ – structural changes

Example:

git checkout -b feat/add-reaction-caching

Write commit messages in the form:

<type>(<scope>): <short description>

Reference issues:

fix(cache): prevent TTL overflow

Closes #123

Issue Templates

Bug Report

Use .github/ISSUE_TEMPLATE/bug_report.md. Include:

  1. Describe the bug – What’s not working?
  2. Error – Paste logs or stack trace.
  3. To Reproduce – Minimal Go snippet.
  4. Expected behavior – Desired outcome.
  5. DisGo Version – e.g. v0.8.3 or go.mod go directive.

Feature Request

Use .github/ISSUE_TEMPLATE/feature_request.md. Include:

  1. Problem – Current limitation.
  2. Solution – Proposed behavior.
  3. Alternatives – Other approaches considered.
  4. Additional context – Diagrams or mockups.

Pull Request Labeling

Automated by .github/workflows/labeler.yml using actions/labeler@v5:

  • Branch prefixes

    • feat/ → type:enhancement
    • fix/, bugfix/, hotfix/ → type:bugfix
    • docs/, documentation/ → type:docs
    • refactor/ → type:refactor
  • File-pattern rules

    • cache/*t:caching
    • gateway/*t:gateway
    • handler/*t:handler
    • oauth2/*t:oauth2
    • rest/*t:rest
    • sharding/*t:sharding
    • voice/*t:voice
    • *_rate_limiter_*t:ratelimits

Example:

git checkout -b fix/cache-ttl-expiry
# On PR open → labels: "type:bugfix", "t:caching"

CI Workflows

Go Pipeline (.github/workflows/go.yml)

Runs on pushes and PRs affecting Go files, go.mod, go.sum, or the workflow itself. Jobs: build, test, lint.

name: Go

on:
  push:
    paths:
      - '**/*.go'
      - 'go.mod'
      - 'go.sum'
      - '.github/workflows/go.yml'
  pull_request:
    paths:
      - '**/*.go'
      - 'go.mod'
      - 'go.sum'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: 1.24
      - uses: actions/checkout@v4
      - name: Build
        run: go build -v ./...

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: 1.24
      - uses: actions/checkout@v4
      - name: Test
        run: go test -v ./...

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: 1.24
      - uses: actions/checkout@v4
      - name: Lint
        uses: golangci/golangci-lint-action@v6
        with:
          version: latest

Tips:

  • Cache Go modules:
    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache/go-build
          ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
    
  • Align go.mod’s go directive with go-version.
  • Customize linting via .golangci.yml.

Labeler Workflow (.github/workflows/labeler.yml)

Triggers on pull request events and applies labels based on rules in .github/labeler.yml.

Local Testing & Linting

Run before every push:

# Format and imports
go fmt ./...
goimports -w .

# Run all tests
go test ./... -cover

# Lint
golangci-lint run

Submitting a Pull Request

  1. Fork and clone the repo.
  2. Create a branch with an appropriate prefix.
  3. Implement changes; update or add tests.
  4. Run formatting, tests, linting.
  5. Push and open a PR against main, linking relevant issues.
  6. Ensure CI passes; address review feedback.
  7. Squash or rebase commits as requested.

Practical Tips

  • Keep PRs focused on a single concern.
  • Write clear, conventional commit messages.
  • Update documentation when adding or changing behavior.
  • Rebase frequently against main to minimize conflicts.
  • Use make ci (if available) to replicate CI steps locally.