Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Select Xcode 16.4
uses: maxim-lobanov/setup-xcode@v1
- name: Install Swift 6.2
uses: compnerd/gha-setup-swift@main
with:
xcode-version: '16.4'
source: swift.org
swift-version: swift-6.2-release
swift-build: 6.2-RELEASE
- name: Toolchain diagnostics
run: |
xcodebuild -version
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ Secrets/
*.profraw
coverage/

# Local reports (not committed)
# Local reports / debug logs (not committed)
advaithpr.ts
VoiceActivity.txt
percentage.txt
Reports/
errors/

# Internal planning documents
v2.0.0.txt

# Carthage / CocoaPods (not used, but ignore if present)
Carthage/
Expand Down
204 changes: 204 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,210 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-03-02

### Overview
SwiftDisc 2.0.0 is a major release delivering a complete Swift 6 strict-concurrency
migration, typed throws throughout the REST layer, 32 new gateway event callbacks,
a fully expanded Guild model, critical bug fixes, and high-impact developer-experience
improvements including `message.reply()`, `client.sendDM()`, typed slash-option
accessors, `EmbedBuilder.timestamp(Date)`, a public `CooldownManager`, filtered
event-stream helpers, and a background cache-eviction task.

### Breaking Changes
- **`DiscordClient`** is now a `public actor` (was `public final class`). All method
calls from outside the actor require `await`. Immutable `let` properties (`token`,
`cache`) remain accessible without `await`.
- **`CommandRouter`**, **`SlashCommandRouter`**, **`AutocompleteRouter`**,
**`ShardManager`** are now `actor` types. All mutating methods (e.g. `register`,
`registerPath`, `useCommands`) require `await`.
- **`CooldownManager`** is now a `public actor`. `isOnCooldown` and `setCooldown`
require `await`.
- All stored handler closures (`onReady`, `onMessage`, `onVoiceFrame`, `Handler`
typealiases, etc.) are now `@Sendable`.
- `SwiftDiscExtension` protocol now requires `Sendable` conformance.
- `VoiceAudioSource` protocol now requires `Sendable` conformance.
- `WebSocketClient` protocol now requires `Sendable` conformance.
- `HTTPClient` REST methods now declare `throws(DiscordError)` instead of untyped
`throws`. Call sites using `catch { }` will need to handle `DiscordError` directly.
- `Guild` model expanded from 4 fields to ~50 fields — any code that relied on the
minimal stub may need to handle new optionals.

### Added — Developer Experience
- **`message.reply(...)`** — reply to any `Message` in one call. Sets
`message_reference` automatically and optionally suppresses the mention:
```swift
let response = try await message.reply(client: client, content: "Pong!")
let quiet = try await message.reply(client: client, content: "...", mention: false)
```
- **`client.sendDM(userId:content:embeds:components:)`** — open a DM channel and
send a message in a single awaitable call, replacing the two-step
`createDM` + `sendMessage` pattern:
```swift
try await client.sendDM(userId: userId, content: "Welcome to the server!")
```
- **`SlashCommandRouter.Context` typed option accessors** — resolve Discord
interaction option values to strongly-typed objects using the `resolved` map:
```swift
let target = ctx.user("target") // → User?
let destination = ctx.channel("channel") // → Interaction.ResolvedChannel?
let role = ctx.role("role") // → Interaction.ResolvedRole?
let file = ctx.attachment("upload") // → Interaction.ResolvedAttachment?
```
- **`EmbedBuilder.timestamp(_ date: Date)`** — pass a `Date` directly instead of
manually formatting an ISO 8601 string:
```swift
EmbedBuilder().timestamp(Date()).build()
```
- **`CooldownManager`** is now `public` — use it directly in any bot code, not just
inside the built-in command routers.
- **Filtered event-stream helpers** on `DiscordClient` — typed `AsyncStream`
subscriptions without manual `switch event` boilerplate:
```swift
for await message in client.messageEvents() { ... }
for await reaction in client.reactionAddEvents() { ... }
for await interaction in client.interactionEvents() { ... }
for await member in client.memberAddEvents() { ... }
```
- **`MessagePayload` fluent builder** — composable payload type covering every
message-send option. Automatically dispatches to multipart when files are present:
```swift
try await client.send(to: channelId, MessagePayload()
.content("Hello!")
.embed(EmbedBuilder().title("World").build())
.ephemeral()
.silent())
try await client.edit(channelId: cid, messageId: mid, MessagePayload().content("Updated"))
try await client.respond(to: interaction, with: MessagePayload().content("OK").ephemeral())
```
- **`WebhookClient`** — standalone token-free webhook client (uses `URLSession`
directly, no bot token required). Parse from URL or supply ID + token directly:
```swift
let hook = WebhookClient(url: "https://discord.com/api/webhooks/123/abc")!
let msg = try await hook.execute(content: "Hi!", wait: true)
try await hook.editMessage(messageId: msg!.id.rawValue, content: "Updated")
try await hook.deleteMessage(messageId: msg!.id.rawValue)
```
- **`EmojiRef` typed enum** — type-safe emoji references for all reaction APIs:
```swift
try await client.addReaction(channelId: cid, messageId: mid, emoji: .unicode("👍"))
try await client.addReaction(channelId: cid, messageId: mid, emoji: .custom(name: "uwu", id: emojiId))
```
- **`DiscordClient.archiveThread(channelId:locked:)`** — archive (and optionally
lock) a thread in one call via `PATCH /channels/{id}`.
- **`DiscordClient.syncCommands(_:guildId:)`** — smart command sync that fetches
existing commands, diffs the name sets, and only calls `bulkOverwrite` when there
is an actual change. Avoids rate-limit churn on every restart:
```swift
try await client.syncCommands(myCommands) // global
try await client.syncCommands(myCommands, guildId: guildId) // guild
```
- **Middleware for `CommandRouter` and `SlashCommandRouter`** — register
cross-cutting concerns (logging, auth gates, rate-limiting) independently of
command handlers:
```swift
router.use { ctx, next in
guard ctx.isAdmin else {
try await ctx.message.reply(client: ctx.client, content: "🚫 Admins only.")
return
}
try await next(ctx)
}
```
- **Permission helpers on `Context`** — both `CommandRouter.Context` and
`SlashCommandRouter.Context` gain `hasPermission(_:)`, `isAdmin`, and
`memberHasRole(_:)`:
```swift
guard ctx.isAdmin else { return }
guard ctx.hasPermission(1 << 5) else { return } // MANAGE_MESSAGES
guard ctx.memberHasRole(modRoleId) else { return }
```
- **Cache role + emoji storage** — `Cache` now stores per-guild roles and emojis:
```swift
cache.upsert(role: role, guildId: guildId)
let allRoles = cache.getRoles(guildId: guildId)
cache.upsert(emojis: guild.emojis ?? [], guildId: guild.id)
let emojis = cache.getEmojis(guildId: guildId)
```
- **`GuildMember.permissions`** — added `permissions: String?` field that Discord
includes in interaction and some gateway member payloads (effective permission
bitfield as a decimal string).

