[](https://github.com/M1tsumi/SwiftDisc/actions/workflows/ci.yml)
[](https://github.com/M1tsumi/SwiftDisc/releases)
[](https://swift.org)
[](LICENSE)
[](https://discord.gg/tWyefRKKEH)
## Features
- **Single-actor client.** `DiscordClient` owns the gateway connection, REST client, event dispatcher, and cache in one place.
- **Automatic gateway management.** Reconnection, session resumption, heartbeats, and rate-limit backoff are handled internally.
- **Actor-based concurrency.** Built on Swift 6.2 structured concurrency with `async/await`. The cache, gateway, and REST client avoid locks by design.
- **Typed models.** Every Discord API object is a native Swift struct with `Codable`, `Hashable`, and `Sendable`. The REST client returns concrete types, not `[String: Any]`.
- **Event callbacks + async streams.** Use callback closures or `for await` on the unified event stream — or both at once.
- **Pluggable transports.** Default transports use URLSession. Swap in AsyncHTTPClient for proxy support on Linux, or conform to `HTTPTransport`/`WebSocketTransport` for custom networking.
- **Modular high-level APIs.** Separate routers for slash commands, prefix commands, autocomplete, views, webhooks, collectors, and cooldowns.
## Quick start (ping bot)
Create a new SwiftPM executable, add SwiftDisc as a dependency, and drop this in:
```swift
import Foundation
import SwiftDisc
@main
struct MyBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
await client.setOnReady { ready in
print("Logged in as \(ready.user.username)")
}
await client.setOnMessage { message in
guard message.content?.lowercased() == "ping" else { return }
do {
try await message.reply(client: client, content: "Pong!")
} catch {
print("Reply failed: \(error)")
}
}
do {
try await client.loginAndConnect(intents: [.guilds, .guildMessages, .messageContent])
let events = await client.events
for await _ in events { }
} catch {
print("Client failed: \(error)")
}
}
}
```
Set your token and run:
```bash
export DISCORD_BOT_TOKEN="your_token_here"
swift run
```
## Quick start (slash command)
```swift
import Foundation
import SwiftDisc
@main
struct SlashBot {
static func main() async {
let token = ProcessInfo.processInfo.environment["DISCORD_BOT_TOKEN"] ?? ""
let client = DiscordClient(token: token)
let slash = SlashCommandRouter()
await slash.register("ping") { ctx in
try await ctx.client.createInteractionResponse(
interactionId: ctx.interaction.id,
token: ctx.interaction.token,
content: "Pong!"
)
}
await client.useSlashCommands(slash)
try? await client.loginAndConnect(intents: [.guilds])
let events = await client.events
for await _ in events { }
}
}
```
## Installation
Add SwiftDisc via Swift Package Manager:
```swift
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "YourBot",
dependencies: [
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "2.5.0")
],
targets: [
.target(
name: "YourBot",
dependencies: ["SwiftDisc"]
)
]
)
```
### Platform support
| Platform | Minimum version |
|----------|----------------|
| macOS | 11+ |
| iOS | 14+ |
| tvOS | 14+ |
| watchOS | 7+ |
| Windows | Swift 6.2+ |
## How SwiftDisc works
SwiftDisc is split into these layers:
**`DiscordClient`** -- the main actor. It owns your Gateway connection, the REST client, the event dispatcher, and the cache. You will spend most of your time here.
**Gateway** -- manages the WebSocket connection to Discord's real-time event system. Handles heartbeats, reconnects, resuming sessions, and rate limits so you do not have to. Monitor connection state with `client.connectionState` and react to lifecycle events with `onResumed`, `onDisconnected`, and `onSessionInvalidated`.
**REST client** -- typed HTTP methods for every Discord API endpoint. Methods throw `DiscordError` with descriptive messages, and the built-in rate limiter keeps you under Discord's global and per-route limits.
**High-level modules** in [Sources/SwiftDisc/HighLevel](Sources/SwiftDisc/HighLevel):
| Module | What it does |
|--------|-------------|
| `SlashCommandRouter` | Register and handle slash commands with typed option accessors |
| `AutocompleteRouter` | Provide live search suggestions for command options |
| `CommandRouter` | Classic prefix-based commands (e.g. `!ping`) |
| `ViewManager` | Persistent UI views with `custom_id` matching |
| `WebhookClient` | Standalone token-free webhook execution |
| `MessagePayload` | Fluent builder for complex message payloads |
| `Collectors` | Streams for messages, reactions, and components |
| `CooldownManager` | Per-user or per-guild rate limiting for commands |
**Models** in [Sources/SwiftDisc/Models](Sources/SwiftDisc/Models) -- complete, typed Swift structs for every Discord API object, from Users and Channels to Interactions, Polls, Auto Moderation, Monetization SKUs, and Onboarding prompts.
**Cache** -- actor-safe in-memory store for users, channels, guilds, roles, emojis, and recent messages. Supports TTL expiration and LRU eviction to keep memory bounded. Inspect cache health with `cache.summary`, `cache.userCount`, etc.
### Transport system
The default transport uses URLSession and works everywhere. If you need proxy support on Linux or want to use AsyncHTTPClient, swap in the optional `SwiftDiscAHCTransport` module:
```swift
import SwiftDisc
import SwiftDiscAHCTransport
var config = DiscordConfiguration()
config.httpTransport = AHCTransport(
proxy: ProxyConfiguration(host: "proxy.corp.com", port: 8080)
)
let client = DiscordClient(token: token, configuration: config)
```
Conform to `HTTPTransport` or `WebSocketTransport` to integrate any networking library.
## Example programs
Run any example from the repo root with `swift run `:
| Example | What it shows | Run with |
|---------|--------------|----------|
| [PingBot](Examples/PingBot.swift) | Minimal message-based bot | `swift run PingBotExample` |
| [SlashBot](Examples/SlashBot.swift) | Slash command registration + response | `swift run SlashBotExample` |
| [CommandsBot](Examples/CommandsBot.swift) | Prefix commands (`!ping` style) | `swift run CommandsBotExample` |
| [AutocompleteBot](Examples/AutocompleteBot.swift) | Slash commands with live search suggestions | `swift run AutocompleteBotExample` |
| [ComponentsExample](Examples/ComponentsExample.swift) | Buttons, select menus, and interaction handling | `swift run ComponentsExample` |
| [ComponentsV2Bot](Examples/ComponentsV2Bot.swift) | The `IS_COMPONENTS_V2` flag, channel select menus, modal with Label+TextInput layout | `swift run ComponentsV2BotExample` |
| [ViewExample](Examples/ViewExample.swift) | Persistent UI views with ViewManager | `swift run ViewExample` |
| [FileUploadBot](Examples/FileUploadBot.swift) | Sending attachments with embeds | `swift run FileUploadBotExample` |
| [WebhookBot](Examples/WebhookBot.swift) | Create, execute, edit, and delete webhooks | `swift run WebhookBotExample` |
| [ShardingBot](Examples/ShardingBot.swift) | Sharded gateway connection with state monitoring | `swift run ShardingBotExample` |
All examples read the bot token from the `DISCORD_BOT_TOKEN` environment variable. Some also use `DISCORD_CHANNEL_ID` -- set them before running:
```bash
export DISCORD_BOT_TOKEN="your_token"
export DISCORD_CHANNEL_ID="your_channel_id"
swift run PingBotExample
```
## Event handling guide
You can handle gateway events with callbacks or an async stream.
### Option 1: Callback closures (simple, self-documenting)
Set individual callbacks for the events you care about:
```swift
await client.setOnReady { ready in
print("Bot is ready in \(ready.guilds.count) guilds")
}
await client.setOnMessage { message in
guard message.content == "ping" else { return }
try await message.reply(client: client, content: "Pong!")
}
await client.setOnGuildCreate { guild in
print("Joined \(guild.name), \(guild.member_count ?? 0) members")
}
```
Available callbacks: `onReady`, `onMessage`, `onMessageUpdate`, `onGuildCreate`, `onInteractionCreate`, `onReactionAdd`, `onMemberAdd`, and 30+ more -- one for every Discord gateway event.
### Option 2: Event AsyncStream (flexible, pattern-matching)
When you need to filter, combine, or transform events, use the unified stream:
```swift
for await event in await client.events {
switch event {
case .messageCreate(let msg) where msg.content == "ping":
try await msg.reply(client: client, content: "Pong!")
case .guildCreate(let guild):
print("New guild: \(guild.name)")
case .reactionAdd(let reaction):
guard reaction.emoji.name == "⭐" else { break }
print("Starred in channel \(reaction.channel_id)")
default:
break
}
}
```
The event stream is an `AsyncStream` -- you can use it with `filter`, `map`, `compactMap`, and other async algorithms. You can also use both callbacks and the stream at the same time.
### Connection state observability
Monitor Gateway connection state in real time:
```swift
for await state in await client.connectionState {
switch state {
case .ready: print("Connected and ready to receive events")
case .reconnecting: print("Connection lost, reconnecting...")
case .resuming: print("Resuming session with Discord")
case .disconnected: print("Fully disconnected")
case .connecting: print("Establishing initial connection")
case .identifying: print("Sending identify payload")
}
}
```
Or grab the current state synchronously: `let state = await client.gatewayStatus`.
### Lifecycle callbacks
React to connection lifecycle events:
```swift
await client.onResumed = { print("Session resumed -- missed events replayed") }
await client.onDisconnected = { reason in print("Disconnected: \(reason)") }
await client.onSessionInvalidated = { print("Session invalidated -- will re-identify on next connect") }
```
## Bot setup checklist
If your bot connects but does not receive events, check these in order:
1. **Is your token set?** -- `DISCORD_BOT_TOKEN` must be a valid bot token from the Discord Developer Portal.
2. **Are you requesting the right intents?** -- Pass them to `loginAndConnect(intents:)`. For example, to read message content you need `[.guilds, .guildMessages, .messageContent]`.
3. **Are privileged intents enabled?** -- In the Developer Portal, go to your app's Bot page and toggle `MESSAGE CONTENT INTENT`, `SERVER MEMBERS INTENT`, and `PRESENCE INTENT` as needed. These are required even if you pass them in code.
4. **Was the bot invited with the right scopes?** -- Use the OAuth2 URL generator in the Developer Portal and include the `bot` scope plus the permissions your bot needs.
5. **Is the bot in the guild?** -- The bot must be a member of the guild to receive events from it.
6. **Check for close codes** -- Watch console output for Gateway close codes like `4004` (bad token), `4013` (invalid intents), or `4014` (disallowed privileged intent).
## Reliability and debugging
Reconnection, rate-limit backoff, and session resumption are handled internally. Debugging tools:
**Gateway decode diagnostics** -- Enable `DiscordConfiguration.enableGatewayDecodeDiagnostics` to log payload decoding failures with opcode context and payload previews. Essential when adding support for new Discord features.
**Rate limit observability** -- Set `DiscordConfiguration.onRateLimit` to receive `RateLimitEvent` snapshots for REST bucket updates and waits. Useful for tuning request patterns.
**Structured logging** -- Provide a custom logger via `DiscordConfiguration.logger`. The built-in `DefaultDiscordLogger` uses `os_log` on Apple platforms and `print` on others. Implement the `DiscordLogger` protocol to route to your own backend.
**Pluggable HTTP and WebSocket transports** -- Swap out the default URLSession networking for a custom implementation. Useful when you need proxy support on Linux, want to use AsyncHTTPClient, or need fine-grained control over connection behavior.
**Typed error handling** -- All operations throw `DiscordError` with descriptive messages. Use convenience properties to inspect errors:
```swift
catch let error as DiscordError {
if error.isRateLimited { /* back off */ }
if error.isAuthenticationFailure { /* token expired */ }
if let statusCode = error.httpStatusCode { /* 400, 404, 429 etc */ }
if let validationErrors = error.validationErrors { /* per-field failures */ }
}
```
**Router error handlers** -- `CommandRouter`, `SlashCommandRouter`, and `ViewManager` support custom error handlers that receive context about the failed operation. Set them during initialization.
**Cache statistics** -- Inspect cache contents any time:
```swift
print(await cache.summary)
// "Cache: 843 users, 127 channels, 5 guilds, 3412 messages in 34 channels, ..."
```
## Documentation map
| Resource | What you will find |
|----------|-------------------|
| [**GitHub Pages**](https://M1tsumi.github.io/SwiftDisc/) | DocC documentation for SwiftDisc -- API reference with search |
| [**CHANGELOG.md**](CHANGELOG.md) | Per-release changelog following Keep a Changelog |
| [**CONTRIBUTING.md**](CONTRIBUTING.md) | How to set up, build, test, and submit PRs |
| [**Examples/README.md**](Examples/README.md) | Quick-start guides for every example bot |
| `SwiftDiscAHCTransport` | Optional AsyncHTTPClient transport. Add `.product(name: "SwiftDiscAHCTransport", package: "SwiftDisc")` to use it. Supports proxies on Linux |
| [**CODE_OF_CONDUCT.md**](CODE_OF_CONDUCT.md) | Community standards and expectations |
You can also build the docs locally:
```bash
# Requires swift-docc-plugin (add it to Package.swift first)
swift package --allow-writing-to-directory generate-documentation --target SwiftDisc --output-path docs --transform-for-static-hosting
```
Then open `docs/index.html` in a browser.
## Community and support
- **Discord server** -- [https://discord.gg/tWyefRKKEH](https://discord.gg/tWyefRKKEH) -- get help, discuss features, show off your bot
- **GitHub Issues** -- [https://github.com/M1tsumi/SwiftDisc/issues](https://github.com/M1tsumi/SwiftDisc/issues) -- report bugs and request features
- **GitHub Discussions** -- available on the repo for longer conversations
## License
MIT. See [LICENSE](LICENSE).