Skip to content

Next#16

Open
M1tsumi wants to merge 89 commits into
mainfrom
next
Open

Next#16
M1tsumi wants to merge 89 commits into
mainfrom
next

Conversation

@M1tsumi
Copy link
Copy Markdown
Owner

@M1tsumi M1tsumi commented May 12, 2026

Summary by CodeRabbit

  • New Features

    • Typed application emoji management
    • Paginated poll-answer voter retrieval
    • Bulk member banning (client-side cap: 200)
    • Soundboard playback in voice channels
    • Slash command localization and richer choice values
  • Improvements

    • Audit-log reason supported for REST and multipart requests
    • Improved request concurrency and rate-limit handling
    • Guild member search now uses POST-based search
    • Sticker creation accepts audit reasons
    • Text input style now uses raw numeric values
    • Comprehensive DocC API docs + GitHub Pages deployment; version bumped to 2.3.1
  • Chores

    • Examples packaging fixes; local failed/ ignored in .gitignore

Review Change Stack

M1tsumi added 26 commits May 12, 2026 12:42
Move local development files to .local/ directory
and update .gitignore to ignore the folder instead of
individual files. This keeps local development artifacts
out of the repository.
Replace [String:Int] decoding with SeqProbe struct to correctly
extract sequence number from gateway dispatch frames. The previous
implementation failed because real frames contain op, d (object), and
t (string) fields, not just integers. This fixes RESUME functionality
and heartbeat sequence tracking.
Include major parameters (channel_id, guild_id, webhook_id) in
rate-limit bucket keys to properly isolate buckets per resource.
Previous implementation lumped all channels/guilds into the same
bucket, causing serialization of unrelated requests and 429 errors.
Now uses format: METHOD:path|major=major_param
Add ensureChannelStub method to Cache that only inserts a stub
channel when the key is missing. Update EventDispatcher to use this
instead of upsert, preventing overwrites of full Channel objects with
stubs that only contain id and type. This preserves channel metadata
like name, topic, and permission_overwrites for permission checks.
Decode the boolean d field from INVALID_SESSION opcode to determine
if the session is resumable. Only clear session state if d is false.
If d is true, sleep 1-5 seconds and retry RESUME instead of
unconditionally throwing away recoverable sessions.
Remove deprecated $ prefix from os, browser, and device keys
in IdentifyConnectionProperties. Discord's current spec uses os,
browser, and device without the $ prefix. The deprecated keys are
tolerated but flagged as deprecated client by anti-spam systems.
Set maximumMessageSize to 16 MiB to handle large gateway
payloads like GUILD_CREATE. The default 1 MB limit can cause
socket closure with code 1009 for large guilds with many
members, channels, roles, threads, and stickers.
Ensure URLError.cancelled is checked before retry logic in the
outer catch block. Cancellation should always be terminal and
should not trigger retry attempts. The check now happens before
the attempt < maxAttempts check.
Add disconnected(reason:) event to DiscordEvent to surface fatal
disconnects. Add closeCode property to WebSocketClient protocol
and implement it in adapters. Check for fatal 4000-series close
codes before attempting reconnection and surface descriptive
errors. Stop reconnection on fatal codes like 4004 (auth failed),
4011 (sharding required), 4013/4014 (invalid intents). Surface
disconnected event when max reconnect attempts are reached.
Add resume_gateway_url field to ReadyEvent and store it in
GatewayClient. Use the resume URL when connecting instead of
the default gateway URL. This is required by Discord for v10
resumes and reduces latency by avoiding redirects.
Mark zlib-stream and zstd-stream compression options as
unavailable until decompression is implemented. Enabling these
currently silently breaks the bot since the readLoop only handles
string and data frames as raw JSON, not compressed binary frames.
Move attachment descriptors into payload_json instead of creating
separate multipart parts per file. Discord expects a single
attachments JSON array embedded in the payload_json part, not
multiple attachments form-data parts. This fixes the issue where
multiple files with descriptions would create duplicate multipart
parts that Discord rejects.
Fix four compilation errors introduced in v2.3 correctness patch:
1. Add disconnected case to EventDispatcher switch for exhaustiveness
2. Fix resume_gateway_url String? to URL conversion
3. Replace non-existent attemptResume with attemptReconnect
4. Remove optional chaining from non-optional URLSessionWebSocketTask.closeCode
Remove stray code after actor closing brace and remove non-existent
onDisconnect callback. The disconnected case now simply breaks
to satisfy switch exhaustiveness without calling undefined properties.
The prior syntax-fix commit restored the actor's closing brace and the func's closing brace but left the switch statement unterminated, so all three CI builds (macOS, Ubuntu, Windows) failed with cascading 'expected ''}''' errors and spurious 'EventDispatcher has no member ''process''' diagnostics in DiscordClient.swift.