### Added — Discord API Coverage
- **32 new gateway event callbacks** added to `DiscordClient` — every previously
missing event now has a dedicated `@Sendable` callback property:
- `onMessageDeleteBulk`, `onReactionAdd`, `onReactionRemove`,
`onReactionRemoveAll`, `onReactionRemoveEmoji`
- `onGuildUpdate`, `onGuildDelete`
- `onGuildMemberAdd`, `onGuildMemberRemove`, `onGuildMemberUpdate`
- `onChannelCreate`, `onChannelUpdate`, `onChannelDelete`
- `onThreadCreate`, `onThreadUpdate`, `onThreadDelete`
- `onGuildRoleCreate`, `onGuildRoleUpdate`, `onGuildRoleDelete`
- `onGuildBanAdd`, `onGuildBanRemove`, `onAutoModerationActionExecution`
- `onInteractionCreate`
- `onTypingStart`, `onPresenceUpdate`, `onVoiceStateUpdate`
- `onGuildScheduledEventCreate`, `onGuildScheduledEventUpdate`, `onGuildScheduledEventDelete`
- `onPollVoteAdd`, `onPollVoteRemove`
- `onEntitlementCreate`, `onEntitlementUpdate`, `onEntitlementDelete`
- `onSoundboardSoundCreate`, `onSoundboardSoundUpdate`, `onSoundboardSoundDelete`
- **Full `Guild` model** — expanded from a 4-field stub to a complete ~50-field
model matching Discord's `GUILD_CREATE` and REST guild response, including
`roles`, `emojis`, `features`, `stickers`, `members`, `channels`, `threads`,
`presences`, `stage_instances`, `guild_scheduled_events`, and more.
- **`EventDispatcher` full rewire** — all previously-stubbed `break` dispatches now
invoke the appropriate callbacks. `GUILD_CREATE` seeds the full channel, thread,
and member user cache. Presence, member, and thread events update the cache.

### Fixed
- **HTTPClient double-wrap bug** — errors thrown inside the inner `do {}` block
(e.g. `DiscordError.decoding`, `.http`, `.api`) were being caught by the outer
`catch {}` and re-wrapped as `DiscordError.network(DiscordError.xxx)`. Fixed by
inserting `catch let de as DiscordError { throw de }` before each generic catch.
- **Soundboard gateway event names** — three event strings were incorrect, causing
soundboard events to be silently dropped:
- `SOUND_BOARD_SOUND_CREATE/UPDATE/DELETE` → `SOUNDBOARD_SOUND_CREATE/UPDATE/DELETE`
- **`DiscordError` not `Sendable`** — error values could not safely cross actor
boundaries. Added explicit `Sendable` conformance.

### Changed — Concurrency Architecture
- **`Package.swift`**: upgraded to `swift-tools-version:6.2` with
`.swiftLanguageMode(.v6)` for both `SwiftDisc` and `SwiftDiscTests` targets.
- **`DiscordClient`**: converted to `actor`. Callback properties typed as
`@Sendable`. All REST and gateway methods are actor-isolated async. `let`
properties are `nonisolated`.
- **`CommandRouter`**, **`SlashCommandRouter`**, **`AutocompleteRouter`**: converted
to `actor`. `Handler` typealiases updated to `@Sendable`. `Context` types conform
to `Sendable`. Static helpers marked `nonisolated`.
- **`ShardManager`**: converted to `actor`.
- **`CooldownManager`**: replaced `NSLock`-guarded `final class` with a clean
`public actor`. All synchronization is now compiler-enforced.
- **`EventDispatcher`**: all `client.*` property accesses use `await`.
- **`GatewayClient`**: `connect` and `readLoop` `eventSink` parameters typed
`@escaping @Sendable`. Soundboard event name strings corrected.
- **`ViewManager`**: `ViewHandler` typealias is now `@Sendable`.
- **`Cache`**: added background periodic eviction task (runs every 60 s) for
entries with configured TTL. No longer requires manual `pruneIfNeeded()` calls.

### Changed — REST Layer
- **Typed throws** — all `HTTPClient` and `RateLimiter` methods now declare
`throws(DiscordError)`. Call sites no longer need `as? DiscordError` casts.
- **`DiscordError`**: added `Sendable` conformance, `.unavailable` case (replaces
the internal `HTTPUnavailable` struct), and doc comments on every case.
- **`RateLimiter.waitTurn(routeKey:)`**: `throws(DiscordError)`. Wraps
`Task.sleep` `CancellationError` as `DiscordError.cancelled`.

### Changed — Types Marked Sendable
- `Box<T>` (`Message.swift`): `@unchecked Sendable`.
- `HTTPClient`: `@unchecked Sendable`.
- `VoiceClient`, `VoiceGateway`, `RTPVoiceSender`, `RTPVoiceReceiver`,
`PipeOpusSource`, `URLSessionWebSocketAdapter`: `@unchecked Sendable`.
- `VoiceEncryptor`, `OpusFrame`, `VoiceAudioSource`, `WebSocketClient`,
`DiscordEvent`: explicit `Sendable` conformance.

### Changed — Tests
- All test methods calling actor methods updated with `await`.
- `CooldownTests.testCooldownSetAndCheck()` made `async`.

## [1.3.1] - 2026-02-22

### Fixed
Expand Down
14 changes: 7 additions & 7 deletions InstallGuide.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ SwiftDisc is a Swift-native Discord API library supporting iOS, macOS, tvOS, wat

Requirements
-----------
- Swift 5.9+
- On Apple platforms: Xcode 15+ recommended
- Swift 6.2+
- On Apple platforms: Xcode 16.4+ recommended
- A Discord Bot token from the Developer Portal
- If using message content, ensure the privileged intent is enabled on your application

Expand All @@ -17,15 +17,15 @@ Install via Swift Package Manager (SPM)
Option A) Add to Package.swift (for server/CLI projects)

1) In your Package.swift dependencies, add:
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "0.10.2")
.package(url: "https://github.com/M1tsumi/SwiftDisc.git", from: "2.0.0")

2) Add "SwiftDisc" to your target dependencies.

Option B) Add to Xcode project (for App targets)

1) Xcode > File > Add Packages...
2) Enter URL: https://github.com/M1tsumi/SwiftDisc.git
3) Choose version: Up to Next Major from 0.10.2
3) Choose version: Up to Next Major from 2.0.0
4) Add the SwiftDisc product to your app target

Configuring your Token
Expand Down Expand Up @@ -71,14 +71,14 @@ struct BotMain {

Windows Notes
-------------
- If URLSessionWebSocketTask is unavailable on your toolchain, the current Gateway adapter will report that WebSocket is unavailable
- A dedicated Windows WebSocket adapter is planned; track the project CHANGELOG for updates
- SwiftDisc uses URLSessionWebSocketTask for gateway connections. On Windows, this is fully supported via the Swift 6.2+ toolchain.
- If you encounter connection issues, confirm you are running Swift 6.2 or later.

Troubleshooting
---------------
- Invalid token: Confirm DISCORD_TOKEN is set and correct
- No events received: Check intents; ensure messageContent privileged intent is enabled if you expect full message bodies
- Rate limiting: REST requests are minimally throttled; heavy usage may require per-route buckets (planned)
- Rate limiting: Per-route and global rate limit buckets are fully implemented with exponential backoff. If you encounter 429 responses, avoid tight concurrent loops to the same endpoint.
- Gateway disconnects: The client has basic heartbeat ACK tracking and will attempt reconnect; intermittent networks may still cause delays

Support & Documentation
Expand Down
13 changes: 10 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.2
import PackageDescription

let package = Package(
Expand All @@ -13,7 +13,14 @@ let package = Package(
.library(name: "SwiftDisc", targets: ["SwiftDisc"])
],
targets: [
.target(name: "SwiftDisc"),
.testTarget(name: "SwiftDiscTests", dependencies: ["SwiftDisc"])
.target(
name: "SwiftDisc",
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "SwiftDiscTests",
dependencies: ["SwiftDisc"],
swiftSettings: [.swiftLanguageMode(.v6)]
)
]
)
Loading