Add the missing '}' after the final '.disconnected' case so the switch, func, and actor all close correctly, restoring a parsable module and resolving every downstream error in the CI logs.
Bump version to 2.3.0 in DiscordConfiguration.swift and update
CHANGELOG.md with comprehensive fix details including EventDispatcher
syntax corrections, Swift 6.2 compilation fixes, multipart attachment
handling, gateway connection improvements, and WebSocket reliability
enhancements. Update README.md to reference version 2.3.0 and document
debugging capabilities including gateway decode diagnostics, rate limit
observability, typed error handling, router error handlers, and default
error logging.
Update isFatalCloseCode to return true only for explicit fatal codes
(4004, 4010, 4011, 4012, 4013, 4014) instead of treating the entire
4000-4999 range as fatal. This allows reconnection attempts for recoverable
close codes like 4000, 4007, 4008, 4009 while still blocking reconnection
for truly fatal authentication, sharding, and intent errors.
Fixed two P0 rate-limiting issues:

1. Bucket key generation now preserves major parameters
   (channel_id, guild_id, webhook_id) instead of replacing all
   snowflakes with :id. This ensures each channel/guild gets its
   own bucket, preventing unrelated requests from being serialized
   together.

2. Added per-bucket AsyncSemaphore to limit concurrent requests
   before bucket limits are known from headers. Defaults to 50
   concurrent requests per bucket (Discord's typical limit) and
   updates dynamically based on X-RateLimit-Limit headers.

These fixes prevent 429 errors in multi-guild scenarios by ensuring
proper bucket isolation and request serialization.

Resolves P0 bugs §1.3 and §1.11 from developer analysis.
Added optional reason parameter to all HTTPClient methods for
audit log tracking on destructive operations (bans, kicks,
deletions, etc.). The reason is URL-encoded and sent via the
X-Audit-Log-Reason header as specified by Discord's API.

This allows bot developers to provide context for moderation
actions in the guild audit log, improving accountability and
debugging capabilities.

Breaking change: All HTTPClient method signatures now include
an optional reason parameter. This is backward compatible
since the parameter has a default value of nil.

Resolves P1 feature §2.11 from developer analysis.
Implemented POST /guilds/{id}/bulk-ban endpoint supporting up to
200 user IDs per call. This is a 2024 Discord API endpoint that
significantly improves efficiency when banning multiple users.

The new bulkBanMembers method:
- Accepts an array of up to 200 user IDs
- Supports delete_message_seconds parameter (0-604800)
- Uses the new X-Audit-Log-Reason header for audit logging
- Returns BulkBanResponse with list of successfully banned users
- Validates the 200-user limit before making the request

Added BulkBanResponse model to GuildBan.swift with banned_users
array containing the IDs of users that were successfully banned.

This resolves P1 feature §2.15 from developer analysis.
Updated searchGuildMembers to use the 2024+ POST endpoint
/guilds/{id}/members-search instead of the deprecated GET
endpoint with query parameter.

The new POST endpoint:
- Uses request body instead of query parameters
- More consistent with Discord's modern API patterns
- Replaces the old GET /guilds/{id}/members/search?query=

This resolves P1 feature §2.4 from developer analysis.
Implemented GET /channels/{id}/polls/{message.id}/answers/{answer_id}
endpoint for paginated retrieval of users who voted for a specific
poll answer.

The new getPollAnswerVoters method:
- Accepts channelId, messageId, and answerId parameters
- Supports pagination via 'after' user ID and 'limit' (1-100)
- Returns PollAnswerUsers with user list and pagination metadata
- Default limit is 25 users per page

Added PollAnswerUsers model to Message.swift containing:
- has_more: Boolean indicating if more users exist
- users: Array of User objects who voted
- after: User ID for next pagination request

This enables efficient retrieval of poll voters for large polls
without fetching all data at once.

Resolves P1 feature §2.5 from developer analysis.
Updated application emoji methods to use correct Discord API path
/applications/{id}/emojis instead of the incorrect /app-emojis.

Changes:
- Renamed createAppEmoji to createApplicationEmoji (breaking change)
- Renamed updateAppEmoji to updateApplicationEmoji (breaking change)
- Renamed deleteAppEmoji to deleteApplicationEmoji (breaking change)
- Added listApplicationEmojis method to fetch all app emojis
- Changed return types from JSONValue to proper Emoji types
- Added typed parameters (name, roles) instead of generic JSONValue
- Changed emojiId parameter type from String to EmojiID

The application emoji endpoints allow bots to create and manage
emojis that are usable across all guilds, not just within
a single guild.

Resolves P1 feature §2.3 from developer analysis.
Fixed createGuildSticker to use Discord's specific multipart format
with individual form-data fields (name, description, tags, file)
instead of the standard payload_json shape.

Added postStickerMultipart method to HTTPClient that builds multipart
body with Discord's exact field layout:
- name: form-data field for sticker name
- description: optional form-data field for sticker description
- tags: form-data field for comma-separated tag string
- file: form-data field with the sticker image file

Updated createGuildSticker to use the new method and added support
for audit log reason parameter.

This fixes sticker uploads which were using the wrong format and
would be rejected by Discord's API.

Resolves P1 feature §2.14 from developer analysis.
Implemented POST /channels/{id}/send-soundboard-sound endpoint
for playing soundboard sounds in voice channels.

The new playSoundboardSound method:
- Accepts channelId (voice channel), soundId, and guildId
- Sends POST request with sound_id and guild_id in body
- Returns the SoundboardSound object
- Enables bots to play soundboard sounds programmatically

This complements the existing soundboard management methods
(list, create, modify, delete) by adding the ability to actually
play sounds in voice channels.

Resolves P1 feature §2.16 from developer analysis.
Added name_localizations and description_localizations fields to
ApplicationCommandCreate and ApplicationCommandOption structs to
support localization during command creation.

Changes:
- Added name_localizations: [String: String]? to ApplicationCommandCreate
- Added description_localizations: [String: String]? to ApplicationCommandCreate
- Added name_localizations: [String: String]? to ApplicationCommandOption
- Added description_localizations: [String: String]? to ApplicationCommandOption
- Added name_localizations: [String: String]? to ApplicationCommandOption.Choice

The setCommandLocalizations method already existed for updating
localizations after creation, but this allows setting localizations
during initial command creation, which is more efficient and follows
Discord's recommended pattern.

This enables bots to provide localized command names, descriptions,
and option names for different locales (e.g., en-US, es-ES, fr-FR).

Resolves P1 feature §2.2 from developer analysis.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-route semaphores, threads optional audit reason through HTTP methods (including multipart/sticker), rewrites route bucketing to preserve major IDs, and implements REST helpers/endpoints and model updates (application emojis, poll voters, bulk bans, search POST, stickers, soundboard), plus extensive docs and minor model tweaks.

Changes

HTTP Client and REST API Enhancements

Layer / File(s) Summary
Core models and contracts
Sources/SwiftDisc/Models/*
Adds BulkBanResponse and PollAnswerUsers, and minor model field additions (Channel.last_pin_timestamp, ThreadMetadata.create_timestamp, ShardStatusSnapshot.lastSequence, ShardingHealth.totalGuilds, DiscordConfiguration.version).
HTTPClient concurrency & headers
Sources/SwiftDisc/REST/HTTPClient.swift
Adds AsyncSemaphore and BucketSemaphores, per-route semaphore acquisition in executeWithRetry, and threading of optional reason into all REST and multipart helpers with X-Audit-Log-Reason header application.
Sticker multipart and route bucketing
Sources/SwiftDisc/REST/HTTPClient.swift
Adds postStickerMultipart/buildStickerMultipartBody and rewrites makeRouteKey to preserve major snowflakes and include `
DiscordClient REST surface changes
Sources/SwiftDisc/DiscordClient.swift
Adds raw REST passthroughs (rawGET/rawPOST/rawPATCH/rawPUT/rawDELETE), typed application emoji endpoints, getPollAnswerVoters, bulkBanMembers (≤200 client-side), playSoundboardSound, changes searchGuildMembers to POST body, and routes createGuildSticker through postStickerMultipart with optional reason.
Slash command payload updates
Sources/SwiftDisc/HighLevel/SlashCommandBuilder.swift, Sources/SwiftDisc/DiscordClient.swift
Adds localization fields (name_localizations/description_localizations) and changes Choice.value from String to JSONValue with initializer updates.
Message component API change
Sources/SwiftDisc/Models/MessageComponents.swift
MessageComponent.TextInput.style changed from TextInput.Style enum to raw Int; initializer updated accordingly.
Package, CI, and gitignore
Package.swift, .github/workflows/docs.yml, .gitignore
Adds example exclude helpers in manifest, adds Documentation GitHub Action to build/deploy DocC docs, and adds failed/ to .gitignore.
Documentation sweep
Sources/SwiftDisc/... (many files)
Extensive DocC/SwiftDoc additions across gateway, models, high-level APIs, and internal modules; mostly comment-only changes preserving behavior except where noted above.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant HTTPClient
  participant applyCustomHeaders
  participant DiscordAPI
  Caller->>HTTPClient: post(path, body, reason: "violation")
  HTTPClient->>applyCustomHeaders: applyCustomHeaders(headers, auditReason: reason)
  applyCustomHeaders->>applyCustomHeaders: URL-encode reason
  applyCustomHeaders-->>HTTPClient: X-Audit-Log-Reason header set
  HTTPClient->>DiscordAPI: POST with audit header
  DiscordAPI-->>HTTPClient: response
  HTTPClient-->>Caller: return response
Loading
sequenceDiagram
  participant HTTPClient
  participant BucketSemaphores
  participant AsyncSemaphore
  participant DiscordAPI
  HTTPClient->>BucketSemaphores: get(for: routeKey)
  BucketSemaphores->>AsyncSemaphore: get or create semaphore
  HTTPClient->>AsyncSemaphore: wait()
  AsyncSemaphore-->>HTTPClient: acquired permit (or await)
  HTTPClient->>DiscordAPI: execute request
  DiscordAPI-->>HTTPClient: response + X-RateLimit-Limit
  HTTPClient->>BucketSemaphores: updatePermits(limit)
  HTTPClient->>AsyncSemaphore: signal() via defer
  AsyncSemaphore-->>HTTPClient: release permit
  HTTPClient-->>HTTPClient: return response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • M1tsumi/SwiftDisc#15: Overlapping changes to Sources/SwiftDisc/REST/HTTPClient.swift—multipart handling and route-bucketing key logic.

Suggested labels

Review effort 3/5

Poem

🐰 I threaded reasons through each call,
Semaphores stand guard to pace them all,
Emojis, polls, and stickers sing,
Docs sprout wings and examples bring,
Hopped in to ship v2.3.1—hip, hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Next' is vague and does not clearly convey the specific changes in this pull request about documentation fixes and type annotation restoration. Use a more descriptive title such as 'Fix DocC documentation and restore missing type annotations' to clearly communicate the PR's primary objectives.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.17% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@M1tsumi
Copy link
Copy Markdown
Owner Author

M1tsumi commented May 12, 2026

@copilot resolve the merge conflicts in this pull request

Co-authored-by: M1tsumi <88093506+M1tsumi@users.noreply.github.com>
M1tsumi added 30 commits May 14, 2026 14:34
The previous code tolerated three consecutive missed ACKs before reconnecting. Discord's gateway spec expects a single missed heartbeat ACK to be treated as a zombied connection. Change the threshold from >= 3 to >= 1 so the client reconnects and resumes immediately after the first unacknowledged heartbeat, matching spec behavior.
Discord invalidates the session when a client sends close code 1000 or 1001.
The previous attemptReconnect always called socket.close, which sent a normal
closure. This forced a fresh identify on every reconnect instead of resuming.

Add forceClose to WebSocketClient protocol and implementations. forceClose
tears down the underlying connection without sending a clean close frame.
Update attemptReconnect to use forceClose so Discord preserves the session_id
for resume. Keep close for intentional user disconnects.
Discord limits each gateway connection to 120 sends per 60 seconds.
Add a token-bucket rate limiter actor (GatewaySendRateLimiter) that
refills at 2 tokens per second with a max of 120 tokens.

Introduce sendGatewayPayload and sendGatewayData helpers inside
GatewayClient so every outgoing payload goes through the limiter.
This covers Identify, Resume, Heartbeat, Presence Update, and
Request Guild Members. Heartbeats remain unaffected in practice
because they occur far below the limit, but they are now protected
from accidental bursts alongside other sends.
Discord rejects identify payloads sent faster than once per 5 seconds
per token. Add lastIdentifyAt tracking inside GatewayClient and
sleep for the remaining time before sending a fresh Identify. This
prevents 4008 (rate limited) or 4013 (invalid intents) style rejections
caused by rapid reconnects and keeps the bot compliant with gateway
session start limits.
Discord recommends fetching the gateway URL via GET /gateway/bot rather
than hardcoding wss://gateway.discord.gg. Add GatewayBotResponse and
SessionStartLimit models, implement fetchGatewayBot inside GatewayClient,
and cache the returned URL in cachedGatewayUrl.

On connect, if no resume_gateway_url or cached URL exists, fetch from REST
and fall back to the configuration default on failure. The cached URL is
reused across reconnects, while resume_gateway_url from READY still takes
precedence for resumable sessions.
Added guard check to prevent crash when charset is empty.
Returns empty string instead of crashing, following defensive
programming practices similar to the Seyfert library issue.
Build compiles cleanly and all 34 tests pass.
Adds disconnect alias, initial presence support, DiscordError.authenticationFailed,
and fatal gateway close code propagation to the v2.3.1 changelog.
Add JSONCoders for shared encoder/decoder instances across the library. Add OptionalField enum to distinguish absent, null, and value states for PATCH payloads. Add RetryPolicy for configurable transient failure retry behavior. Add RedactedToken wrapper to prevent token leakage in logs. Add DiscordAPIErrorBody to parse Discord's nested validation error responses. Add comprehensive unit tests in InternalTests.swift.
Add apiValidation case to handle Discord's nested validation error responses (e.g., Invalid Form Body). Add convenience properties: httpStatusCode, apiErrorCode, validationErrors, isRateLimited, isAuthenticationFailure, isCancelled, isTransient. These provide easy access to error metadata without pattern matching.
Replace local JSONEncoder/JSONDecoder instances with shared JSONCoders.encoder/decoder. Integrate RetryPolicy for configurable transient failure retry behavior. Add makeAPIError helper to parse Discord API errors including nested validation errors using DiscordAPIErrorBody. Use RetryPolicy for exponential backoff on network errors, cancellations, and 5xx responses.
Add proactive global rate limiting using a sliding 1-second window of 50 requests per second to prevent unnecessary 429s during cross-route bursts. Implement Discord bucket ID tracking with routeKeyToBucket mapping for accurate per-bucket state management. Add scope detection (user/global/shared) to prevent shared-scope limits from promoting to global reset. Add fallback to X-RateLimit-Reset epoch timestamp header.
Add messageToChannelIndex reverse index for O(1) message removal instead of O(n) channel scan. Update add(message:) to maintain reverse index and clean up removed messages when cap is exceeded. Update removeMessage(id:) to use reverse index for efficient lookup. Add removeGuild(id:) method to clear guild entry, roles, emojis, and associated data from cache.
Replace token String with RedactedToken to prevent accidental leakage in logs and errors. Replace local JSONEncoder/JSONDecoder with shared JSONCoders.encoder/decoder. Use token.rawValue for identify/resume payloads and token.authorizationHeaderValue for Authorization header. Update User-Agent header to DiscordBot format with repository URL.
Replace local JSONEncoder/JSONDecoder instances with shared JSONCoders.encoder/decoder. This ensures webhook payloads round-trip with the same key strategy as the rest of the library. The previous local coders applied .convertFromSnakeCase which silently broke Message decoding since models declare snake_case property names directly.
Change content parameter from String? to OptionalField<String> in DiscordClient.editMessage and editMessageWithFiles. Update MessagePayload.content to OptionalField<String> with default .absent. Add clearContent() method to explicitly send null to Discord. Add custom encode(to:) implementations to handle three-state encoding. This allows distinguishing between absent (do not change), null (clear field), and value (set field).
Add comprehensive documentation for Mentions, EmojiUtils, TimestampStyle, DiscordTimestamp, and MessageFormat enums. Fix MessageFormat.escapeSpecialCharacters() to only escape Discord's actual markdown metacharacters (\\, *, _, ~, |, >, ) and remove unnecessary escapes for parentheses, brackets, braces, plus, minus, equals, period, exclamation mark.
Call cache.removeGuild(id:) in EventDispatcher when handling guildDelete events. This ensures guild data, roles, emojis, and associated cache entries are cleaned up when a guild is deleted or the bot leaves a guild.
Add changelog entry for version 2.4.0 documenting internal utility infrastructure improvements including JSONCoders, OptionalField, RetryPolicy, RedactedToken, DiscordAPIErrorBody, cache optimizations, DiscordError enhancements, and various bug fixes.
Merge new internal utility infrastructure changes (JSONCoders, OptionalField, RetryPolicy, RedactedToken, DiscordAPIErrorBody, cache optimizations, DiscordError enhancements) into the existing 2.3.1 release entry instead of creating a new 2.4.0 version since 2.4.0 has not been released and is not planned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants