├── .github └── workflows │ ├── generate-docc.yml │ └── lint-and-test.yml ├── .gitignore ├── .idea └── .gitignore ├── .swiftlint.yml ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── DiscordKit │ ├── Extensions │ │ └── Presence+.swift │ ├── Gateway │ │ ├── DiscordGateway.swift │ │ └── GatewayCachedState.swift │ ├── Objects │ │ └── REST │ │ │ └── UserSettingsProtoUpdate.swift │ ├── REST │ │ └── APIUser+.swift │ └── protos │ │ ├── DiscordProtos.pb.swift │ │ ├── FavoriteChannel.pb.swift │ │ ├── FavoriteGIFs.pb.swift │ │ ├── GuildFolders.pb.swift │ │ ├── Settings │ │ ├── AllGuildSettings.pb.swift │ │ ├── Appearance.pb.swift │ │ ├── Audio.pb.swift │ │ ├── Debug.pb.swift │ │ ├── GameLibrary.pb.swift │ │ ├── Inbox.pb.swift │ │ ├── Localization.pb.swift │ │ ├── Notification.pb.swift │ │ ├── Privacy.pb.swift │ │ ├── TextAndImages.pb.swift │ │ ├── UserContent.pb.swift │ │ └── VoiceAndVideo.pb.swift │ │ └── Status.pb.swift ├── DiscordKitBot │ ├── ApplicationCommand │ │ ├── AppCommandBuilder.swift │ │ ├── CommandData.swift │ │ ├── NewAppCommand.swift │ │ └── Option │ │ │ ├── BooleanOption.swift │ │ │ ├── CommandOption.swift │ │ │ ├── IntegerOption.swift │ │ │ ├── NumberOption.swift │ │ │ ├── OptionBuilder.swift │ │ │ ├── StringOption.swift │ │ │ └── SubCommand.swift │ ├── BotMessage.swift │ ├── Client.swift │ ├── Embed │ │ ├── BotEmbed.swift │ │ ├── BotEmbedBuilder.swift │ │ └── Field │ │ │ └── EmbedFieldBuilder.swift │ ├── MessageComponent │ │ ├── ActionRow.swift │ │ ├── Button.swift │ │ └── ComponentBuilder.swift │ ├── NCWrapper.swift │ ├── NotificationNames.swift │ ├── Objects │ │ ├── InteractionResponse.swift │ │ └── WebhookResponse.swift │ └── REST │ │ └── APICommand.swift └── DiscordKitCore │ ├── APIUtils.swift │ ├── DiscordKitConfig.swift │ ├── DiscordREST.swift │ ├── Documentation.docc │ └── DiscordKit.md │ ├── Extensions │ ├── Collection+Identifiable.swift │ ├── Int+decodeFlags.swift │ ├── Logger+.swift │ ├── Objects │ │ ├── Message+.swift │ │ └── User+.swift │ ├── Snowflake+decode.swift │ ├── String+random.swift │ ├── URL+.swift │ └── URLSession+.swift │ ├── Gateway │ ├── DecompressionEngine.swift │ ├── GatewayIdentify.swift │ ├── Intents.swift │ ├── README.md │ └── RobustWebSocket.swift │ ├── Objects │ ├── Data │ │ ├── Activity.swift │ │ ├── AppCommand.swift │ │ ├── Application.swift │ │ ├── Attachment.swift │ │ ├── Channel.swift │ │ ├── Connection.swift │ │ ├── Embed.swift │ │ ├── Emoji.swift │ │ ├── Guild.swift │ │ ├── Integration.swift │ │ ├── Interaction.swift │ │ ├── Levels.swift │ │ ├── Locale.swift │ │ ├── Member.swift │ │ ├── Mention.swift │ │ ├── Message.swift │ │ ├── Nonce.swift │ │ ├── Permission.swift │ │ ├── Presence.swift │ │ ├── Reaction.swift │ │ ├── Snowflake.swift │ │ ├── Stage.swift │ │ ├── Sticker.swift │ │ ├── Team.swift │ │ ├── User+Flags.swift │ │ ├── User+PremiumType.swift │ │ ├── User.swift │ │ └── Voice.swift │ ├── Gateway │ │ ├── ApplicationObj.swift │ │ ├── DataStructs.swift │ │ ├── Event │ │ │ ├── ChUnreadUpdate.swift │ │ │ ├── ChannelPinsUpdate.swift │ │ │ ├── GatewayEvent.swift │ │ │ ├── GatewaySettingsProtoUpdate.swift │ │ │ ├── GuildBan.swift │ │ │ ├── GuildMemberEvt.swift │ │ │ ├── GuildMembersChunk.swift │ │ │ ├── GuildMiscUpdate.swift │ │ │ ├── GuildRoleEvt.swift │ │ │ ├── GuildSchEvtUserEvt.swift │ │ │ ├── MessageACKEvt.swift │ │ │ ├── MessageDelete.swift │ │ │ ├── ReadyEvt.swift │ │ │ ├── ReadySuppEvt.swift │ │ │ ├── ThreadListSync.swift │ │ │ ├── ThreadMembersUpdate.swift │ │ │ ├── TypingStart.swift │ │ │ └── TypingStartEvt.swift │ │ ├── Gateway.swift │ │ ├── GatewayIO.swift │ │ ├── UserAccount │ │ │ └── ReadState.swift │ │ └── UserSettings.swift │ ├── README.md │ └── REST │ │ ├── LogOut.swift │ │ ├── MessageReadAck.swift │ │ ├── NewMessage.swift │ │ └── ResolvedInvite.swift │ ├── REST │ ├── APIAchievements.swift │ ├── APIApplicationCommands.swift │ ├── APIApplicationRoleConnectionMetadata.swift │ ├── APIAuditLog.swift │ ├── APIAutoModeration.swift │ ├── APIChannel.swift │ ├── APICurrentUser.swift │ ├── APIEmoji.swift │ ├── APIGateway.swift │ ├── APIGuild.swift │ ├── APIGuildScheduledEvent.swift │ ├── APIGuildTemplate.swift │ ├── APIInvite.swift │ ├── APILobbies.swift │ ├── APIMultipartFormBody.swift │ ├── APIOAuth2.swift │ ├── APIReceivingandResponding.swift │ ├── APIRequest.swift │ ├── APIStageInstance.swift │ ├── APISticker.swift │ ├── APIStore.swift │ ├── APIUser.swift │ ├── APIVoice.swift │ ├── APIWebhook.swift │ └── README.md │ └── Utils │ ├── DecodeThrowable.swift │ ├── DiscordRange.swift │ ├── EventDispatch.swift │ ├── HashedAsset.swift │ ├── HybridSnowflake.swift │ └── NullEncodable.swift └── Tests └── DiscordKitCommonTests └── PermissionTests.swift /.github/workflows/generate-docc.yml: -------------------------------------------------------------------------------- 1 | name: Generate DocC 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 11 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: false 15 | 16 | env: 17 | GH_USER: cryptoAlgorithm 18 | BUILD_TARGET: DiscordKitBot 19 | 20 | jobs: 21 | generate: 22 | runs-on: macos-12 23 | env: 24 | BUILD_DIR: _docs/ 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | # - uses: webfactory/ssh-agent@v0.5.4 30 | # with: 31 | # ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 32 | # - name: Init submodules 33 | # env: 34 | # TOKEN: ${{ secrets.GH_TOKEN }} 35 | # USER: ${{ env.GH_USER }} 36 | # run: | 37 | # git config --system credential.helper store 38 | # echo "https://$USER:$TOKEN@github.com" > ~/.git-credentials 39 | # git submodule update --init 40 | 41 | ########################## 42 | ## Select Xcode 43 | ########################## 44 | - name: Select Xcode 14.2 45 | run: sudo xcode-select -s /Applications/Xcode_14.2.app 46 | 47 | ########################## 48 | ## Cache 49 | ########################## 50 | - name: Cache Swift Build 51 | uses: actions/cache@v3 52 | with: 53 | path: .build 54 | key: swift-build-cache 55 | 56 | ########################## 57 | ## Generate Docs 58 | ########################## 59 | - name: Generate DocC 60 | run: mkdir -p ${{ env.BUILD_DIR }} && 61 | swift package --allow-writing-to-directory ${{ env.BUILD_DIR }} 62 | generate-documentation --target ${{ env.BUILD_TARGET }} --disable-indexing --transform-for-static-hosting 63 | --hosting-base-path DiscordKit --output-path ${{ env.BUILD_DIR }} 64 | 65 | ########################## 66 | ## Upload generated pages 67 | ########################## 68 | - name: Upload artifact 69 | uses: actions/upload-pages-artifact@v1 70 | with: 71 | path: ${{ env.BUILD_DIR }} 72 | 73 | deploy: 74 | needs: generate 75 | 76 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 77 | permissions: 78 | pages: write # to deploy to Pages 79 | id-token: write # to verify the deployment originates from an appropriate source 80 | 81 | # Deploy to the github-pages environment 82 | environment: 83 | name: github-pages 84 | url: ${{ steps.deployment.outputs.page_url }} 85 | 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Deploy to GitHub Pages 89 | uses: actions/deploy-pages@v2 90 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] # Running on every branch causes double runs for PR commits 6 | pull_request: 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | # The type of runner that the job will run on 14 | runs-on: macos-12 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Xcode Select 20 | uses: maxim-lobanov/setup-xcode@v1 21 | with: 22 | xcode-version: '14.1' 23 | 24 | - name: Cache Swift Build 25 | uses: actions/cache@v3 26 | with: 27 | path: .build 28 | key: swift-build-cache 29 | 30 | # Runs a single command using the runners shell 31 | - name: Build 32 | run: swift build 33 | 34 | test: 35 | runs-on: macos-12 36 | needs: [build] 37 | 38 | steps: 39 | - uses: actions/checkout@v3 40 | 41 | - name: Xcode Select 42 | uses: maxim-lobanov/setup-xcode@v1 43 | with: 44 | xcode-version: '14.1' 45 | 46 | - name: Cache Swift Build 47 | uses: actions/cache@v3 48 | with: 49 | path: .build 50 | key: swift-build-cache # -${{ hashFiles('**/*.swift') }} 51 | 52 | - name: Test 53 | run: swift test 54 | 55 | lint: 56 | runs-on: ubuntu-latest 57 | needs: [build] 58 | 59 | steps: 60 | - uses: actions/checkout@v1 61 | 62 | # Don't need submodules for linting 63 | - name: SwiftLint 64 | uses: norio-nomura/action-swiftlint@3.2.1 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .DS_Store 3 | .build/ 4 | .swiftpm/ 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - unused_closure_parameter 4 | - multiple_closures_with_trailing_closure 5 | - large_tuple 6 | - todo # TODOs are precisely for reminding me of tasks I'll have to do in the future. If they are flagged as violations, it completely defeats the point. 7 | - file_length 8 | opt_in_rules: 9 | 10 | force_cast: warning 11 | force_try: warning 12 | 13 | excluded: 14 | - Sources/DiscordKit/protos 15 | - .build 16 | 17 | identifier_name: 18 | min_length: 19 | warning: 3 20 | error: 0 21 | max_length: 22 | warning: 40 23 | error: 50 24 | allowed_symbols: ["_"] 25 | cyclomatic_complexity: 26 | ignores_case_statements: true 27 | nesting: 28 | type_level: 29 | warning: 5 30 | error: 8 31 | 32 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) 33 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "bitbytedata", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/tsolomko/BitByteData", 7 | "state" : { 8 | "revision" : "b4b41619522aacd7aae7b02fa8360833e796a03d", 9 | "version" : "2.0.2" 10 | } 11 | }, 12 | { 13 | "identity" : "opencombine", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/OpenCombine/OpenCombine.git", 16 | "state" : { 17 | "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", 18 | "version" : "0.14.0" 19 | } 20 | }, 21 | { 22 | "identity" : "reachability.swift", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/ashleymills/Reachability.swift", 25 | "state" : { 26 | "revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", 27 | "version" : "5.1.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swcompression", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/tsolomko/SWCompression.git", 34 | "state" : { 35 | "revision" : "cd39ca0a3b269173bab06f68b182b72fa690765c", 36 | "version" : "4.8.5" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-docc-plugin", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-docc-plugin", 43 | "state" : { 44 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6", 45 | "version" : "1.0.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-log", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-log.git", 52 | "state" : { 53 | "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", 54 | "version" : "1.4.4" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-nio", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-nio.git", 61 | "state" : { 62 | "revision" : "124119f0bb12384cef35aa041d7c3a686108722d", 63 | "version" : "2.40.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-nio-ssl", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-nio-ssl.git", 70 | "state" : { 71 | "revision" : "1750873bce84b4129b5303655cce2c3d35b9ed3a", 72 | "version" : "2.19.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-protobuf", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-protobuf.git", 79 | "state" : { 80 | "revision" : "88c7d15e1242fdb6ecbafbc7926426a19be1e98a", 81 | "version" : "1.20.2" 82 | } 83 | }, 84 | { 85 | "identity" : "websocket.swift", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/tesseract-one/WebSocket.swift.git", 88 | "state" : { 89 | "revision" : "9f616c35127c83651d3112f8bdb41284d3c5c213", 90 | "version" : "0.2.0" 91 | } 92 | } 93 | ], 94 | "version" : 2 95 | } 96 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DiscordKit", 8 | platforms: [ 9 | .macOS(.v11), 10 | .iOS(.v15) 11 | ], 12 | products: [ 13 | .library(name: "DiscordKitCore", targets: ["DiscordKitCore"]), 14 | .library(name: "DiscordKit", targets: ["DiscordKit"]), // User-oriented module, simplifies use of API for UI apps 15 | .library(name: "DiscordKitBot", targets: ["DiscordKitBot"]) // Bot-oriented module, for use in bots 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/ashleymills/Reachability.swift", from: "5.1.0"), 19 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 20 | .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.6.0"), 21 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 22 | .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"), 23 | .package(url: "https://github.com/tsolomko/SWCompression.git", from: "4.8.0"), 24 | .package(url: "https://github.com/tesseract-one/WebSocket.swift.git", from: "0.2.0") 25 | ], 26 | targets: [ 27 | .target( 28 | name: "DiscordKitCore", 29 | dependencies: [ 30 | .product(name: "Reachability", package: "Reachability.swift", condition: .when(platforms: [.macOS])), 31 | .product(name: "SwiftProtobuf", package: "swift-protobuf"), 32 | .product(name: "Logging", package: "swift-log"), 33 | .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), 34 | .product(name: "OpenCombineFoundation", package: "OpenCombine", condition: .when(platforms: [.linux])), 35 | .product(name: "SWCompression", package: "SWCompression", condition: .when(platforms: [.linux])), 36 | .product(name: "WebSocket", package: "WebSocket.swift", condition: .when(platforms: [.linux])) 37 | ], 38 | exclude: [ 39 | "REST/README.md", 40 | "Gateway/README.md", 41 | "Objects/README.md" 42 | ] 43 | ), 44 | .target( 45 | name: "DiscordKit", 46 | dependencies: [ 47 | .target(name: "DiscordKitCore"), 48 | .product(name: "Logging", package: "swift-log") 49 | ] 50 | ), 51 | .target( 52 | name: "DiscordKitBot", 53 | dependencies: [ 54 | .target(name: "DiscordKitCore"), 55 | .product(name: "SwiftProtobuf", package: "swift-protobuf") 56 | ] 57 | ), 58 | .testTarget(name: "DiscordKitCommonTests", dependencies: ["DiscordKitCore"]) 59 | ], 60 | swiftLanguageVersions: [.v5] 61 | ) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

DiscordKit

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 |

Package for interacting with Discord's API to build Swift bots

21 | 22 | > DiscordKit for Bots is now released! Use DiscordKit to create that bot you've been 23 | > looking to make, in the Swift that you know and love! 24 | 25 | ## About 26 | 27 | DiscordKit is a Swift package for creating Discord bots in Swift. 28 | 29 | **If DiscordKit has helped you, please give it a ⭐ star and consider sponsoring! It 30 | keeps me motivated to continue developing this and other projects.** 31 | 32 | ## Installation 33 | 34 | ### Swift Package Manager (SPM): 35 | 36 |
37 | Package.swift 38 | 39 | Add the following to your `Package.swift`: 40 | ```swift 41 | .package(url: "https://github.com/SwiftcordApp/DiscordKit", branch: "main") 42 | ``` 43 |
44 |
45 | Xcode Project 46 | 47 | Add a package dependancy in your Xcode project with the following parameters: 48 | 49 | **Package URL:** 50 | ``` 51 | https://github.com/SwiftcordApp/DiscordKit 52 | ``` 53 | 54 | **Branch:** 55 | ``` 56 | main 57 | ``` 58 | 59 | **Product:** 60 | - [x] DiscordKitBot 61 |
62 | 63 | For more detailed instructions, refer to [this page](https://app.gitbook.com/o/bq2pyf3PEDPf2CURHt4z/s/WJuHiYLW9jKqPb7h8D7t/getting-started/installation) 64 | in the DiscordKit guide. 65 | 66 | ## Example Usage 67 | 68 | Create a simple bot with a **/ping** command: 69 | 70 | ```swift 71 | import DiscordKitBot 72 | 73 | let bot = Client(intents: .unprivileged) 74 | 75 | // Guild to register commands in. If the COMMAND_GUILD_ID environment variable is set, commands are scoped 76 | // to that server and update instantly, useful for debugging. Otherwise, they are registered globally. 77 | let commandGuildID = ProcessInfo.processInfo.environment["COMMAND_GUILD_ID"] 78 | 79 | bot.ready.listen { 80 | print("Logged in as \(bot.user!.username)#\(bot.user!.discriminator)!") 81 | 82 | try? await bot.registerApplicationCommands(guild: commandGuildID) { 83 | NewAppCommand("ping", description: "Ping me!") { interaction in 84 | try? await interaction.reply("Pong!") 85 | } 86 | } 87 | } 88 | 89 | bot.login() // Reads the bot token from the DISCORD_TOKEN environment variable and logs in with the token 90 | 91 | // Run the main RunLoop to prevent the program from exiting 92 | RunLoop.main.run() 93 | ``` 94 | _(Yes, that's really the whole code, no messing with registering commands with the REST 95 | API or anything!)_ 96 | 97 | Not sure what to do next? Check out the guide below, which walks you through 98 | all the steps to create your own Discord bot! 99 | 100 | ## Resources 101 | 102 | Here are some (WIP) resources that might be useful while developing with DiscordKit. 103 | 104 | * [DiscordKit Guide](https://swiftcord.gitbook.io/discordkit-guide/) 105 | * [Developer Documentation](https://swiftcordapp.github.io/DiscordKit/documentation/discordkitbot/) (Built with DocC) 106 | 107 | ## Platform Support 108 | 109 | Currently, DiscordKit only offically supports macOS versions 11 and up. Theoretically, you should be able to compile and use DiscordKit on any Apple platform with equivalent APIs, however this has not been tested and is considered an unsupported setup. 110 | 111 | DiscordKitBot and DiscordKitCore is supported on Linux, but not DiscordKit itself. However, you are able to develop and host bots made with DiscordKit on Linux. 112 | 113 | Windows is not supported natively at the moment. The recommended method is to use Windows Subsystem for Linux to do any development/hosting of DiscordKit bots on Windows. Native support may come in the future. 114 | -------------------------------------------------------------------------------- /Sources/DiscordKit/Extensions/Presence+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presence+.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 7/9/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | extension Presence { 12 | init(protoStatus: StatusSettings, id: Snowflake) { 13 | let presence = PresenceStatus(rawValue: protoStatus.status.value) ?? .online 14 | var activities: [Activity] = [] 15 | if protoStatus.hasCustomStatus { 16 | activities.append(Activity( 17 | name: "Custom Status", 18 | type: .custom, 19 | created_at: 0, 20 | state: protoStatus.customStatus.text 21 | )) 22 | } 23 | self.init( 24 | userID: id, 25 | status: presence, 26 | clientStatus: PresenceClientStatus(desktop: presence), 27 | activities: activities 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DiscordKit/Gateway/GatewayCachedState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CachedState.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 22/2/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// A struct for storing cached data from the Gateway 12 | /// 13 | /// Used in ``DiscordGateway/cache``. 14 | public class CachedState: ObservableObject { 15 | /// Dictionary of guilds the user is in 16 | /// 17 | /// > The guild's ID is its key 18 | @Published public private(set) var guilds: [Snowflake: PreloadedGuild] = [:] 19 | 20 | @Published public private(set) var members: [Snowflake: Member] = [:] 21 | 22 | /// DM channels the user is in 23 | @Published public private(set) var dms: [Channel] = [] 24 | 25 | /// Cached object of current user 26 | @Published public private(set) var user: CurrentUser? 27 | 28 | /// Cached users, initially populated from `READY` event and might 29 | /// grow over time 30 | @Published public private(set) var users: [Snowflake: User] = [:] 31 | 32 | /// Populates the cache using the provided event. 33 | /// - Parameter event: An incoming Gateway "ready" event. 34 | func configure(using event: ReadyEvt) { 35 | event.guilds.forEach(appendOrReplace(_:)) 36 | dms = event.private_channels 37 | user = event.user 38 | event.users.forEach(appendOrReplace(_:)) 39 | event.merged_members.enumerated().forEach { (idx, guildMembers) in 40 | members[event.guilds[idx].id] = guildMembers.first(where: { $0.user_id == event.user.id }) 41 | } 42 | print(members) 43 | } 44 | 45 | // MARK: - Guilds 46 | 47 | /// Updates or appends the provided guild. 48 | /// - Parameter guild: The guild you want to update or append to the cache. 49 | func appendOrReplace(_ guild: PreloadedGuild) { 50 | guilds.updateValue(guild, forKey: guild.id) 51 | } 52 | 53 | /// Removes any guilds with an identifier matching the identifier of the provided guild parameter. 54 | /// - Parameter guild: A ``Guild`` instance whose identifier will be used to remove any guilds with a matching identifier. 55 | func remove(_ guild: GuildUnavailable) { 56 | guilds.removeValue(forKey: guild.id) 57 | } 58 | 59 | // MARK: - Channels 60 | 61 | /// Appends the provided channel to the appropriate cached guild. 62 | /// - Parameter channel: The channel to append. 63 | func append(_ channel: Channel) { 64 | guard let identifier = channel.guild_id else { 65 | return 66 | } 67 | 68 | // guilds[identifier]?.channels?.append(channel) 69 | } 70 | 71 | /// Removes the provided channel from the appropriate cached guild. 72 | /// - Parameter channel: The channel to remove. 73 | func remove(_ channel: Channel) { 74 | guard let identifier = channel.guild_id else { 75 | return 76 | } 77 | 78 | // guilds[ 79 | 80 | // guilds[identifier]?.channels?.removeAll(matchingIdentifierFor: channel) 81 | } 82 | 83 | /// Replaces the first channel with an identifier that matches the provided channel's identifier.. 84 | /// - Parameter channel: The channel to replace 85 | func replace(_ channel: Channel) { 86 | /*guard 87 | let guildID = channel.guild_id, 88 | let channelIndex = guilds[guildID]? 89 | .channels? 90 | .firstIndex(matchingIdentifierFor: channel) 91 | else { 92 | return 93 | } 94 | 95 | guilds[guildID]?.channels?[channelIndex] = channel*/ 96 | } 97 | 98 | // MARK: - Messages 99 | 100 | /// Appends or replaces the given message within the appropriate channel. 101 | /// - Parameter message: The message to append. 102 | func appendOrReplace(_ message: Message) { 103 | if let idx = dms.firstIndex(where: { $0.id == message.channel_id }) { 104 | dms[idx].last_message_id = message.id 105 | } 106 | } 107 | 108 | // MARK: - Users 109 | 110 | /// Appends or replaces the provided user in the cache. 111 | /// - Parameter user: The user to cache. 112 | func appendOrReplace(_ user: User) { 113 | users.updateValue(user, forKey: user.id) 114 | } 115 | 116 | /// Replaces the current user with the provided one 117 | func replace(_ user: CurrentUser) { 118 | self.user = user 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/DiscordKit/Objects/REST/UserSettingsProtoUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserSettingsProtoUpdate.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 7/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserSettingsProtoUpdate: Encodable { 11 | let settings: String 12 | } 13 | -------------------------------------------------------------------------------- /Sources/DiscordKit/REST/APIUser+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIUser+.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 7/9/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public extension DiscordREST { 12 | /// Update user settings proto 13 | /// 14 | /// `PATCH /users/@me/settings-proto/{id}` 15 | func updateSettingsProto( 16 | proto: Data, 17 | type: Int = 1 // Always 1 for now 18 | ) async throws { 19 | return try await patchReq( 20 | path: "users/@me/settings-proto/\(type)", 21 | body: UserSettingsProtoUpdate(settings: proto.base64EncodedString()) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DiscordKit/protos/Settings/Appearance.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // swift-format-ignore-file 3 | // 4 | // Generated by the Swift generator plugin for the protocol buffer compiler. 5 | // Source: Settings/Appearance.proto 6 | // 7 | // For information on using the generated types, please see the documentation: 8 | // https://github.com/apple/swift-protobuf/ 9 | 10 | import Foundation 11 | import SwiftProtobuf 12 | 13 | // If the compiler emits an error on this type, it is because this file 14 | // was generated by a version of the `protoc` Swift plug-in that is 15 | // incompatible with the version of SwiftProtobuf to which you are linking. 16 | // Please ensure that you are building against the same version of the API 17 | // that was used to generate this file. 18 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 19 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 20 | typealias Version = _2 21 | } 22 | 23 | /// ====== Appearance ====== // 24 | public enum Theme: SwiftProtobuf.Enum { 25 | public typealias RawValue = Int 26 | case unset // = 0 27 | case dark // = 1 28 | case light // = 2 29 | case UNRECOGNIZED(Int) 30 | 31 | public init() { 32 | self = .unset 33 | } 34 | 35 | public init?(rawValue: Int) { 36 | switch rawValue { 37 | case 0: self = .unset 38 | case 1: self = .dark 39 | case 2: self = .light 40 | default: self = .UNRECOGNIZED(rawValue) 41 | } 42 | } 43 | 44 | public var rawValue: Int { 45 | switch self { 46 | case .unset: return 0 47 | case .dark: return 1 48 | case .light: return 2 49 | case .UNRECOGNIZED(let i): return i 50 | } 51 | } 52 | 53 | } 54 | 55 | #if swift(>=4.2) 56 | 57 | extension Theme: CaseIterable { 58 | // The compiler won't synthesize support with the UNRECOGNIZED case. 59 | public static var allCases: [Theme] = [ 60 | .unset, 61 | .dark, 62 | .light, 63 | ] 64 | } 65 | 66 | #endif // swift(>=4.2) 67 | 68 | public struct AppearanceSettings { 69 | // SwiftProtobuf.Message conformance is added in an extension below. See the 70 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 71 | // methods supported on all messages. 72 | 73 | public var theme: Theme = .unset 74 | 75 | public var developerMode: Bool = false 76 | 77 | public var unknownFields = SwiftProtobuf.UnknownStorage() 78 | 79 | public init() {} 80 | } 81 | 82 | #if swift(>=5.5) && canImport(_Concurrency) 83 | extension Theme: @unchecked Sendable {} 84 | extension AppearanceSettings: @unchecked Sendable {} 85 | #endif // swift(>=5.5) && canImport(_Concurrency) 86 | 87 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 88 | 89 | extension Theme: SwiftProtobuf._ProtoNameProviding { 90 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 91 | 0: .same(proto: "UNSET"), 92 | 1: .same(proto: "DARK"), 93 | 2: .same(proto: "LIGHT"), 94 | ] 95 | } 96 | 97 | extension AppearanceSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 98 | public static let protoMessageName: String = "AppearanceSettings" 99 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 100 | 1: .same(proto: "theme"), 101 | 2: .standard(proto: "developer_mode"), 102 | ] 103 | 104 | public mutating func decodeMessage(decoder: inout D) throws { 105 | while let fieldNumber = try decoder.nextFieldNumber() { 106 | // The use of inline closures is to circumvent an issue where the compiler 107 | // allocates stack space for every case branch when no optimizations are 108 | // enabled. https://github.com/apple/swift-protobuf/issues/1034 109 | switch fieldNumber { 110 | case 1: try { try decoder.decodeSingularEnumField(value: &self.theme) }() 111 | case 2: try { try decoder.decodeSingularBoolField(value: &self.developerMode) }() 112 | default: break 113 | } 114 | } 115 | } 116 | 117 | public func traverse(visitor: inout V) throws { 118 | if self.theme != .unset { 119 | try visitor.visitSingularEnumField(value: self.theme, fieldNumber: 1) 120 | } 121 | if self.developerMode != false { 122 | try visitor.visitSingularBoolField(value: self.developerMode, fieldNumber: 2) 123 | } 124 | try unknownFields.traverse(visitor: &visitor) 125 | } 126 | 127 | public static func ==(lhs: AppearanceSettings, rhs: AppearanceSettings) -> Bool { 128 | if lhs.theme != rhs.theme {return false} 129 | if lhs.developerMode != rhs.developerMode {return false} 130 | if lhs.unknownFields != rhs.unknownFields {return false} 131 | return true 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/DiscordKit/protos/Settings/Debug.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // swift-format-ignore-file 3 | // 4 | // Generated by the Swift generator plugin for the protocol buffer compiler. 5 | // Source: Settings/Debug.proto 6 | // 7 | // For information on using the generated types, please see the documentation: 8 | // https://github.com/apple/swift-protobuf/ 9 | 10 | import Foundation 11 | import SwiftProtobuf 12 | 13 | // If the compiler emits an error on this type, it is because this file 14 | // was generated by a version of the `protoc` Swift plug-in that is 15 | // incompatible with the version of SwiftProtobuf to which you are linking. 16 | // Please ensure that you are building against the same version of the API 17 | // that was used to generate this file. 18 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 19 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 20 | typealias Version = _2 21 | } 22 | 23 | public struct DebugSettings { 24 | // SwiftProtobuf.Message conformance is added in an extension below. See the 25 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 26 | // methods supported on all messages. 27 | 28 | public var rtcPanelShowVoiceStates: SwiftProtobuf.Google_Protobuf_BoolValue { 29 | get {return _rtcPanelShowVoiceStates ?? SwiftProtobuf.Google_Protobuf_BoolValue()} 30 | set {_rtcPanelShowVoiceStates = newValue} 31 | } 32 | /// Returns true if `rtcPanelShowVoiceStates` has been explicitly set. 33 | public var hasRtcPanelShowVoiceStates: Bool {return self._rtcPanelShowVoiceStates != nil} 34 | /// Clears the value of `rtcPanelShowVoiceStates`. Subsequent reads from it will return its default value. 35 | public mutating func clearRtcPanelShowVoiceStates() {self._rtcPanelShowVoiceStates = nil} 36 | 37 | public var unknownFields = SwiftProtobuf.UnknownStorage() 38 | 39 | public init() {} 40 | 41 | fileprivate var _rtcPanelShowVoiceStates: SwiftProtobuf.Google_Protobuf_BoolValue? = nil 42 | } 43 | 44 | #if swift(>=5.5) && canImport(_Concurrency) 45 | extension DebugSettings: @unchecked Sendable {} 46 | #endif // swift(>=5.5) && canImport(_Concurrency) 47 | 48 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 49 | 50 | extension DebugSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 51 | public static let protoMessageName: String = "DebugSettings" 52 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 53 | 1: .standard(proto: "rtc_panel_show_voice_states"), 54 | ] 55 | 56 | public mutating func decodeMessage(decoder: inout D) throws { 57 | while let fieldNumber = try decoder.nextFieldNumber() { 58 | // The use of inline closures is to circumvent an issue where the compiler 59 | // allocates stack space for every case branch when no optimizations are 60 | // enabled. https://github.com/apple/swift-protobuf/issues/1034 61 | switch fieldNumber { 62 | case 1: try { try decoder.decodeSingularMessageField(value: &self._rtcPanelShowVoiceStates) }() 63 | default: break 64 | } 65 | } 66 | } 67 | 68 | public func traverse(visitor: inout V) throws { 69 | // The use of inline closures is to circumvent an issue where the compiler 70 | // allocates stack space for every if/case branch local when no optimizations 71 | // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and 72 | // https://github.com/apple/swift-protobuf/issues/1182 73 | try { if let v = self._rtcPanelShowVoiceStates { 74 | try visitor.visitSingularMessageField(value: v, fieldNumber: 1) 75 | } }() 76 | try unknownFields.traverse(visitor: &visitor) 77 | } 78 | 79 | public static func ==(lhs: DebugSettings, rhs: DebugSettings) -> Bool { 80 | if lhs._rtcPanelShowVoiceStates != rhs._rtcPanelShowVoiceStates {return false} 81 | if lhs.unknownFields != rhs.unknownFields {return false} 82 | return true 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/DiscordKit/protos/Settings/Inbox.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // swift-format-ignore-file 3 | // 4 | // Generated by the Swift generator plugin for the protocol buffer compiler. 5 | // Source: Settings/Inbox.proto 6 | // 7 | // For information on using the generated types, please see the documentation: 8 | // https://github.com/apple/swift-protobuf/ 9 | 10 | import Foundation 11 | import SwiftProtobuf 12 | 13 | // If the compiler emits an error on this type, it is because this file 14 | // was generated by a version of the `protoc` Swift plug-in that is 15 | // incompatible with the version of SwiftProtobuf to which you are linking. 16 | // Please ensure that you are building against the same version of the API 17 | // that was used to generate this file. 18 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 19 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 20 | typealias Version = _2 21 | } 22 | 23 | /// ====== Inbox ====== // 24 | public enum InboxTab: SwiftProtobuf.Enum { 25 | public typealias RawValue = Int 26 | case unspecified // = 0 27 | case mentions // = 1 28 | case unreads // = 2 29 | case todos // = 3 30 | case UNRECOGNIZED(Int) 31 | 32 | public init() { 33 | self = .unspecified 34 | } 35 | 36 | public init?(rawValue: Int) { 37 | switch rawValue { 38 | case 0: self = .unspecified 39 | case 1: self = .mentions 40 | case 2: self = .unreads 41 | case 3: self = .todos 42 | default: self = .UNRECOGNIZED(rawValue) 43 | } 44 | } 45 | 46 | public var rawValue: Int { 47 | switch self { 48 | case .unspecified: return 0 49 | case .mentions: return 1 50 | case .unreads: return 2 51 | case .todos: return 3 52 | case .UNRECOGNIZED(let i): return i 53 | } 54 | } 55 | 56 | } 57 | 58 | #if swift(>=4.2) 59 | 60 | extension InboxTab: CaseIterable { 61 | // The compiler won't synthesize support with the UNRECOGNIZED case. 62 | public static var allCases: [InboxTab] = [ 63 | .unspecified, 64 | .mentions, 65 | .unreads, 66 | .todos, 67 | ] 68 | } 69 | 70 | #endif // swift(>=4.2) 71 | 72 | public struct InboxSettings { 73 | // SwiftProtobuf.Message conformance is added in an extension below. See the 74 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 75 | // methods supported on all messages. 76 | 77 | public var currentTab: InboxTab = .unspecified 78 | 79 | public var viewedTutorial: Bool = false 80 | 81 | public var unknownFields = SwiftProtobuf.UnknownStorage() 82 | 83 | public init() {} 84 | } 85 | 86 | #if swift(>=5.5) && canImport(_Concurrency) 87 | extension InboxTab: @unchecked Sendable {} 88 | extension InboxSettings: @unchecked Sendable {} 89 | #endif // swift(>=5.5) && canImport(_Concurrency) 90 | 91 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 92 | 93 | extension InboxTab: SwiftProtobuf._ProtoNameProviding { 94 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 95 | 0: .same(proto: "UNSPECIFIED"), 96 | 1: .same(proto: "MENTIONS"), 97 | 2: .same(proto: "UNREADS"), 98 | 3: .same(proto: "TODOS"), 99 | ] 100 | } 101 | 102 | extension InboxSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 103 | public static let protoMessageName: String = "InboxSettings" 104 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 105 | 1: .standard(proto: "current_tab"), 106 | 2: .standard(proto: "viewed_tutorial"), 107 | ] 108 | 109 | public mutating func decodeMessage(decoder: inout D) throws { 110 | while let fieldNumber = try decoder.nextFieldNumber() { 111 | // The use of inline closures is to circumvent an issue where the compiler 112 | // allocates stack space for every case branch when no optimizations are 113 | // enabled. https://github.com/apple/swift-protobuf/issues/1034 114 | switch fieldNumber { 115 | case 1: try { try decoder.decodeSingularEnumField(value: &self.currentTab) }() 116 | case 2: try { try decoder.decodeSingularBoolField(value: &self.viewedTutorial) }() 117 | default: break 118 | } 119 | } 120 | } 121 | 122 | public func traverse(visitor: inout V) throws { 123 | if self.currentTab != .unspecified { 124 | try visitor.visitSingularEnumField(value: self.currentTab, fieldNumber: 1) 125 | } 126 | if self.viewedTutorial != false { 127 | try visitor.visitSingularBoolField(value: self.viewedTutorial, fieldNumber: 2) 128 | } 129 | try unknownFields.traverse(visitor: &visitor) 130 | } 131 | 132 | public static func ==(lhs: InboxSettings, rhs: InboxSettings) -> Bool { 133 | if lhs.currentTab != rhs.currentTab {return false} 134 | if lhs.viewedTutorial != rhs.viewedTutorial {return false} 135 | if lhs.unknownFields != rhs.unknownFields {return false} 136 | return true 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/DiscordKit/protos/Settings/Localization.pb.swift: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | // swift-format-ignore-file 3 | // 4 | // Generated by the Swift generator plugin for the protocol buffer compiler. 5 | // Source: Settings/Localization.proto 6 | // 7 | // For information on using the generated types, please see the documentation: 8 | // https://github.com/apple/swift-protobuf/ 9 | 10 | import Foundation 11 | import SwiftProtobuf 12 | 13 | // If the compiler emits an error on this type, it is because this file 14 | // was generated by a version of the `protoc` Swift plug-in that is 15 | // incompatible with the version of SwiftProtobuf to which you are linking. 16 | // Please ensure that you are building against the same version of the API 17 | // that was used to generate this file. 18 | fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { 19 | struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} 20 | typealias Version = _2 21 | } 22 | 23 | /// ====== Localization ====== // 24 | public struct LocalizationSettings { 25 | // SwiftProtobuf.Message conformance is added in an extension below. See the 26 | // `Message` and `Message+*Additions` files in the SwiftProtobuf library for 27 | // methods supported on all messages. 28 | 29 | public var locale: SwiftProtobuf.Google_Protobuf_StringValue { 30 | get {return _locale ?? SwiftProtobuf.Google_Protobuf_StringValue()} 31 | set {_locale = newValue} 32 | } 33 | /// Returns true if `locale` has been explicitly set. 34 | public var hasLocale: Bool {return self._locale != nil} 35 | /// Clears the value of `locale`. Subsequent reads from it will return its default value. 36 | public mutating func clearLocale() {self._locale = nil} 37 | 38 | public var timezoneOffset: SwiftProtobuf.Google_Protobuf_Int32Value { 39 | get {return _timezoneOffset ?? SwiftProtobuf.Google_Protobuf_Int32Value()} 40 | set {_timezoneOffset = newValue} 41 | } 42 | /// Returns true if `timezoneOffset` has been explicitly set. 43 | public var hasTimezoneOffset: Bool {return self._timezoneOffset != nil} 44 | /// Clears the value of `timezoneOffset`. Subsequent reads from it will return its default value. 45 | public mutating func clearTimezoneOffset() {self._timezoneOffset = nil} 46 | 47 | public var unknownFields = SwiftProtobuf.UnknownStorage() 48 | 49 | public init() {} 50 | 51 | fileprivate var _locale: SwiftProtobuf.Google_Protobuf_StringValue? = nil 52 | fileprivate var _timezoneOffset: SwiftProtobuf.Google_Protobuf_Int32Value? = nil 53 | } 54 | 55 | #if swift(>=5.5) && canImport(_Concurrency) 56 | extension LocalizationSettings: @unchecked Sendable {} 57 | #endif // swift(>=5.5) && canImport(_Concurrency) 58 | 59 | // MARK: - Code below here is support for the SwiftProtobuf runtime. 60 | 61 | extension LocalizationSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { 62 | public static let protoMessageName: String = "LocalizationSettings" 63 | public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 64 | 1: .same(proto: "locale"), 65 | 2: .standard(proto: "timezone_offset"), 66 | ] 67 | 68 | public mutating func decodeMessage(decoder: inout D) throws { 69 | while let fieldNumber = try decoder.nextFieldNumber() { 70 | // The use of inline closures is to circumvent an issue where the compiler 71 | // allocates stack space for every case branch when no optimizations are 72 | // enabled. https://github.com/apple/swift-protobuf/issues/1034 73 | switch fieldNumber { 74 | case 1: try { try decoder.decodeSingularMessageField(value: &self._locale) }() 75 | case 2: try { try decoder.decodeSingularMessageField(value: &self._timezoneOffset) }() 76 | default: break 77 | } 78 | } 79 | } 80 | 81 | public func traverse(visitor: inout V) throws { 82 | // The use of inline closures is to circumvent an issue where the compiler 83 | // allocates stack space for every if/case branch local when no optimizations 84 | // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and 85 | // https://github.com/apple/swift-protobuf/issues/1182 86 | try { if let v = self._locale { 87 | try visitor.visitSingularMessageField(value: v, fieldNumber: 1) 88 | } }() 89 | try { if let v = self._timezoneOffset { 90 | try visitor.visitSingularMessageField(value: v, fieldNumber: 2) 91 | } }() 92 | try unknownFields.traverse(visitor: &visitor) 93 | } 94 | 95 | public static func ==(lhs: LocalizationSettings, rhs: LocalizationSettings) -> Bool { 96 | if lhs._locale != rhs._locale {return false} 97 | if lhs._timezoneOffset != rhs._timezoneOffset {return false} 98 | if lhs.unknownFields != rhs.unknownFields {return false} 99 | return true 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/AppCommandBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCommandBuilder.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 26/11/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// A `resultBuilder` which allows constructing ``NewAppCommand``s with blocks 12 | /// 13 | /// This provides syntactic sugar for constructing application commands. 14 | @resultBuilder 15 | public struct AppCommandBuilder { 16 | public static func buildBlock(_ components: NewAppCommand...) -> [NewAppCommand] { 17 | components 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/NewAppCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewAppCommand.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 10/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// A block to build a new application command with the ``AppCommandBuilder`` 12 | /// 13 | /// > This struct is not designed to be constructed outside of the ``AppCommandBuilder``. 14 | /// > Use methods like ``Client/registerApplicationCommands(guild:_:)-3vqy0`` 15 | /// > which allow you to construct commands with an ``AppCommandBuilder``. 16 | public struct NewAppCommand: Encodable { 17 | public let type: AppCommand.CommandType 18 | /// Name of this application command 19 | public let name: String 20 | /// Description of this application command 21 | public let description: String? 22 | /// Options of this application command 23 | public let options: [CommandOption]? 24 | /// Interaction handler that will be called upon interactions with this command 25 | let handler: Handler 26 | 27 | enum CodingKeys: CodingKey { 28 | case type 29 | case name 30 | case description 31 | case options 32 | } 33 | 34 | public func encode(to encoder: Encoder) throws { 35 | var container = encoder.container(keyedBy: CodingKeys.self) 36 | try container.encode(type, forKey: .type) 37 | try container.encode(name, forKey: .name) 38 | try container.encode(description, forKey: .description) 39 | 40 | // Workaround to encode array of protocols 41 | if let options = options { 42 | var optContainer = container.nestedUnkeyedContainer(forKey: .options) 43 | for option in options { 44 | try optContainer.encode(option) 45 | } 46 | } 47 | } 48 | 49 | /// Create an instance of a ``NewAppCommand``, with options provided as an array without an ``OptionBuilder`` 50 | public init( 51 | _ name: String, description: String? = nil, 52 | type: AppCommand.CommandType = .slash, 53 | options: [CommandOption]? = nil, 54 | handler: @escaping Handler 55 | ) { 56 | self.name = name 57 | self.description = description 58 | self.type = type 59 | self.options = options 60 | self.handler = handler 61 | } 62 | 63 | /// Create an instance of a ``NewAppCommand``, adding options with an ``OptionBuilder`` 64 | public init( 65 | _ name: String, description: String? = nil, 66 | type: AppCommand.CommandType = .slash, 67 | @OptionBuilder options: () -> [CommandOption], 68 | handler: @escaping Handler 69 | ) { 70 | self.init(name, description: description, type: type, options: options(), handler: handler) 71 | } 72 | 73 | /// An application command handler that will be called on invocation of the command 74 | public typealias Handler = (_ interaction: CommandData) async -> Void 75 | } 76 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/BooleanOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BooleanOption.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 13/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// An option accepting `Bool` values for an application command 12 | /// 13 | /// To be used with the ``OptionBuilder`` from the ``NewAppCommand`` initialiser 14 | public struct BooleanOption: CommandOption { 15 | public init(_ name: String, description: String) { 16 | type = .boolean 17 | 18 | self.name = name 19 | self.description = description 20 | } 21 | 22 | public let type: CommandOptionType 23 | 24 | public let name: String 25 | 26 | public let description: String 27 | 28 | public var required: Bool? 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/CommandOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CmdOption.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 12/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// An option in an application command 12 | public protocol CommandOption: Encodable { 13 | /// The type of this option 14 | var type: CommandOptionType { get } 15 | 16 | /// Name of this command 17 | /// 18 | /// > Important: Must be 1-32 characters long, matching the following Regex: `^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$` 19 | var name: String { get } 20 | 21 | /// Description of this command 22 | /// 23 | /// > Important: Must be 1-100 characters long 24 | var description: String { get } 25 | /// If this command is required 26 | var required: Bool? { get set } 27 | 28 | // Channel types to restrict visibility of command to 29 | // var channel_types: ChannelType? { get } 30 | } 31 | 32 | // MARK: Modifiers 33 | public extension CommandOption { 34 | func required() -> Self { 35 | var opt = self 36 | opt.required = true 37 | return opt 38 | } 39 | } 40 | 41 | public struct AppCommandOptionChoice: Encodable { 42 | public init(name: String, value: Interaction.Data.AppCommandData.OptionData.Value) { 43 | self.name = name 44 | self.value = value 45 | } 46 | 47 | public let name: String 48 | public let value: Interaction.Data.AppCommandData.OptionData.Value // Trust me it makes more sense nested like this 49 | } 50 | 51 | /// An enum to store either a `Double` or `Int` value for setting the minimum or maximum value of an option 52 | enum MinMaxValue: Codable { 53 | /// Min or max value for an option of ``CommandOptionType/number`` type 54 | case number(Double) 55 | /// Min or max value for an option of ``CommandOptionType/integer`` type 56 | case integer(Int) 57 | 58 | public init(from decoder: Decoder) throws { 59 | let container = try decoder.singleValueContainer() 60 | 61 | if let val = try? container.decode(Double.self) { 62 | self = .number(val) 63 | } else if let val = try? container.decode(Int.self) { 64 | self = .integer(val) 65 | } else { 66 | throw DecodingError.typeMismatch( 67 | Int.self, 68 | .init(codingPath: [], debugDescription: "Expected either Int or Double, found neither") 69 | ) 70 | } 71 | } 72 | 73 | public func encode(to encoder: Encoder) throws { 74 | var container = encoder.singleValueContainer() 75 | 76 | switch self { 77 | case .number(let value): try container.encode(value) 78 | case .integer(let value): try container.encode(value) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/IntegerOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntegerOption.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 13/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// An option accepting `Int` values for an application command 12 | /// 13 | /// To be used with the ``OptionBuilder`` from the ``NewAppCommand`` initialiser 14 | public struct IntegerOption: CommandOption { 15 | public init(_ name: String, description: String, choices: [AppCommandOptionChoice]? = nil, autocomplete: Bool? = nil) { 16 | type = .integer 17 | 18 | self.name = name 19 | self.description = description 20 | self.choices = choices 21 | self.autocomplete = autocomplete 22 | } 23 | 24 | public let type: CommandOptionType 25 | 26 | public let name: String 27 | 28 | public let description: String 29 | 30 | public var required: Bool? 31 | 32 | /// Choices for the user to pick from 33 | /// 34 | /// > Important: There can be a max of 25 choices. 35 | public let choices: [AppCommandOptionChoice]? 36 | 37 | /// Minimium value permitted for this option 38 | fileprivate(set) var min_value: Int? 39 | /// Maximum value permitted for this option 40 | fileprivate(set) var max_value: Int? 41 | 42 | /// If autocomplete interactions are enabled for this option 43 | public let autocomplete: Bool? 44 | } 45 | 46 | extension IntegerOption { 47 | /// Require the value of this option to be greater than or equal to this value 48 | public func min(_ min: Int) -> Self { 49 | var opt = self 50 | opt.min_value = min 51 | return opt 52 | } 53 | 54 | /// Require the value of this option to be smaller than or equal to this value 55 | public func max(_ max: Int) -> Self { 56 | var opt = self 57 | opt.max_value = max 58 | return opt 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/NumberOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberOption.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 13/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// An option accepting `Double` values for an application command 12 | /// 13 | /// To be used with the ``OptionBuilder`` from the ``NewAppCommand`` initialiser 14 | public struct NumberOption: CommandOption { 15 | public init(_ name: String, description: String, choices: [AppCommandOptionChoice]? = nil, min: Double? = nil, max: Double? = nil, autocomplete: Bool? = nil) { 16 | type = .number 17 | 18 | self.name = name 19 | self.description = description 20 | self.choices = choices 21 | self.min_value = min 22 | self.max_value = max 23 | self.autocomplete = autocomplete 24 | } 25 | 26 | public let type: CommandOptionType 27 | 28 | public let name: String 29 | 30 | public let description: String 31 | 32 | public var required: Bool? 33 | 34 | /// Choices for the user to pick from 35 | /// 36 | /// > Important: There can be a max of 25 choices. 37 | public let choices: [AppCommandOptionChoice]? 38 | 39 | /// Minimium value permitted for this option 40 | public let min_value: Double? 41 | /// Maximum value permitted for this option 42 | public let max_value: Double? 43 | 44 | /// If autocomplete interactions are enabled for this option 45 | public let autocomplete: Bool? 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/OptionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionBuilder.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 12/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | @resultBuilder 12 | public struct OptionBuilder { 13 | public static func buildBlock(_ components: CommandOption...) -> [CommandOption] { 14 | components 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/StringOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringOption.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 12/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// An option for an application command that accepts a string value 12 | public struct StringOption: CommandOption { 13 | public init(_ name: String, description: String, `required`: Bool? = nil, choices: [AppCommandOptionChoice]? = nil, minLength: Int? = nil, maxLength: Int? = nil, autocomplete: Bool? = nil) { 14 | type = .string 15 | 16 | self.required = `required` 17 | self.choices = choices 18 | self.name = name 19 | self.description = description 20 | self.min_length = minLength 21 | self.max_length = maxLength 22 | self.autocomplete = autocomplete 23 | } 24 | 25 | public var type: CommandOptionType 26 | 27 | public var required: Bool? 28 | 29 | /// Choices for the user to pick from 30 | /// 31 | /// > Important: There can be a max of 25 choices. 32 | public let choices: [AppCommandOptionChoice]? 33 | 34 | public let name: String 35 | public let description: String 36 | 37 | /// The minimum allowed length of the value 38 | /// 39 | /// This parameter has a minimum of 0 and maximum of 6000 40 | public let min_length: Int? 41 | /// The maximum allowed length 42 | /// 43 | /// This parameter has a minimum of 1 and maximum of 6000 44 | public let max_length: Int? 45 | 46 | /// If autocomplete interactions are enabled for this option 47 | public let autocomplete: Bool? 48 | } 49 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/ApplicationCommand/Option/SubCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubCommand.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 13/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public struct SubCommand: CommandOption { 12 | /// Create a sub-command, optionally with an array of options 13 | public init(_ name: String, description: String, options: [CommandOption]? = nil) { 14 | type = .subCommand 15 | 16 | self.name = name 17 | self.description = description 18 | self.options = options 19 | } 20 | 21 | /// Create a sub-command with options built by an ``OptionBuilder`` 22 | public init(_ name: String, description: String, @OptionBuilder options: () -> [CommandOption]) { 23 | self.init(name, description: description, options: options()) 24 | } 25 | 26 | public let type: CommandOptionType 27 | 28 | public let name: String 29 | 30 | public let description: String 31 | 32 | public var required: Bool? 33 | 34 | /// If this command is a subcommand or subcommand group type, these nested options will be its parameters 35 | public let options: [CommandOption]? 36 | 37 | enum CodingKeys: CodingKey { 38 | case type 39 | case name 40 | case description 41 | case required 42 | case options 43 | } 44 | 45 | public func encode(to encoder: Encoder) throws { 46 | var container: KeyedEncodingContainer = encoder.container(keyedBy: SubCommand.CodingKeys.self) 47 | 48 | try container.encode(self.type, forKey: SubCommand.CodingKeys.type) 49 | try container.encode(self.name, forKey: SubCommand.CodingKeys.name) 50 | try container.encode(self.description, forKey: SubCommand.CodingKeys.description) 51 | try container.encodeIfPresent(self.required, forKey: SubCommand.CodingKeys.required) 52 | if let options = options { 53 | var optContainer = container.nestedUnkeyedContainer(forKey: .options) 54 | for option in options { 55 | try optContainer.encode(option) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/BotMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BotMessage.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 22/11/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | /// A Discord message, with convenience methods 12 | /// 13 | /// This struct represents a message on Discord, 14 | /// > Internally, `Message`s are converted to and from this type 15 | /// > for easier use 16 | public struct BotMessage { 17 | public let content: String 18 | public let channelID: Snowflake // This will be changed very soon 19 | public let id: Snowflake // This too 20 | 21 | // The REST handler associated with this message, used for message actions 22 | fileprivate weak var rest: DiscordREST? 23 | 24 | internal init(from message: Message, rest: DiscordREST) { 25 | content = message.content 26 | channelID = message.channel_id 27 | id = message.id 28 | 29 | self.rest = rest 30 | } 31 | } 32 | 33 | public extension BotMessage { 34 | func reply(_ content: String) async throws -> Message { 35 | return try await rest!.createChannelMsg( 36 | message: .init(content: content, message_reference: .init(message_id: id), components: []), 37 | id: channelID 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/Embed/BotEmbed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BotEmbed.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 16/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public struct BotEmbed: Codable { 12 | /// An embed field 13 | public struct Field: Codable { 14 | /// Create an embed field 15 | public init(_ name: String, value: String, inline: Bool = false) { 16 | assert(!name.isEmpty, "Name cannot be empty") 17 | assert(!value.isEmpty, "Value cannot be empty") 18 | 19 | self.name = name 20 | self.value = value 21 | self.inline = inline 22 | } 23 | /// Construct an empty field 24 | /// 25 | /// This populates both the name and inline field values with `\u{200b}`, as 26 | /// [recommended in the Discord.JS Guide](https://discordjs.guide/popular-topics/embeds.html#using-the-embed-constructor) 27 | public init(inline: Bool = false) { 28 | self.init("\u{200b}", value: "\u{200b}", inline: inline) 29 | } 30 | 31 | public let name: String 32 | public let value: String 33 | public let inline: Bool 34 | } 35 | 36 | enum CodingKeys: CodingKey { 37 | case type 38 | case title 39 | case description 40 | case url 41 | case timestamp 42 | case color 43 | case fields 44 | case footer 45 | } 46 | 47 | // Always rich as that's the only type supported 48 | private let type = EmbedType.rich 49 | 50 | // Fields are implicitly internal(get) as we do not want them appearing in autocomplete 51 | fileprivate(set) var title: String? 52 | fileprivate(set) var description: String? 53 | fileprivate(set) var url: URL? 54 | fileprivate(set) var timestamp: Date? 55 | fileprivate(set) var color: Int? 56 | fileprivate(set) var footer: EmbedFooter? 57 | private let fields: [Field]? 58 | 59 | public init(fields: [Field]? = nil) { 60 | self.fields = fields 61 | } 62 | public init(@EmbedFieldBuilder fields: () -> [Field]) { 63 | self.init(fields: fields()) 64 | } 65 | } 66 | 67 | public extension BotEmbed { 68 | func title(_ title: String?) -> Self { 69 | var embed = self 70 | embed.title = title 71 | return embed 72 | } 73 | 74 | func description(_ description: String?) -> Self { 75 | var embed = self 76 | embed.description = description 77 | return embed 78 | } 79 | 80 | func footer(_ text: String) -> Self { 81 | var embed = self 82 | embed.footer = .init(text: text) 83 | return embed 84 | } 85 | 86 | func url(_ url: URL?) -> Self { 87 | var embed = self 88 | embed.url = url 89 | return embed 90 | } 91 | func url(_ newURL: String?) -> Self { 92 | url(newURL != nil ? URL(string: newURL!) : nil) 93 | } 94 | 95 | func timestamp(_ timestamp: Date?) -> Self { 96 | var embed = self 97 | embed.timestamp = timestamp 98 | return embed 99 | } 100 | 101 | func color(_ color: Int?) -> Self { 102 | var embed = self 103 | embed.color = color 104 | return embed 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/Embed/BotEmbedBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbedBuilder.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 16/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | @resultBuilder 12 | public struct EmbedBuilder { 13 | public static func buildBlock(_ components: BotEmbed...) -> [BotEmbed] { 14 | components 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/Embed/Field/EmbedFieldBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbedFieldBuilder.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 16/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct EmbedFieldBuilder { 12 | public static func buildBlock(_ components: BotEmbed.Field...) -> [BotEmbed.Field] { 13 | components 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/MessageComponent/ActionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionRow.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 27/1/23. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public struct ActionRow: Component { 12 | public let type: MessageComponentTypes = .actionRow 13 | public let components: [Component] 14 | 15 | public init(@ComponentBuilder _ components: () -> [Component]) { 16 | self.components = components() 17 | assert(self.components.count <= 5, "An action row can contain up to 5 buttons") 18 | } 19 | 20 | enum CodingKeys: CodingKey { 21 | case type 22 | case components 23 | } 24 | 25 | public func encode(to encoder: Encoder) throws { 26 | var container = encoder.container(keyedBy: CodingKeys.self) 27 | 28 | try container.encode(1, forKey: .type) 29 | var componentContainer = container.nestedUnkeyedContainer(forKey: .components) 30 | for component in components { 31 | try componentContainer.encode(component) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/MessageComponent/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 27/1/23. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public struct Button: Component { 12 | public enum ButtonType: Int, Codable { 13 | /// An action button with a blurple background 14 | case primary = 1 15 | /// An action button with a grey background 16 | case secondary = 2 17 | /// An action button with a green background 18 | case success = 3 19 | /// An action button with a red background 20 | case danger = 4 21 | /// A grey link button 22 | case link = 5 23 | } 24 | 25 | public let type: MessageComponentTypes = .button 26 | fileprivate(set) var style: ButtonType = .primary 27 | public let label: String? 28 | public let emoji: Emoji? 29 | public let custom_id: String? 30 | public let url: URL? 31 | fileprivate(set) var disabled: Bool? 32 | 33 | public init(_ label: String? = nil, emoji: Emoji? = nil, id: String) { 34 | assert(label != nil || emoji != nil, "One of label or emoji must be provided") 35 | self.label = label 36 | self.custom_id = id 37 | self.emoji = emoji 38 | self.url = nil 39 | } 40 | 41 | public init(_ label: String? = nil, emoji: Emoji? = nil, url: URL) { 42 | assert(label != nil || emoji != nil, "One of label or emoji must be provided") 43 | self.label = label 44 | self.custom_id = nil 45 | self.emoji = emoji 46 | self.url = url 47 | } 48 | } 49 | 50 | public extension Button { 51 | func buttonStyle(_ style: ButtonType) -> Self { 52 | var opt = self 53 | opt.style = style 54 | return opt 55 | } 56 | 57 | func disabled(_ disabled: Bool = true) -> Self { 58 | var opt = self 59 | opt.disabled = disabled 60 | return opt 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/MessageComponent/ComponentBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentBuilder.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 27/1/23. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | @resultBuilder 12 | public struct ComponentBuilder { 13 | public static func buildBlock(_ components: [Component]...) -> [Component] { 14 | components.flatMap { $0 } 15 | } 16 | 17 | public static func buildArray(_ components: [[Component]]) -> [Component] { 18 | components.flatMap { $0 } 19 | } 20 | 21 | public static func buildExpression(_ expression: Component) -> [Component] { 22 | [expression] 23 | } 24 | 25 | public static func buildOptional(_ component: [Component]?) -> [Component] { 26 | component ?? [] 27 | } 28 | public static func buildEither(first component: [Component]) -> [Component] { 29 | component 30 | } 31 | public static func buildEither(second component: [Component]) -> [Component] { 32 | component 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/NCWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NCWrapper.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 24/11/22. 6 | // Credits: Helloyunho for original iteration 7 | // 8 | 9 | import Foundation 10 | 11 | public struct NCWrapper { 12 | private let notificationCenter: NotificationCenter 13 | 14 | private let name: NSNotification.Name 15 | 16 | init(_ name: NSNotification.Name, notificationCenter: NotificationCenter = .default) { 17 | self.name = name 18 | self.notificationCenter = notificationCenter 19 | } 20 | 21 | func emit(value: Data) { 22 | notificationCenter.post(name: name, object: value) 23 | } 24 | 25 | public func listen(listener: @escaping (Data) -> Void) { 26 | _ = notificationCenter.addObserver(forName: name, object: nil, queue: nil) { notif in 27 | guard let obj = notif.object as? Data else { return } 28 | listener(obj) 29 | } 30 | } 31 | 32 | public func listen(listener: @escaping (Data) async -> Void) { 33 | listen { data in Task { await listener(data) } } 34 | } 35 | } 36 | 37 | // Wrapper functions if the data is of type void 38 | extension NCWrapper where Data == Void { 39 | func emit() { 40 | emit(value: ()) 41 | } 42 | 43 | public func listen(listener: @escaping () -> Void) { 44 | listen { _ in listener() } 45 | } 46 | public func listen(listener: @escaping () async -> Void) { 47 | listen { _ in await listener() } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/NotificationNames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationNames.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 23/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension NSNotification.Name { 11 | static let ready = Self("dk-ready") 12 | 13 | static let messageCreate = Self("dk-msg-create") 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/Objects/InteractionResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractionResponse.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 17/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | // MARK: - Interaction Response 12 | public struct InteractionResponse: Encodable { 13 | public init(type: InteractionResponse.ResponseType, data: InteractionResponse.ResponseData?) { 14 | self.type = type 15 | self.data = data 16 | } 17 | 18 | public enum ResponseType: Int, Codable { 19 | case pong = 1 20 | case interactionReply = 4 21 | case deferredInteractionReply = 5 22 | case deferredUpdateMessage = 6 23 | case updateMessage = 7 24 | case appCommandAutocompleteResult = 8 25 | case modal = 9 26 | } 27 | 28 | public enum ResponseData: Encodable { 29 | public struct Message: Encodable { 30 | public init( 31 | content: String? = nil, tts: String? = nil, embeds: [BotEmbed]? = nil, 32 | allowed_mentions: AllowedMentions? = nil, 33 | flags: DiscordKitCore.Message.Flags? = nil, 34 | components: [Component]? = nil, 35 | attachments: [NewAttachment]? = nil 36 | ) { 37 | self.content = content 38 | self.tts = tts 39 | self.embeds = embeds 40 | self.allowed_mentions = allowed_mentions 41 | self.flags = flags 42 | self.components = components 43 | self.attachments = attachments 44 | } 45 | 46 | public let content: String? 47 | public let tts: String? 48 | public let embeds: [BotEmbed]? 49 | public let allowed_mentions: AllowedMentions? 50 | public let flags: DiscordKitCore.Message.Flags? 51 | public let components: [Component]? 52 | public let attachments: [NewAttachment]? 53 | 54 | enum CodingKeys: CodingKey { 55 | case content 56 | case tts 57 | case embeds 58 | case allowed_mentions 59 | case flags 60 | case attachments 61 | case components 62 | } 63 | 64 | public func encode(to encoder: Encoder) throws { 65 | var container = encoder.container(keyedBy: CodingKeys.self) 66 | 67 | try container.encodeIfPresent(self.content, forKey: .content) 68 | try container.encodeIfPresent(self.tts, forKey: .tts) 69 | try container.encodeIfPresent(self.embeds, forKey: .embeds) 70 | try container.encodeIfPresent(self.allowed_mentions, forKey: .allowed_mentions) 71 | try container.encodeIfPresent(self.flags, forKey: .flags) 72 | try container.encodeIfPresent(self.attachments, forKey: .attachments) 73 | 74 | if let components { 75 | var componentContainer = container.nestedUnkeyedContainer(forKey: .components) 76 | for component in components { 77 | try componentContainer.encode(component) 78 | } 79 | } 80 | } 81 | } 82 | 83 | case message(Message) 84 | // case autocompleteResult 85 | // case modal 86 | 87 | public func encode(to encoder: Encoder) throws { 88 | var container = encoder.singleValueContainer() 89 | 90 | switch self { 91 | case .message(let message): try container.encode(message) 92 | } 93 | } 94 | } 95 | 96 | public let type: ResponseType 97 | public let data: ResponseData? 98 | } 99 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/Objects/WebhookResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebhookResponse.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 17/12/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public struct WebhookResponse: Encodable { 12 | public init( 13 | content: String? = nil, embeds: [BotEmbed]? = nil, tts: Bool? = nil, 14 | attachments: [NewAttachment]? = nil, 15 | components: [Component]? = nil, 16 | username: String? = nil, avatarURL: URL? = nil, 17 | allowedMentions: AllowedMentions? = nil, 18 | flags: Message.Flags? = nil, 19 | threadName: String? = nil 20 | ) { 21 | assert(content != nil || embeds != nil, "Must have at least one of content or embeds (files unsupported)") 22 | 23 | self.content = content 24 | self.username = username 25 | self.avatar_url = avatarURL 26 | self.tts = tts 27 | self.embeds = embeds 28 | self.allowed_mentions = allowedMentions 29 | self.components = components 30 | self.attachments = attachments 31 | self.flags = flags 32 | self.thread_name = threadName 33 | } 34 | 35 | public let content: String? 36 | public let username: String? 37 | public let avatar_url: URL? 38 | public let tts: Bool? 39 | public let embeds: [BotEmbed]? 40 | public let allowed_mentions: AllowedMentions? 41 | public let components: [Component]? 42 | public let attachments: [NewAttachment]? 43 | public let flags: Message.Flags? 44 | public let thread_name: String? 45 | 46 | enum CodingKeys: CodingKey { 47 | case content 48 | case username 49 | case avatar_url 50 | case tts 51 | case embeds 52 | case allowed_mentions 53 | case components 54 | case attachments 55 | case flags 56 | case thread_name 57 | } 58 | 59 | public func encode(to encoder: Encoder) throws { 60 | var container = encoder.container(keyedBy: CodingKeys.self) 61 | 62 | try container.encodeIfPresent(content, forKey: .content) 63 | try container.encodeIfPresent(username, forKey: .username) 64 | try container.encodeIfPresent(avatar_url, forKey: .avatar_url) 65 | try container.encodeIfPresent(tts, forKey: .tts) 66 | try container.encodeIfPresent(embeds, forKey: .embeds) 67 | try container.encodeIfPresent(allowed_mentions, forKey: .allowed_mentions) 68 | try container.encodeIfPresent(attachments, forKey: .attachments) 69 | try container.encodeIfPresent(flags, forKey: .flags) 70 | try container.encodeIfPresent(thread_name, forKey: .thread_name) 71 | 72 | if let components { 73 | var componentContainer = container.nestedUnkeyedContainer(forKey: .components) 74 | for component in components { 75 | try componentContainer.encode(component) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/DiscordKitBot/REST/APICommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICommand.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 26/11/22. 6 | // 7 | 8 | import Foundation 9 | import DiscordKitCore 10 | 11 | public extension DiscordREST { 12 | /// Create global application command 13 | /// 14 | /// > POST: `/applications/{application.id}/commands` 15 | /// This creates a global application command available in all guilds. 16 | func createGlobalCommand(_ command: NewAppCommand, applicationID: Snowflake) async throws -> AppCommand { 17 | try await postReq(path: "applications/\(applicationID)/commands", body: command) 18 | } 19 | 20 | /// Create guild application command 21 | /// 22 | /// > POST: `/applications/{application.id}/guilds/{guild.id}/commands` 23 | /// 24 | /// This creates a global application command scoped to a specific guild. 25 | /// 26 | /// > Tip: This is useful for testing as guild commands update immediately, 27 | /// > while updates to global commands take some time to propagate. 28 | func createGuildCommand( 29 | _ command: NewAppCommand, 30 | applicationID: Snowflake, guildID: Snowflake 31 | ) async throws -> AppCommand { 32 | try await postReq(path: "applications/\(applicationID)/guilds/\(guildID)/commands", body: command) 33 | } 34 | 35 | /// Utility method to conditionally create a guild or global command depending on parameters 36 | func createCommand( 37 | _ command: NewAppCommand, 38 | applicationID: Snowflake, guildID: Snowflake? 39 | ) async throws -> AppCommand { 40 | if let guildID = guildID { 41 | return try await createGuildCommand(command, applicationID: applicationID, guildID: guildID) 42 | } else { 43 | return try await createGlobalCommand(command, applicationID: applicationID) 44 | } 45 | } 46 | 47 | /// Builk overwrite global application command 48 | /// 49 | /// > PUT: `/applications/{application.id}/commands` 50 | /// 51 | /// Overwrite global application commands with those provided. 52 | /// 53 | /// > Warning: 54 | /// > This will overwrite **all** types of application commands: slash commands, user 55 | /// > commands, and message commands. 56 | func bulkOverwriteGlobalCommands( 57 | _ commands: [NewAppCommand], applicationID: Snowflake 58 | ) async throws -> [AppCommand] { 59 | try await putReq(path: "applications/\(applicationID)/commands", body: commands) 60 | } 61 | 62 | /// Builk overwrite guild application command 63 | /// 64 | /// > PUT: `/applications/{application.id}/guilds/{guild.id}/commands` 65 | /// 66 | /// Overwrite the application commands scoped to a certain guild with those provided. 67 | /// 68 | /// > Warning: 69 | /// > This will overwrite **all** types of application commands: slash commands, user 70 | /// > commands, and message commands. 71 | /// 72 | /// > Tip: This is useful for testing as guild commands update immediately, 73 | /// > while updates to global commands take some time to propagate. 74 | func bulkOverwriteGuildCommands( 75 | _ commands: [NewAppCommand], 76 | applicationID: Snowflake, 77 | guildID: Snowflake 78 | ) async throws -> [AppCommand] { 79 | try await putReq(path: "applications/\(applicationID)/guilds/\(guildID)/commands", body: commands) 80 | } 81 | 82 | /// Utility method to conditionally bulk overwrite guild or global commands depending on parameters 83 | func bulkOverwriteCommands( 84 | _ commands: [NewAppCommand], 85 | applicationID: Snowflake, guildID: Snowflake? 86 | ) async throws -> [AppCommand] { 87 | if let guildID = guildID { 88 | return try await bulkOverwriteGuildCommands(commands, applicationID: applicationID, guildID: guildID) 89 | } else { 90 | return try await bulkOverwriteGlobalCommands(commands, applicationID: applicationID) 91 | } 92 | } 93 | 94 | /// Send a response to an interaction 95 | func sendInteractionResponse(_ response: InteractionResponse, interactionID: Snowflake, token: String) async throws { 96 | try await postReq(path: "interactions/\(interactionID)/\(token)/callback", body: response) 97 | } 98 | 99 | /// Send a follow up response to an interaction 100 | /// 101 | /// > POST: `/webhooks/{application.id}/{interaction.token}` 102 | func sendInteractionFollowUp(_ response: WebhookResponse, applicationID: Snowflake, token: String) async throws -> Message { 103 | try await postReq(path: "webhooks/\(applicationID)/\(token)", body: response) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/APIUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIUtils.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 13/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | let iso8601 = { () -> ISO8601DateFormatter in 11 | let fmt = ISO8601DateFormatter() 12 | fmt.formatOptions = [.withInternetDateTime] 13 | return fmt 14 | }() 15 | 16 | let iso8601WithFractionalSeconds = { () -> ISO8601DateFormatter in 17 | let fmt = ISO8601DateFormatter() 18 | fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds] 19 | return fmt 20 | }() 21 | 22 | public extension DiscordREST { 23 | // Encoders and decoders with custom date en/decoders 24 | static let encoder: JSONEncoder = { 25 | let enc = JSONEncoder() 26 | enc.dateEncodingStrategy = .custom({ date, encoder in 27 | var container = encoder.singleValueContainer() 28 | let dateString = iso8601WithFractionalSeconds.string(from: date) 29 | try container.encode(dateString) 30 | }) 31 | return enc 32 | }() 33 | static let decoder: JSONDecoder = { 34 | let dec = JSONDecoder() 35 | dec.dateDecodingStrategy = .custom({ decoder in 36 | let container = try decoder.singleValueContainer() 37 | let dateString = try container.decode(String.self) 38 | 39 | if let date = iso8601.date(from: dateString) { 40 | return date 41 | } 42 | if let date = iso8601WithFractionalSeconds.date(from: dateString) { 43 | return date 44 | } 45 | 46 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)") 47 | }) 48 | return dec 49 | }() 50 | } 51 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/DiscordREST.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscordREST.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 5/6/22. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | #if canImport(FoundationNetworking) 12 | import FoundationNetworking 13 | #endif 14 | 15 | public class DiscordREST { 16 | static let log = Logger(label: "DiscordREST", level: nil) 17 | // How empty, everything is broken into smaller files (for now xD) 18 | 19 | static let session: URLSession = { 20 | // Create URL Session Configuration 21 | let configuration = URLSessionConfiguration.default 22 | 23 | // Define Request Cache Policy (causes stale data sometimes) 24 | // configuration.requestCachePolicy = .returnCacheDataElseLoad 25 | 26 | return URLSession(configuration: configuration) 27 | }() 28 | 29 | internal var token: String? 30 | 31 | public init() {} 32 | 33 | public func setToken(token: String?) { 34 | self.token = token 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Documentation.docc/DiscordKit.md: -------------------------------------------------------------------------------- 1 | # ``DiscordKitCore`` 2 | 3 | DiscordKit: A Discord API implementation for Swift. 4 | 5 | > DiscordKit is geared towards supporting human accounts, 6 | > and does not work with bot accounts at the moment. Adding 7 | > bot support would not be tough, and might be considered in the future. 8 | 9 | Supports most documented REST endpoints, plus most Gateway opcodes and events. 10 | Written in pure Swift 5, and uses only built-in macOS APIs (with the excecption 11 | of the `Reachability` package for improved Gateway reconnection). The Gateway 12 | implementation uses a convenient subscription pattern, for the greatest ease 13 | in listening for them. All responses are also decoded as dedicated structs 14 | per API "object", which takes the guesswork out of using this package. 15 | 16 | ## Topics 17 | 18 | ### Gateway 19 | 20 | Connect to, send payloads to and from, and listen for dispatched events from 21 | Discord's WebSocket Gateway API. 22 | 23 | Events such as message create, presence update and channel are received through 24 | the Gateway. This allows realtime events to be received, for each client to 25 | be notified immediately about an event. Presence updates, among others, are 26 | also sent to Discord through the Gateway. 27 | 28 | - ``DiscordGateway`` 29 | - ``CachedState`` 30 | - ``GatewayConnProperties`` 31 | 32 | ### REST 33 | 34 | Interact with the Discord REST API. Sending/fetching messages, getting a user's 35 | full profile and signaling typing start are all done through the REST API. 36 | 37 | - ``DiscordAPI`` 38 | 39 | ### Utilities 40 | 41 | - ``EventDispatch`` 42 | - ``DecodableThrowable`` 43 | 44 | ### Low-level Socket Management 45 | 46 | Handles the low level socket connection to the Discord Gateway, 47 | 48 | - ``RobustWebSocket`` 49 | - ``DecompressionEngine`` 50 | 51 | ### Configuration 52 | 53 | Configuration options like client parity version and URLs, used in various places 54 | to make requests/set headers/identify. 55 | 56 | - ``GatewayConfig`` 57 | - ``ClientParityVersion`` 58 | 59 | ### Endpoint "Objects" 60 | 61 | Structs of all (documented) payloads that can be sent to or received from Discord 62 | endpoints. These are named "objects" in the official Discord Developer docs. 63 | 64 | - ``Activity`` 65 | - ``ActivityAssets`` 66 | - ``ActivityButton`` 67 | - ``ActivityEmoji`` 68 | - ``ActivityOutgoing`` 69 | - ``ActivityParty`` 70 | - ``ActivitySecrets`` 71 | - ``ActivityTimestamp`` 72 | - ``AllowedMentions`` 73 | - ``Application`` 74 | - ``Attachment`` 75 | - ``Channel`` 76 | - ``ChannelMention`` 77 | - ``ChannelPinsUpdate`` 78 | - ``ChannelUnreadUpdate`` 79 | - ``ChannelUnreadUpdateItem`` 80 | - ``Connection`` 81 | - ``Embed`` 82 | - ``EmbedAuthor`` 83 | - ``EmbedField`` 84 | - ``EmbedFooter`` 85 | - ``EmbedMedia`` 86 | - ``EmbedProvider`` 87 | - ``Emoji`` 88 | - ``GatewayGuildRequestMembers`` 89 | - ``GatewayHeartbeat`` 90 | - ``GatewayHello`` 91 | - ``GatewayIdentify`` 92 | - ``GatewayIncoming`` 93 | - ``GatewayOutgoing`` 94 | - ``GatewayPresenceUpdate`` 95 | - ``GatewayResume`` 96 | - ``GatewayVoiceStateUpdate`` 97 | - ``Guild`` 98 | - ``GuildBan`` 99 | - ``GuildEmojisUpdate`` 100 | - ``GuildFolderItem`` 101 | - ``GuildIntegrationsUpdate`` 102 | - ``GuildMemberRemove`` 103 | - ``GuildMemberUpdate`` 104 | - ``GuildRoleDelete`` 105 | - ``GuildRoleEvt`` 106 | - ``GuildSchEvtUserEvt`` 107 | - ``GuildScheduledEvent`` 108 | - ``GuildScheduledEventEntityMeta`` 109 | - ``GuildStickersUpdate`` 110 | - ``GuildUnavailable`` 111 | - ``GuildWelcomeScreen`` 112 | - ``GuildWelcomeScreenChannel`` 113 | - ``Integration`` 114 | - ``IntegrationAccount`` 115 | - ``IntegrationApplication`` 116 | - ``Member`` 117 | - ``Message`` 118 | - ``MessageACKEvt`` 119 | - ``MessageActivity`` 120 | - ``MessageComponent`` 121 | - ``MessageDelete`` 122 | - ``MessageDeleteBulk`` 123 | - ``MessageInteraction`` 124 | - ``MessageReadAck`` 125 | - ``MessageReference`` 126 | - ``MutualGuild`` 127 | - ``NewAttachment`` 128 | - ``NewMessage`` 129 | - ``OutgoingMessage`` 130 | - ``PartialApplication`` 131 | - ``PartialGuild`` 132 | - ``PartialMessage`` 133 | - ``PartialPresenceUpdate`` 134 | - ``PermOverwrite`` 135 | - ``Presence`` 136 | - ``PresenceClientStatus`` 137 | - ``PresenceUpdate`` 138 | - ``PresenceUser`` 139 | - ``Reaction`` 140 | - ``ReadyEvt`` 141 | - ``Role`` 142 | - ``RoleTags`` 143 | - ``StageInstance`` 144 | - ``Sticker`` 145 | - ``StickerItem`` 146 | - ``SubscribeGuildEvts`` 147 | - ``Team`` 148 | - ``TeamMember`` 149 | - ``ThreadListSync`` 150 | - ``ThreadMember`` 151 | - ``ThreadMembersUpdate`` 152 | - ``ThreadMeta`` 153 | - ``TypingStart`` 154 | - ``User`` 155 | - ``UserProfile`` 156 | - ``UserSettings`` 157 | - ``VoiceState`` 158 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Collection+Identifiable.swift: -------------------------------------------------------------------------------- 1 | public extension Collection where Element: Identifiable { 2 | /// Returns the first element of the sequence that has an identifier matching the provided identifier. 3 | /// - Parameter identifier: The stable identity of the element you want to find. 4 | /// - Returns: The first element of the sequence that has an identifier matching the provided identifier. 5 | func first(identifiedBy identifier: Element.ID) -> Element? { 6 | first { $0.id == identifier } 7 | } 8 | 9 | /// Returns the first element of the sequence that has an identifier matching the identifier of the provided element. 10 | /// - Parameter element: The identifiable element you want to find. 11 | /// - Returns: The first element of the sequence that has an identifier matching the identifier of the provided element. 12 | func first(matchingIdentifierFor element: Element) -> Element? { 13 | first(identifiedBy: element.id) 14 | } 15 | 16 | /// Returns the first index in the sequence that has an identifier matching the provided identifier. 17 | /// - Parameter identifier: The stable identity of the element you want to find. 18 | /// - Returns: The first index in the sequence that has an identifier matching the provided identifier. 19 | func firstIndex(identifiedBy identifier: Element.ID) -> Index? { 20 | firstIndex { $0.id == identifier } 21 | } 22 | 23 | /// Returns the first index in the sequence that has an identifier matching the identifier of the provided element. 24 | /// - Parameter element: The identifiable element you want to find. 25 | /// - Returns: The first index in the sequence that has an identifier matching the identifier of the provided element. 26 | func firstIndex(matchingIdentifierFor element: Element) -> Index? { 27 | firstIndex(identifiedBy: element.id) 28 | } 29 | } 30 | 31 | public extension RangeReplaceableCollection where Element: Identifiable { 32 | /// Removes all the elements that have the given identifier. 33 | /// 34 | /// Use this method to remove every element in a collection that has 35 | /// the given identifier. The order of the remaining elements is preserved. 36 | /// - Parameter identifier: The stable identity of the element you want to find. 37 | /// - Complexity: O(*n*), where *n* is the length of the collection. 38 | mutating func removeAll(identifiedBy identifier: Element.ID) { 39 | removeAll { $0.id == identifier } 40 | } 41 | 42 | /// Removes all the elements that have the same identifier as the given element. 43 | /// 44 | /// Use this method to remove every element in a collection that has 45 | /// the same identifier as the given element. The order of the remaining elements 46 | /// is preserved. 47 | /// - Parameter element: The identifiable element you want to find. 48 | /// - Complexity: O(*n*), where *n* is the length of the collection. 49 | mutating func removeAll(matchingIdentifierFor element: Element) { 50 | removeAll(identifiedBy: element.id) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Int+decodeFlags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 7/3/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int { 11 | /// Takes a dict of bit positions to flags 12 | /// and returns an array of flags where the 13 | /// corrosponding bit in the Int is true 14 | func decodeFlags(flags: T) -> [T] where T.RawValue == Int { 15 | var decoded: [T] = [] 16 | T.allCases.forEach { flag in 17 | if (self & (1 << flag.rawValue)) != 0 { decoded.append(flag) } 18 | } 19 | return decoded 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Logger+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger+.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 25/11/22. 6 | // 7 | 8 | import Foundation 9 | import Logging 10 | 11 | public extension Logger { 12 | /// Create a Logger instance at a specific log level 13 | init(label: String, level: Level?) { 14 | self.init(label: label) 15 | if let level = level { 16 | logLevel = level 17 | } else { 18 | #if DEBUG 19 | logLevel = .trace 20 | #endif 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Objects/Message+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message+.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 8/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Message { 11 | func mentions(_ userID: Snowflake?) -> Bool { 12 | guard let userID else { return false } 13 | return mentions.first(identifiedBy: userID) != nil 14 | } 15 | } 16 | 17 | // MARK: Protocol Conformance 18 | extension Message: Equatable, Hashable { 19 | public static func == (lhs: Message, rhs: Message) -> Bool { 20 | lhs.id == rhs.id && lhs.content == rhs.content && lhs.attachments == rhs.attachments && lhs.embeds == rhs.embeds 21 | } 22 | 23 | public func hash(into hasher: inout Hasher) { 24 | hasher.combine(id) 25 | hasher.combine(content) 26 | hasher.combine(attachments) 27 | hasher.combine(embeds) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Objects/User+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 8/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | @inline(__always) 11 | private func _avatar(_ asset: HashedAsset?, size: Int?, discrim: String, id: Snowflake) -> URL { 12 | if let url = asset?.avatarURL(of: id, size: size) { 13 | return url 14 | } 15 | let index = discrim == "0" 16 | ? ((UInt(id) ?? 0) >> 22) % 6 17 | : (UInt(discrim) ?? 0) % 5 18 | // If user is without a set avatar, display one of the default ones. 19 | // These do not have support for custom sizes. 20 | return URL(string: "\(DiscordKitConfig.default.cdnURL)embed/avatars/\(index).png")! 21 | } 22 | 23 | public extension User { 24 | func avatarURL(size: Int? = nil) -> URL { 25 | _avatar(avatar, size: size, discrim: discriminator, id: id) 26 | } 27 | } 28 | 29 | public extension CurrentUser { 30 | func avatarURL(size: Int? = nil) -> URL { 31 | _avatar(avatar, size: size, discrim: discriminator, id: id) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/Snowflake+decode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snowflake+decode.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 26/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Snowflake { 11 | static let DISCORD_EPOCH = 1420070400000 12 | 13 | /// Decodes this Snowflake into a Date 14 | func decodeToDate() -> Date? { 15 | guard let intSnowflake = Int(self) else { return nil } 16 | let millisTimestamp = (intSnowflake >> 22) + Self.DISCORD_EPOCH 17 | return Date(timeIntervalSince1970: Double(millisTimestamp) / 1000.0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/String+random.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+random.swift 3 | // DiscordAPI 4 | // 5 | // Created by royal on 16/05/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension String { 11 | static func random(count: Int) -> String { 12 | let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 13 | return String((0.. URL { 25 | guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) 26 | else { return self } 27 | 28 | var qItems = components.queryItems ?? [] 29 | for item in items { qItems.append(item) } 30 | components.queryItems = qItems 31 | 32 | return components.url! 33 | } 34 | 35 | func setSize(size: Int?) -> URL { 36 | if let size = size { 37 | return self.appendingQueryItems( 38 | URLQueryItem(name: "size", value: String(size)) 39 | ) 40 | } 41 | return self 42 | } 43 | 44 | func setSize(width: Int?, height: Int?) -> URL { 45 | if let width = width, let height = height { 46 | return self.appendingQueryItems( 47 | URLQueryItem(name: "width", value: String(width)), 48 | URLQueryItem(name: "height", value: String(height)) 49 | ) 50 | } 51 | return self 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Extensions/URLSession+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 5/6/22. 6 | // 7 | 8 | import Foundation 9 | #if canImport(FoundationNetworking) 10 | import FoundationNetworking 11 | #endif 12 | 13 | @available(macOS, deprecated: 12.0, message: "Use the built-in API instead") 14 | public extension URLSession { 15 | func data(for request: URLRequest) async throws -> (Data, URLResponse) { 16 | try await withCheckedThrowingContinuation { continuation in 17 | let task = self.dataTask(with: request) { data, response, error in 18 | guard let data = data, let response = response else { 19 | let error = error ?? URLError(.badServerResponse) 20 | return continuation.resume(throwing: error) 21 | } 22 | 23 | continuation.resume(returning: (data, response)) 24 | } 25 | 26 | task.resume() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Gateway/GatewayIdentify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Identify.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension RobustWebSocket { 11 | /// Returns a `GatewayIdentify` struct for identification 12 | /// during Gateway connection 13 | /// 14 | /// Retrives the Discord token from the keychain and populates 15 | /// the `GatewayIdentify` struct. This method should not normally 16 | /// need to be called from outside `RobustWebSocket`. 17 | /// 18 | /// - Returns: A `GatewayIdentify` struct, or nil if the Discord token is 19 | /// not present in the keychain 20 | internal func getIdentify() -> GatewayIdentify? { 21 | return GatewayIdentify( 22 | token: token, 23 | properties: DiscordKitConfig.default.properties, 24 | compress: false, 25 | large_threshold: nil, 26 | shard: nil, 27 | presence: GatewayPresenceUpdate(since: 0, activities: [], status: .online, afk: false), 28 | client_state: DiscordKitConfig.default.isBot ? nil : ClientState( // Just a dummy client_state 29 | api_code_version: 0, 30 | guild_versions: .init(), 31 | highest_last_message_id: "0", 32 | initial_guild_id: nil, 33 | private_channels_version: "0", 34 | read_state_version: 0, 35 | user_guild_settings_version: -1, 36 | user_settings_version: -1 37 | ), 38 | capabilities: DiscordKitConfig.default.isBot ? nil : 8189, // TODO: Reverse engineer this 39 | intents: DiscordKitConfig.default.isBot ? DiscordKitConfig.default.intents : nil 40 | ) 41 | } 42 | 43 | /// Returns a GatewayResume struct based on the provided session ID and sequence 44 | /// 45 | /// This method is similar to the `getIdentify()` method, but 46 | /// returns a `GatewayResume` struct instead, which is used when 47 | /// attempting to resume. This method should not normally need 48 | /// to be called from outside `RobustWebSocket`. 49 | /// 50 | /// - Returns: A `GatewayResume` struct, or nil if the Discord token is 51 | /// not present in the keychain 52 | internal func getResume(seq: Int?, sessionID: String) -> GatewayResume? { 53 | return GatewayResume( 54 | token: token, 55 | session_id: sessionID, 56 | seq: seq 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Gateway/Intents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Intents.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 21/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// List of intents to select the events that should be sent back from the gateway 11 | /// 12 | /// 13 | public struct Intents: OptionSet, Encodable { 14 | public let rawValue: Int 15 | 16 | public init(rawValue: Int) { 17 | self.rawValue = rawValue 18 | } 19 | 20 | public func encode(to encoder: Encoder) throws { 21 | var container = encoder.singleValueContainer() 22 | try container.encode(rawValue) 23 | } 24 | 25 | /// Guilds 26 | static public let guilds = Self(rawValue: 1 << 0) 27 | /// Guild members 28 | /// 29 | /// > Warning: This is a privileged intent 30 | static public let guildMembers = Self(rawValue: 1 << 1) 31 | /// Guild bans 32 | static public let guildBans = Self(rawValue: 1 << 2) 33 | /// Guild emote and stickers 34 | static public let emoteSticker = Self(rawValue: 1 << 3) 35 | /// Guild integrations 36 | static public let integrations = Self(rawValue: 1 << 4) 37 | /// Guild webhooks 38 | static public let webhooks = Self(rawValue: 1 << 5) 39 | /// Guild invites 40 | static public let guildInvites = Self(rawValue: 1 << 6) 41 | /// Guild voice states 42 | static public let voiceStates = Self(rawValue: 1 << 7) 43 | /// Guild presences 44 | /// 45 | /// > Warning: This is a privileged intent 46 | static public let presences = Self(rawValue: 1 << 8) 47 | /// Guild messages 48 | static public let messages = Self(rawValue: 1 << 9) 49 | /// Guild message reactions 50 | static public let reactions = Self(rawValue: 1 << 10) 51 | /// Guild message typing 52 | static public let msgTyping = Self(rawValue: 1 << 11) 53 | /// Direct messages 54 | static public let directMsgs = Self(rawValue: 1 << 12) 55 | /// DM reactions 56 | static public let dmReactions = Self(rawValue: 1 << 13) 57 | /// DM message typing 58 | static public let dmMsgTyping = Self(rawValue: 1 << 14) 59 | /// Message content 60 | /// 61 | /// This intent does not represent individual events, but rather affects what data 62 | /// is present for events that could contain message content fields. 63 | /// > Warning: This is a privileged intent 64 | static public let messageContent = Self(rawValue: 1 << 15) 65 | /// Guild scheduled events 66 | static public let scheduledEvt = Self(rawValue: 1 << 16) 67 | /// Auto moderation configuration 68 | static public let autoModCfg = Self(rawValue: 1 << 20) 69 | /// Auto moderation execution 70 | static public let autoModExec = Self(rawValue: 1 << 20) 71 | 72 | static public let unprivileged: Self = [.guilds, .guildBans, .emoteSticker, .integrations, .webhooks, .guildInvites, .voiceStates, .messages, .reactions, .msgTyping, .directMsgs, .dmReactions, .dmMsgTyping, .scheduledEvt, .autoModCfg, .autoModExec] 73 | static public let privileged: Self = [.guildMembers, .presences, .messageContent] 74 | static public let all: Self = [.unprivileged, .privileged] 75 | } 76 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Gateway/README.md: -------------------------------------------------------------------------------- 1 | # Gateway ⛩️ 2 | 3 | Here're all the files that implement the Discord Gateway API. 4 | They handle everything from identify and heartbeating to 5 | reconnection. Currently, identification and heartbeating works 6 | very well, but reconnection/resuming not so well. 7 | 8 | Emits gateway events, connection state changes and auth errors 9 | thru a simple listener/emiter event helper in Utils. 10 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/AppCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCommand.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 12/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An application command 11 | /// 12 | /// > This is pretty incomplete at the moment since it was added as part 13 | /// > of another feature 14 | public struct AppCommand: Codable { 15 | public enum CommandType: Int, Codable { 16 | case slash = 1 17 | case user = 2 18 | case message = 3 19 | 20 | public init(from decoder: Decoder) throws { 21 | let container = try decoder.singleValueContainer() 22 | if container.decodeNil() { 23 | self = .slash 24 | return 25 | } 26 | guard let type = Self(rawValue: try container.decode(Int.self)) else { 27 | throw DecodingError.dataCorrupted(.init( 28 | codingPath: [], debugDescription: "Int value could not be cast to a valid command type" 29 | )) 30 | } 31 | self = type 32 | } 33 | } 34 | 35 | public let id: Snowflake 36 | public let type: CommandType 37 | public let application_id: Snowflake 38 | public let guild_id: Snowflake? 39 | public let name: String 40 | public let description: String 41 | } 42 | 43 | /// The type of an option 44 | public enum CommandOptionType: Int, Codable { 45 | /// A "sub-command" with no options 46 | case subCommand = 1 47 | /// A group for nesting other options 48 | case subCommandGroup = 2 49 | /// An option accepting a `String` value 50 | case string = 3 51 | /// An option accepting an `Int` value 52 | case integer = 4 53 | /// An option accepting a `Bool` value 54 | case boolean = 5 55 | /// An option accepting a user as its value 56 | case user = 6 57 | /// An option accepting a channel as its value 58 | case channel = 7 59 | /// An option accepting a role as its value 60 | case role = 8 61 | /// An option accepting a @mention as its value 62 | case mentionable = 9 63 | /// An option accepting a `Double` value 64 | case number = 10 65 | /// An option accepting a file attachment as its value 66 | case attachment = 11 67 | } 68 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Application: Codable { 11 | public let id: Snowflake 12 | public let name: String 13 | public let icon: String? // Icon hash of app 14 | public let description: String 15 | public let rpc_origins: [String]? // An array of rpc origin urls, if rpc is enabled 16 | public let bot_public: Bool // When false only app owner can join the app's bot to guilds 17 | public let bot_require_code_grant: Bool // When true the app's bot will only join upon completion of the full oauth2 code grant flow 18 | public let terms_of_service_url: String? 19 | public let privacy_policy_url: String? 20 | public let owner: User? 21 | public let summary: String 22 | public let verify_key: String 23 | public let team: Team? 24 | public let guild_id: Snowflake? 25 | public let primary_sku_id: Snowflake? 26 | public let slug: String? 27 | public let cover_image: String? // The application's default rich presence invite cover image hash 28 | public let flags: Int? // The application's public flags 29 | } 30 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Attachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attachment.swift 3 | // DiscordKit 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Attachment: Codable, Identifiable, Equatable, Hashable { 11 | public let id: Snowflake 12 | public let filename: String 13 | public let description: String? 14 | public let content_type: String? // Attachment's MIME type 15 | public let size: Int // Size of file in bytes 16 | public let url: String // Source URL of file 17 | public let proxy_url: String // A proxied URL of the file 18 | public let height: Int? // Height of file (if image) 19 | public let width: Int? // Width of file (if image) 20 | public let ephemeral: Bool? 21 | /// Thumbhash placeholder of image 22 | public let placeholder: String? 23 | /// Version of the contents of ``placeholder`` 24 | public let placeholder_version: Int? 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Channel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Channel.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum VideoQualityMode: Int, Codable { 11 | case auto = 1 // Discord chooses quality for optimal performance 12 | case full = 2 // 720p 13 | } 14 | 15 | public enum ChannelType: Int, Codable { 16 | case text = 0 17 | case dm = 1 // swiftlint:disable:this identifier_name 18 | case voice = 2 19 | case groupDM = 3 20 | case category = 4 21 | case news = 5 22 | case store = 6 // Depreciated game-selling channel 23 | case newsThread = 10 24 | case publicThread = 11 25 | case privateThread = 12 26 | case stageVoice = 13 27 | case directory = 14 // Hubs 28 | case forum = 15 // (still in development) a channel that can only contain threads 29 | 30 | case unknown = -1 // An unknown value 31 | 32 | public init(from decoder: Decoder) throws { 33 | let container = try decoder.singleValueContainer() 34 | self = Self(rawValue: try container.decode(Int.self)) ?? Self.unknown 35 | } 36 | } 37 | 38 | public struct Channel: Identifiable, Codable, GatewayData, Equatable { 39 | public static func == (lhs: Channel, rhs: Channel) -> Bool { 40 | lhs.id == rhs.id && lhs.name == rhs.name && lhs.position == rhs.position && lhs.parent_id == rhs.parent_id && lhs.permission_overwrites == rhs.permission_overwrites 41 | } 42 | 43 | public let id: Snowflake 44 | public let type: ChannelType 45 | public let guild_id: Snowflake? 46 | public let position: Int? 47 | public let permission_overwrites: [PermOverwrite]? 48 | public let name: String? 49 | public let topic: String? 50 | public let nsfw: Bool? 51 | public var last_message_id: Snowflake? // The id of the last message sent in this channel (may not point to an existing or valid message) 52 | public let bitrate: Int? 53 | public let user_limit: Int? 54 | public let rate_limit_per_user: Int? 55 | public let recipients: [User]? 56 | public let recipient_ids: [Snowflake]? 57 | public let icon: String? // Icon hash of group DM 58 | public let owner_id: Snowflake? 59 | public let application_id: Snowflake? 60 | public let parent_id: Snowflake? // ID of parent category (for channels) or parent channel (for threads) 61 | public let last_pin_timestamp: Date? 62 | public let rtc_region: String? 63 | public let video_quality_mode: VideoQualityMode? 64 | public let message_count: Int? // Approx. msg count in threads, stops counting at 50 65 | public let member_count: Int? // Approx. member count in threads, stops counting at 50 66 | public let thread_metadata: ThreadMeta? 67 | public let member: ThreadMember? // Thread member object for the current user, if they have joined the thread, only included on certain API endpoints 68 | public let default_auto_archive_duration: Int? // Default duration that the clients (not the API) will use for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 69 | public let permissions: Permissions? // Computed permissions for the invoking user in the channel, including overwrites, only included when part of the resolved data received on a slash command interaction 70 | } 71 | 72 | /* 73 | Structs for threads, which are reskinned channels that can be 74 | children of a channel, for small discussions and the like. 75 | */ 76 | 77 | public struct ThreadMeta: Codable { 78 | public let archived: Bool 79 | public let auto_archive_duration: Int // Duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 80 | public let archive_timestamp: Date 81 | public let locked: Bool 82 | public let invitable: Bool? // Only available in private threads 83 | public let create_timestamp: Date? // Timestamp when the thread was created; only populated for threads created after 2022-01-09 84 | } 85 | 86 | public struct ThreadMember: Codable, GatewayData { 87 | public let id: Snowflake? // ID of thread 88 | public let user_id: Snowflake? // ID of user 89 | public let join_timestamp: Date // When user last joined thread 90 | public let flags: Int // Any user-thread settings, currently only used for notifications 91 | public let guild_id: Snowflake? 92 | } 93 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 22/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum ConnectionVisibility: Int, Codable { 11 | case none = 0 // Only visible to owner 12 | case everyone = 1 13 | } 14 | 15 | // Note: purely by observation 16 | public enum ConnectionType: String { 17 | case steam = "steam" 18 | case youtube = "youtube" 19 | case spotify = "spotify" 20 | case github = "github" 21 | case twitch = "twitch" 22 | case reddit = "reddit" 23 | case facebook = "facebook" 24 | case twitter = "twitter" 25 | case xbox = "xbox" 26 | case battleNet = "battlenet" 27 | case playstation = "playstation" 28 | case leagueOfLegends = "leagueoflegends" 29 | case unknown 30 | } 31 | extension ConnectionType: Codable { 32 | public init(from decoder: Decoder) throws { 33 | self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown 34 | } 35 | } 36 | 37 | // Connections with external accounts (e.g. Reddit, YouTube, Steam etc.) 38 | public struct Connection: Codable, GatewayData { 39 | public let id: String 40 | public let name: String 41 | public let type: ConnectionType 42 | public let revoked: Bool? 43 | public let integrations: [Integration]? 44 | public let verified: Bool 45 | public let friend_sync: Bool? 46 | public let show_activity: Bool? 47 | public let visibility: ConnectionVisibility? 48 | } 49 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Embed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Embed.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum EmbedType: String, Codable { 11 | case rich = "rich" // Generic embed rendered from embed attributes 12 | case image = "image" 13 | case video = "video" 14 | case gifVid = "gifv" // GIF rendered as video 15 | case article = "article" 16 | case link = "link" 17 | case autoModAlert = "auto_moderation_message" 18 | case autoModNotif = "auto_moderation_notification" 19 | } 20 | 21 | public struct Embed: Codable, Identifiable, Equatable, Hashable { 22 | public init(title: String? = nil, type: EmbedType? = nil, description: String? = nil, url: String? = nil, timestamp: Date? = nil, color: Int? = nil, footer: EmbedFooter? = nil, image: EmbedMedia? = nil, thumbnail: EmbedMedia? = nil, video: EmbedMedia? = nil, provider: EmbedProvider? = nil, author: EmbedAuthor? = nil, fields: [EmbedField]? = nil) { 23 | self.title = title 24 | self.type = type 25 | self.description = description 26 | self.url = url 27 | self.timestamp = timestamp 28 | self.color = color 29 | self.footer = footer 30 | self.image = image 31 | self.thumbnail = thumbnail 32 | self.video = video 33 | self.provider = provider 34 | self.author = author 35 | self.fields = fields 36 | } 37 | 38 | public var title: String? 39 | public let type: EmbedType? 40 | public var description: String? 41 | public var url: String? 42 | public var timestamp: Date? 43 | public var color: Int? 44 | public var footer: EmbedFooter? 45 | public let image: EmbedMedia? 46 | public let thumbnail: EmbedMedia? 47 | public let video: EmbedMedia? 48 | public let provider: EmbedProvider? 49 | public var author: EmbedAuthor? 50 | public var fields: [EmbedField]? 51 | 52 | public var id: String { 53 | "\(title ?? "")\(description ?? "")\(url ?? "")\(String(color ?? 0))\(String(timestamp?.timeIntervalSince1970 ?? 0))" 54 | } 55 | } 56 | 57 | public struct EmbedFooter: Codable, Equatable, Hashable { 58 | public init(text: String, icon_url: String? = nil, proxy_icon_url: String? = nil) { 59 | self.text = text 60 | self.icon_url = icon_url 61 | self.proxy_icon_url = proxy_icon_url 62 | } 63 | 64 | public let text: String 65 | public let icon_url: String? 66 | public let proxy_icon_url: String? 67 | } 68 | 69 | public struct EmbedMedia: Codable, Equatable, Hashable { 70 | public let url: String 71 | public let proxy_url: String? 72 | public let height: Int? 73 | public let width: Int? 74 | } 75 | 76 | public struct EmbedProvider: Codable, Equatable, Hashable { 77 | public let name: String? 78 | public let url: String? 79 | } 80 | 81 | public struct EmbedAuthor: Codable, Equatable, Hashable { 82 | public let name: String 83 | public let url: String? 84 | public let icon_url: String? 85 | public let proxy_icon_url: String? 86 | } 87 | 88 | public struct EmbedField: Codable, Identifiable, Equatable, Hashable { 89 | public let name: String 90 | public let value: String 91 | public let inline: Bool? 92 | public var id: String { 93 | name + value 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Emoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emoji.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Emoji: Codable { 11 | public init(id: Snowflake? = nil, name: String? = nil, roles: [Role]? = nil, user: User? = nil, require_colons: Bool? = nil, managed: Bool? = nil, animated: Bool? = nil, available: Bool? = nil) { 12 | self.id = id 13 | self.name = name 14 | self.roles = roles 15 | self.user = user 16 | self.require_colons = require_colons 17 | self.managed = managed 18 | self.animated = animated 19 | self.available = available 20 | } 21 | 22 | public let id: Snowflake? 23 | public let name: String? // Can be null only in reaction emoji objects 24 | public let roles: [Role]? 25 | public let user: User? // User that created this emoji 26 | public let require_colons: Bool? // Whether this emoji must be wrapped in colons 27 | public let managed: Bool? 28 | public let animated: Bool? 29 | public let available: Bool? // Whether this emoji can be used, may be false due to loss of Server Boosts 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Integration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Integration.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 22/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum IntegrationType: String, Codable { 11 | case youtube 12 | case twitch 13 | case discord 14 | } 15 | 16 | public enum InteractionExpireBehaviour: Int, Codable { 17 | case removeRole = 0 18 | case kick = 1 19 | } 20 | 21 | public struct Integration: Codable, GatewayData { 22 | public let id: Snowflake 23 | public let name: String 24 | public let type: IntegrationType 25 | public let enabled: Bool 26 | public let syncing: Bool? 27 | public let role_id: Snowflake? // ID that this integration uses for "subscribers" 28 | public let enable_emoticons: Bool? // Twitch only, currently 29 | public let expire_behavior: InteractionExpireBehaviour? 30 | public let expire_grace_period: Int? // The grace period (in days) before expiring subscribers 31 | public let user: User? 32 | public let account: IntegrationAccount 33 | public let synced_at: Date? 34 | public let subscriber_count: Int? 35 | public let revoked: Bool? 36 | public let application: IntegrationApplication? 37 | } 38 | 39 | public struct IntegrationAccount: Codable, GatewayData { 40 | public let id: String 41 | public let name: String 42 | } 43 | 44 | public struct IntegrationApplication: Codable, GatewayData { 45 | public let id: Snowflake // ID of the app 46 | public let name: String 47 | public let icon: String? 48 | public let description: String 49 | public let summary: String 50 | public let bot: User? // The bot associated with this application 51 | } 52 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Levels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Levels.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum VerificationLevel: Int, Codable { 11 | case `none` = 0 // Unrestricted 12 | case low = 1 // Must have verified email 13 | case medium = 2 // Registeded on Discord for > 5 mins 14 | case high = 3 // Member of server for > 10 mins 15 | case veryHigh = 4 // Must have verified hp 16 | } 17 | 18 | public enum MessageNotifLevel: Int, Codable { 19 | case all = 0 20 | case mentions = 1 21 | } 22 | 23 | public enum ExplicitContentFilterLevel: Int, Codable { 24 | case disabled = 0 25 | case withoutRoles = 1 // Scan messages from members without roles 26 | case all = 2 // Scan everyone's messages 27 | } 28 | 29 | public enum MFALevel: Int, Codable { 30 | case `none` = 0 31 | case elevated = 1 32 | } 33 | 34 | public enum NSFWLevel: Int, Codable { 35 | case `default` = 0 36 | case explicit = 1 37 | case `safe` = 2 38 | case ageRestricted = 3 39 | } 40 | 41 | public enum PremiumLevel: Int, Codable { 42 | case `none` = 0 43 | case tier1 = 1 44 | case tier2 = 2 45 | case tier3 = 3 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Locale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Locale.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Locale: String, Codable { 11 | case englishUS = "en-US" 12 | case englishGB = "en-GB" 13 | case bulgarian = "bg" 14 | case chineseChina = "zh-CN" 15 | case chineseTaiwan = "zh-TW" 16 | case croatian = "hr" 17 | case czech = "cs" 18 | case danish = "da" 19 | case dutch = "nl" 20 | case finnish = "fi" 21 | case french = "fr" 22 | case german = "de" 23 | case greek = "el" 24 | case hindi = "hi" 25 | case hungarian = "hu" 26 | case italian = "it" 27 | case japanese = "ja" 28 | case korean = "ko" 29 | case lithuanian = "lt" 30 | case norwegian = "no" 31 | case polish = "pl" 32 | case portugueseBrazil = "pt-BR" 33 | case romaninan = "ro" 34 | case russian = "ru" 35 | case spannishSpain = "es-ES" 36 | case swedish = "sv-SE" 37 | case thai = "th" 38 | case turkish = "tr" 39 | case ukrainian = "uk" 40 | case vietnamese = "vi" 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Member.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Member.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Member: Codable, GatewayData { 11 | public let user: User? 12 | public let nick: String? 13 | public let avatar: String? 14 | public let roles: [Snowflake] 15 | public let joined_at: Date 16 | public let premium_since: Date? // When the user started boosting the guild 17 | public let deaf: Bool 18 | public let mute: Bool 19 | public let pending: Bool? 20 | public let permissions: String? // Total permissions of the member in the channel, including overwrites, returned when in the interaction object 21 | public let communication_disabled_until: Date? // When the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out 22 | public let guild_id: Snowflake? 23 | public let user_id: Snowflake? // Only present in merged_members in READY payload! 24 | 25 | public init(from updateMember: GuildMemberUpdate, merging: Self? = nil) { 26 | self.user = updateMember.user 27 | self.nick = updateMember.nick 28 | self.avatar = updateMember.avatar 29 | self.roles = updateMember.roles 30 | self.joined_at = merging?.joined_at ?? updateMember.joined_at ?? .distantPast 31 | self.premium_since = updateMember.premium_since 32 | self.deaf = merging?.deaf ?? updateMember.deaf ?? false 33 | self.mute = merging?.mute ?? updateMember.mute ?? false 34 | self.pending = updateMember.pending 35 | self.permissions = merging?.permissions 36 | self.communication_disabled_until = updateMember.communication_disabled_until 37 | self.guild_id = updateMember.guild_id 38 | self.user_id = merging?.user_id 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Mention.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mention.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum AllowedMentionTypes: String, Codable { 11 | case role = "roles" // Controls role mentions 12 | case user = "users" // Controls user mentions 13 | case everyone = "everyone" // Controls @everyone and @here mentions 14 | } 15 | 16 | public struct AllowedMentions: Codable { 17 | public init(parse: [AllowedMentionTypes]? = nil, roles: [Snowflake]? = nil, users: [Snowflake]? = nil, replied_user: Bool? = nil) { 18 | self.parse = parse 19 | self.roles = roles 20 | self.users = users 21 | self.replied_user = replied_user 22 | } 23 | 24 | /// An array of allowed mention types to parse from the content. 25 | public let parse: [AllowedMentionTypes]? 26 | /// Array of role\_ids to mention (Max size of 100) 27 | public let roles: [Snowflake]? 28 | /// Array of user\_ids to mention (Max size of 100) 29 | public let users: [Snowflake]? 30 | /// For replies, whether to mention the author of the message being replied to (default false) 31 | public let replied_user: Bool? 32 | } 33 | 34 | public struct ChannelMention: Codable { 35 | public let id: Snowflake 36 | public let guild_id: Snowflake 37 | public let type: ChannelType 38 | public let name: String 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Nonce.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Nonce.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 19/3/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Nonce: Codable, Equatable { 11 | case string(String) 12 | case int(Int) 13 | 14 | public static func == (lhs: Self, rhs: Self) -> Bool { 15 | lhs.value == rhs.value 16 | } 17 | 18 | public var value: String { 19 | switch self { 20 | case .string(let val): 21 | return val 22 | case .int(let val): 23 | return String(val) 24 | } 25 | } 26 | 27 | public func encode(to encoder: any Encoder) throws { 28 | var container = encoder.singleValueContainer() 29 | switch self { 30 | case .string(let val): 31 | try container.encode(val) 32 | case .int(let val): 33 | try container.encode(val) 34 | } 35 | } 36 | 37 | public init(from decoder: any Decoder) throws { 38 | let container = try decoder.singleValueContainer() 39 | if let str = try? container.decode(String.self) { 40 | self = .string(str) 41 | } else { 42 | self = .int(try container.decode(Int.self)) 43 | } 44 | } 45 | 46 | public init() { 47 | self = .string(Snowflake(timestamp: Date())) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Presence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presence.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// User presences sent in the ``GatewayEvent/readySupplemental`` event 11 | public struct Presence: GatewayData { 12 | public let user_id: Snowflake 13 | public let status: PresenceStatus 14 | public let client_status: PresenceClientStatus 15 | public let activities: [Activity] 16 | 17 | public init(userID: Snowflake, status: PresenceStatus, clientStatus: PresenceClientStatus, activities: [Activity]) { 18 | self.user_id = userID 19 | self.status = status 20 | self.client_status = clientStatus 21 | self.activities = activities 22 | } 23 | 24 | public init(update: PresenceUpdate) { 25 | user_id = update.user.id 26 | status = update.status 27 | client_status = update.client_status 28 | activities = update.activities 29 | } 30 | } 31 | 32 | public enum PresenceStatus: String, Codable { 33 | case idle 34 | case dnd 35 | case online 36 | case offline 37 | case invisible 38 | } 39 | 40 | public struct PresenceUser: Codable, GatewayData { 41 | public let id: Snowflake 42 | public let username: String? 43 | public let discriminator: String? 44 | public let avatar: String? 45 | } 46 | 47 | public struct PresenceUpdate: GatewayData { 48 | public let user: PresenceUser 49 | public let guild_id: Snowflake? 50 | public let status: PresenceStatus 51 | public let activities: [Activity] 52 | public let client_status: PresenceClientStatus 53 | } 54 | 55 | public struct PartialPresenceUpdate: GatewayData { 56 | public let user: PresenceUser 57 | public let guild_id: Snowflake? 58 | public let status: PresenceStatus? 59 | public let activities: [Activity]? 60 | public let client_status: PresenceClientStatus? 61 | } 62 | 63 | public struct PresenceClientStatus: Codable, GatewayData { 64 | public init(desktop: PresenceStatus? = nil, mobile: PresenceStatus? = nil, web: PresenceStatus? = nil) { 65 | self.desktop = desktop 66 | self.mobile = mobile 67 | self.web = web 68 | } 69 | 70 | public let desktop: PresenceStatus? 71 | public let mobile: PresenceStatus? 72 | public let web: PresenceStatus? 73 | } 74 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Reaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reaction.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Reaction: Codable { 11 | public let count: Int 12 | public let me: Bool // swiftlint:disable:this identifier_name 13 | public let emoji: Emoji 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Snowflake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snowflake.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias Snowflake = String 11 | 12 | extension Snowflake { 13 | init(timestamp: Date = .init()) { 14 | let epoch = Int(timestamp.timeIntervalSince1970*1000) - Self.DISCORD_EPOCH 15 | self.init(epoch << 22) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Stage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stage.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum StageDiscovery: Int, Codable { 11 | case `public` = 1 // Depreciated 12 | case guildOnly = 2 13 | } 14 | 15 | public struct StageInstance: Codable, GatewayData { 16 | public let id: Snowflake 17 | public let guild_id: Snowflake 18 | public let channel_id: Snowflake 19 | public let topic: String 20 | public let privacy_level: StageDiscovery 21 | public let discoverable_disabled: Bool // Depreciated 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Sticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sticker.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum StickerType: Int, Codable { 11 | case standard = 1 12 | case guild 13 | } 14 | 15 | public enum StickerFormat: Int, Codable { 16 | case png = 1 17 | case aPNG // Animated PNG 18 | case lottie 19 | case gif 20 | } 21 | 22 | public struct Sticker: Codable, GatewayData, Identifiable { 23 | public let id: Snowflake 24 | public let pack_id: Snowflake? // For standard stickers, id of the pack the sticker is from 25 | public let name: String 26 | public let description: String? 27 | public let tags: String // Autocomplete/suggestion tags for the sticker (max 200 characters), might be CSV 28 | public let asset: String? 29 | // Depreciated: now an empty string 30 | public let type: StickerType 31 | public let format_type: StickerFormat 32 | public let available: Bool? // Whether this guild sticker can be used, may be false due to loss of Server Boosts 33 | public let guild_id: Snowflake? 34 | public let user: User? // User that uploaded sticker 35 | public let sort_value: Int? // Sticker's sort order in its pack 36 | } 37 | 38 | public struct StickerItem: Codable, Identifiable { 39 | public let id: Snowflake 40 | public let name: String 41 | public let format_type: StickerFormat 42 | } 43 | 44 | public struct StickerPack: Codable, GatewayData, Identifiable { 45 | public let id: Snowflake 46 | public let stickers: [StickerItem] 47 | public let name: String 48 | public let sku_id: Snowflake 49 | public let cover_sticker_id: Snowflake? 50 | public let description: String 51 | public let banner_asset_id: HashedAsset? 52 | } 53 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Team.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Team.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 19/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Team: Codable { 11 | public let icon: String? 12 | public let id: Snowflake 13 | public let members: [TeamMember] 14 | public let name: String 15 | public let owner_user_id: Snowflake 16 | } 17 | 18 | public enum MembershipState: Int, Codable { 19 | case invited = 1 20 | case accepted = 2 21 | } 22 | 23 | public struct TeamMember: Codable { 24 | public let membership_state: MembershipState 25 | public let permissions: [String] // Will always be ["*"] 26 | public let team_id: Snowflake 27 | public let user: User 28 | } 29 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/User+PremiumType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension User { 4 | enum PremiumType: Int, Codable, Hashable, Identifiable, CustomStringConvertible { 5 | /// No premium subscription 6 | case none = 0 7 | 8 | /// Nitro classic 9 | case nitroClassic = 1 10 | 11 | /// Nitro 12 | case nitro = 2 13 | 14 | /// Nitro Basic 15 | case nitroBasic = 3 16 | 17 | // MARK: Identifiable 18 | 19 | public var id: RawValue { 20 | return rawValue 21 | } 22 | 23 | // MARK: CustomStringConvertible 24 | 25 | public var description: String { 26 | switch self { 27 | case .none: 28 | return "None" 29 | 30 | case .nitroClassic: 31 | return "Nitro Classic" 32 | 33 | case .nitro: 34 | return "Nitro" 35 | 36 | case .nitroBasic: 37 | return "Nitro Basic" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Data/Voice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Voice.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VoiceState: Codable, GatewayData { 11 | public let guild_id: Snowflake? 12 | public let channel_id: Snowflake? 13 | public let user_id: Snowflake 14 | public let member: Member? 15 | public let session_id: String 16 | public let deaf: Bool // Deafened by server 17 | public let mute: Bool 18 | public let self_deaf: Bool 19 | public let self_mute: Bool 20 | public let self_stream: Bool? 21 | public let self_video: Bool 22 | public let suppress: Bool 23 | public let request_to_speak_timestamp: Date? // Time when user requested to speak, if any 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/ApplicationObj.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationObj.swift 3 | // Creating a file called Application.swift causes a build error 4 | // Xcode, why didn't you tell me? 5 | // 6 | // DiscordAPI 7 | // 8 | // Created by Vincent Kwok on 21/2/22. 9 | // 10 | 11 | import Foundation 12 | 13 | /// Partial application 14 | /// 15 | /// Just to get things working, add full application later 16 | public struct PartialApplication: Codable, GatewayData { 17 | public let id: Snowflake 18 | public let flags: Int 19 | } 20 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ChUnreadUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChUnreadUpdate.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 13/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ChannelUnreadUpdateItem: Codable { 11 | public let last_message_id: Snowflake 12 | public let id: Snowflake // ID of channel 13 | } 14 | 15 | public struct ChannelUnreadUpdate: Codable, GatewayData { 16 | public let guild_id: Snowflake 17 | public let channel_unread_updates: [ChannelUnreadUpdateItem] 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ChannelPinsUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChannelPinsUpdate.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // Not sent when pinned message is deleted 11 | public struct ChannelPinsUpdate: Codable, GatewayData { 12 | public let guild_id: Snowflake? 13 | public let channel_id: Snowflake 14 | public let last_pin_timestamp: Date? 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GatewayEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Events.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 20/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // Basically just one long enum 11 | 12 | public enum GatewayEvent: String, Codable { 13 | // MARK: Gateway WebSocket Lifecycle 14 | case ready = "READY" 15 | case readySupplemental = "READY_SUPPLEMENTAL" 16 | case resumed = "RESUMED" // End of events replay 17 | 18 | // MARK: Channels 19 | case channelCreate = "CHANNEL_CREATE" 20 | case channelUpdate = "CHANNEL_UPDATE" 21 | case channelDelete = "CHANNEL_DELETE" 22 | case channelPinUpdate = "CHANNEL_PIN_UPDATE" 23 | 24 | // MARK: Threads 25 | case threadCreate = "THREAD_CREATE" 26 | case threadUpdate = "THREAD_UPDATE" 27 | case threadDelete = "THREAD_DELETE" 28 | case threadListSync = "THREAD_LIST_SYNC" // Sent when gaining access to a channel, contains all active threads in that channel 29 | case threadMemberUpdate = "THREAD_MEMBER_UPDATE" // Thread member for the current user was updated 30 | case threadMembersUpdate = "THREAD_MEMBERS_UPDATE" 31 | 32 | // MARK: - Guilds 33 | case guildCreate = "GUILD_CREATE" 34 | case guildUpdate = "GUILD_UPDATE" 35 | case guildDelete = "GUILD_DELETE" 36 | case guildBanAdd = "GUILD_BAN_ADD" 37 | case guildBanRemove = "GUILD_BAN_REMOVE" 38 | case guildEmojisUpdate = "GUILD_EMOJIS_UPDATE" 39 | case guildStickersUpdate = "GUILD_STICKERS_UPDATE" 40 | case guildIntegrationsUpdate = "GUILD_INTEGRATIONS_UPDATE" 41 | // MARK: Guild Members 42 | case guildMemberAdd = "GUILD_MEMBER_ADD" 43 | case guildMemberRemove = "GUILD_MEMBER_REMOVE" 44 | case guildMemberUpdate = "GUILD_MEMBER_UPDATE" 45 | case guildMembersChunk = "GUILD_MEMBERS_CHUNK" 46 | case guildMemberListUpdate = "GUILD_MEMBER_LIST_UPDATE" 47 | // MARK: Guild Roles 48 | case guildRoleCreate = "GUILD_ROLE_CREATE" 49 | case guildRoleUpdate = "GUILD_ROLE_UPDATE" 50 | case guildRoleDelete = "GUILD_ROLE_DELETE" 51 | // MARK: Guild scheduled events 52 | case guildSchEvtCreate = "GUILD_SCHEDULED_EVENT_CREATE" 53 | case guildSchEvtUpdate = "GUILD_SCHEDULED_EVENT_UPDATE" 54 | case guildSchEvtDelete = "GUILD_SCHEDULED_EVENT_DELETE" 55 | case guildSchEvtUserAdd = "GUILD_SCHEDULED_EVENT_USER_ADD" 56 | case guildSchEvtUserRemove = "GUILD_SCHEDULED_EVENT_USER_REMOVE" 57 | 58 | // MARK: Integrations 59 | case integrationCreate = "INTEGRATION_CREATE" 60 | case integrationUpdate = "INTEGRATION_UPDATE" 61 | case integrationDelete = "INTEGRATION_DELETE" 62 | 63 | // MARK: Interaction 64 | case interactionCreate = "INTERACTION_CREATE" 65 | 66 | // MARK: Invites 67 | case inviteCreate = "INVITE_CREATE" 68 | case inviteDelete = "INVITE_DELETE" 69 | 70 | // MARK: - Messages 71 | case messageCreate = "MESSAGE_CREATE" 72 | case messageUpdate = "MESSAGE_UPDATE" 73 | case messageDelete = "MESSAGE_DELETE" 74 | case messageACK = "MESSAGE_ACK" // When messages have been read 75 | case messageDeleteBulk = "MESSAGE_DELETE_BULK" 76 | // MARK: Message Reactions 77 | case messageReactAdd = "MESSAGE_REACTION_ADD" 78 | case messageReactRemove = "MESSAGE_REACTION_REMOVE" 79 | case messageReactRemoveAll = "MESSAGE_REACTION_REMOVE_ALL" 80 | case messageReactRemoveEmoji = "MESSAGE_REACTION_REMOVE_EMOJI" 81 | 82 | // MARK: Presence Update 83 | case presenceUpdate = "PRESENCE_UPDATE" 84 | 85 | // MARK: Sessions 86 | case sessionsReplace = "SESSIONS_REPLACE" 87 | 88 | // MARK: Stages 89 | case stageInstanceCreate = "STAGE_INSTANCE_CREATE" 90 | case stageInstanceDelete = "STAGE_INSTANCE_DELETE" 91 | case stageInstanceUpdate = "STAGE_INSTANCE_UPDATE" 92 | 93 | // MARK: Typing 94 | case typingStart = "TYPING_START" 95 | 96 | // MARK: Misc Updates 97 | case userUpdate = "USER_UPDATE" 98 | case voiceStateUpdate = "VOICE_STATE_UPDATE" 99 | case voiceServerUpdate = "VOICE_SERVER_UPDATE" 100 | case webhooksUpdate = "WEBHOOKS_UPDATE" 101 | 102 | // MARK: Human account-specific Events 103 | case channelUnreadUpdate = "CHANNEL_UNREAD_UPDATE" 104 | case userSettingsUpdate = "USER_SETTINGS_UPDATE" 105 | case userSettingsProtoUpdate = "USER_SETTINGS_PROTO_UPDATE" 106 | } 107 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GatewaySettingsProtoUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GatewaySettingsProtoUpdate.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 7/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GatewaySettingsProtoUpdate: GatewayData { 11 | public let settings: GatewaySettingsProto 12 | public let partial: Bool 13 | } 14 | 15 | public struct GatewaySettingsProto: GatewayData { 16 | public let type: Int 17 | public let proto: String 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildBan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildBan.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GuildBan: Codable, GatewayData { 11 | public let guild_id: Snowflake 12 | public let user: User 13 | } 14 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildMemberEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildMember.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GuildMemberRemove: Codable, GatewayData { 11 | public let guild_id: Snowflake 12 | public let user: User 13 | } 14 | 15 | /// Sent when a guild member is updated. 16 | /// This will also fire when the user object of a guild member changes. 17 | /// Very similar to Member, but with some optional value changes 18 | public struct GuildMemberUpdate: Codable, GatewayData { 19 | public let guild_id: Snowflake 20 | public let roles: [Snowflake] // User role IDs 21 | public let user: User 22 | public let nick: String? 23 | public let avatar: String? // User's guild avatar hash 24 | public let joined_at: Date? 25 | public let premium_since: Date? // When user started boosting guild 26 | public let deaf: Bool? 27 | public let mute: Bool? 28 | public let pending: Bool? 29 | public let communication_disabled_until: Date? 30 | } 31 | 32 | public struct GuildMemberListUpdate: Decodable, GatewayData { 33 | public struct Group: Decodable, Identifiable, GatewayData { 34 | public let id: Snowflake 35 | public let count: Int 36 | } 37 | 38 | public struct Data: Codable { 39 | public struct Group: Codable { 40 | public let id: Snowflake 41 | } 42 | 43 | public let member: Member? 44 | public let group: Group? 45 | } 46 | 47 | public enum UpdateOp: Decodable, GatewayData { 48 | private enum Op: String, Codable { 49 | case update = "UPDATE" 50 | case sync = "SYNC" 51 | case delete = "DELETE" 52 | case insert = "INSERT" 53 | case invalidate = "INVALIDATE" 54 | } 55 | 56 | case update(Data, index: Int) 57 | case insert(Data, index: Int) 58 | case delete(Int) 59 | case sync([Data], range: DiscordRange) 60 | case invalidate(DiscordRange) 61 | 62 | enum CodingKeys: CodingKey { 63 | case index 64 | case range 65 | case item 66 | case items 67 | case op 68 | } 69 | 70 | public init(from decoder: any Decoder) throws { 71 | let container = try decoder.container(keyedBy: CodingKeys.self) 72 | let op = try container.decode(Op.self, forKey: .op) 73 | switch op { 74 | case .sync: 75 | self = .sync(try container.decode([Data].self, forKey: .items), range: try container.decode(DiscordRange.self, forKey: .range)) 76 | case .update: 77 | self = .update(try container.decode(Data.self, forKey: .item), index: try container.decode(Int.self, forKey: .index)) 78 | case .insert: 79 | self = .insert(try container.decode(Data.self, forKey: .item), index: try container.decode(Int.self, forKey: .index)) 80 | case .delete: 81 | self = .delete(try container.decode(Int.self, forKey: .index)) 82 | case .invalidate: 83 | self = .invalidate(try container.decode(DiscordRange.self, forKey: .range)) 84 | } 85 | } 86 | } 87 | 88 | public let groups: [Group] 89 | public let guild_id: Snowflake 90 | public let id: String 91 | public let member_count: Int 92 | public let online_count: Int 93 | public let ops: [UpdateOp] 94 | } 95 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildMembersChunk.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildMembersChunk.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 11/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Guild Members Chunk 11 | /// 12 | /// Sent in response to a 13 | public struct GuildMembersChunk: GatewayData { 14 | public let guild_id: Snowflake 15 | public let members: [Member] 16 | public let chunk_index: Int 17 | public let chunk_count: Int 18 | public let not_found: [Snowflake]? 19 | public let presences: [Presence]? 20 | public let nonce: String? 21 | } 22 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildMiscUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildMiscUpdate.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GuildEmojisUpdate: Codable, GatewayData { 11 | public let guild_id: Snowflake 12 | public let emojis: [Emoji] 13 | } 14 | 15 | public struct GuildStickersUpdate: Codable, GatewayData { 16 | public let guild_id: Snowflake 17 | public let stickers: [Sticker] 18 | } 19 | 20 | public struct GuildIntegrationsUpdate: Codable, GatewayData { 21 | public let guild_id: Snowflake 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildRoleEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildRoleEvt.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GuildRoleEvt: Codable, GatewayData { 11 | public let guild_id: Snowflake 12 | public let role: Role 13 | } 14 | 15 | public struct GuildRoleDelete: Codable, GatewayData { 16 | public let guild_id: Snowflake 17 | public let role: Snowflake 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/GuildSchEvtUserEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuildSchEvtUserEvt.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GuildSchEvtUserEvt: Codable, GatewayData { 11 | public let guild_scheduled_event_id: Snowflake 12 | public let user_id: Snowflake 13 | public let guild_id: Snowflake 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/MessageACKEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageACKEvent.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 11/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MessageACKEvt: Codable, GatewayData { 11 | public let message_id: Snowflake 12 | public let channel_id: Snowflake 13 | public let version: Int 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/MessageDelete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageDelete.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MessageDelete: Codable, GatewayData { 11 | public let id: Snowflake 12 | public let channel_id: Snowflake 13 | public let guild_id: Snowflake? 14 | } 15 | 16 | public struct MessageDeleteBulk: Codable, GatewayData { 17 | public let id: [Snowflake] 18 | public let channel_id: Snowflake 19 | public let guild_id: Snowflake? 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ReadyEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadyEvt.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The ready event palyoad for user accounts 11 | public struct ReadyEvt: Decodable, GatewayData { 12 | // swiftlint:disable:next identifier_name 13 | public let v: Int 14 | public let user: CurrentUser 15 | public let users: [User] 16 | public let guilds: [DecodeThrowable] 17 | public let session_id: String 18 | public let user_settings: UserSettings? // Depreciated, no longer sent 19 | /// Protobuf of user settings 20 | public let user_settings_proto: String 21 | /// DMs for this user 22 | public let private_channels: [DecodeThrowable] 23 | 24 | public let merged_members: [[Member]] 25 | 26 | /// The user's unreads 27 | /// 28 | /// > An implementation for unreads is still WIP in Swiftcord 29 | public let read_state: ReadState 30 | 31 | public let auth_token: String? 32 | 33 | public let resume_gateway_url: URL 34 | } 35 | 36 | /// The ready event payload for bot accounts 37 | public struct BotReadyEvt: Decodable, GatewayData { 38 | // swiftlint:disable:next identifier_name 39 | public let v: Int 40 | public let user: User 41 | public let guilds: [GuildUnavailable] 42 | public let session_id: String 43 | public let shard: [Int]? // Included for inclusivity, will not be used 44 | public let application: PartialApplication 45 | public let resume_gateway_url: URL 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ReadySuppEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 5/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Payload sent with ``GatewayEvent/readySupplemental`` 11 | public struct ReadySuppEvt: Decodable, GatewayData { 12 | public let merged_presences: MergedPresences 13 | } 14 | 15 | public struct MergedPresences: GatewayData { 16 | public let guilds: [[Presence]] 17 | public let friends: [Presence] 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ThreadListSync.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadListSync.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ThreadListSync: Codable, GatewayData { 11 | public let guild_id: Snowflake 12 | public let channel_ids: [Snowflake]? 13 | public let threads: [Channel] 14 | public let members: [ThreadMember] 15 | } 16 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/ThreadMembersUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadMembersUpdate.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 21/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ThreadMembersUpdate: Codable, GatewayData { 11 | public let id: Snowflake 12 | public let guild_id: Snowflake 13 | public let member_count: Int // The approximate number of members in the thread, capped at 50 14 | public let added_members: [ThreadMember]? 15 | public let removed_member_ids: [Snowflake]? 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/TypingStart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingStart.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 12/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TypingStart: Codable, GatewayData { 11 | public let user_id: Snowflake 12 | public let channel_id: Snowflake 13 | public let guild_id: Snowflake? 14 | public let timestamp: Int 15 | public let member: Member? 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Event/TypingStartEvt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingStartEvt.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent on 4/15/22. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/Gateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gateway.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 20/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /* 11 | but enough for what this app needs to do. 12 | */ 13 | 14 | public enum GatewayCloseCode: Int { 15 | case unknown = 4000 16 | case unknownOpcode = 4001 17 | case decodeErr = 4002 18 | case notAuthenthicated = 4003 19 | case authenthicationFail = 4004 20 | case alreadyAuthenthicated = 4005 21 | case invalidSeq = 4007 22 | case rateLimited = 4008 23 | case timedOut = 4009 24 | case invalidVersion = 4012 25 | case invalidIntent = 4013 26 | case disallowedIntent = 4014 27 | } 28 | 29 | // MARK: - Gateway Opcode enums 30 | public enum GatewayOutgoingOpcodes: Int, Codable { 31 | case heartbeat = 1 32 | case identify = 2 33 | case presenceUpdate = 3 34 | case voiceStateUpdate = 4 35 | case resume = 6 // Attempt to resume disconnected session 36 | case requestGuildMembers = 8 37 | case subscribeGuildEvents = 14 38 | case updateGuildSubscriptions = 37 39 | } 40 | 41 | public enum GatewayIncomingOpcodes: Int, Codable { 42 | case dispatchEvent = 0 // Event dispatched 43 | case heartbeat = 1 44 | case reconnect = 7 // Server is closing connection, should disconnect and resume 45 | case invalidSession = 9 46 | case hello = 10 47 | case heartbeatAck = 11 48 | } 49 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/UserAccount/ReadState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadState.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 16/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ReadState: Codable { 11 | /// A read state entry for a channel 12 | public struct Entry: Codable { 13 | public init( 14 | id: Snowflake, 15 | lastMessageID: Snowflake? = nil, lastPinTimestamp: Date? = nil, 16 | mention_count: Int? = nil 17 | ) { 18 | self.id = id 19 | if let lastMessageID { 20 | self.last_message_id = .string(lastMessageID) 21 | } else { 22 | self.last_message_id = nil 23 | } 24 | self.last_pin_timestamp = lastPinTimestamp 25 | self.mention_count = mention_count 26 | } 27 | 28 | public let id: Snowflake 29 | public let last_message_id: HybridSnowflake? 30 | public let last_pin_timestamp: Date? 31 | public let mention_count: Int? 32 | 33 | public func updatingLastMessage(id messageID: Snowflake) -> Self { 34 | .init( 35 | id: id, 36 | lastMessageID: messageID, 37 | lastPinTimestamp: last_pin_timestamp, 38 | mention_count: mention_count 39 | ) 40 | } 41 | } 42 | 43 | /// Read state entries 44 | public let entries: [Entry] 45 | 46 | /// If this read state update is partial 47 | public let partial: Bool 48 | 49 | /// Version of this read state, will be incremented for major updates 50 | public let version: Int 51 | } 52 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/Gateway/UserSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserSettings.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 10/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum UITheme: String, Codable { 11 | case dark 12 | case light 13 | } 14 | 15 | public struct UserSettings: Decodable, GatewayData, Equatable { 16 | /// Sequence of guild IDs 17 | /// 18 | /// The IDs of ordered guilds are in this array. If the guild 19 | /// is not ordered (i.e. never dragged from its initial position at 20 | /// the top of the server list), its id will not be in this array. 21 | public let guild_positions: [Snowflake]? 22 | 23 | /// Guild folders 24 | public let guild_folders: [GuildFolderItem]? 25 | 26 | /// If the new inline attachment upload experience is enabled 27 | public let inline_attachment_media: Bool? 28 | 29 | /// User interface locale 30 | public let locale: Locale? 31 | 32 | /// Client UI theme 33 | /// 34 | /// Although there's a sync with system setting in the official client, 35 | /// only light/dark themes are saved in the user settings. 36 | public let theme: UITheme? 37 | 38 | /// User timezone, in minutes 39 | public let timezone_offset: Int? 40 | 41 | /// If developer mode is enabled (mainly enables copying of IDs) 42 | public let developer_mode: Bool? 43 | 44 | /// If compact message view is enabled 45 | public let message_display_compact: Bool? 46 | } 47 | 48 | extension UserSettings { 49 | public func merged(with settings: UserSettings) -> UserSettings { 50 | return UserSettings( 51 | guild_positions: settings.guild_positions ?? guild_positions, 52 | guild_folders: settings.guild_folders ?? guild_folders, 53 | inline_attachment_media: settings.inline_attachment_media ?? inline_attachment_media, 54 | locale: settings.locale ?? locale, 55 | theme: settings.theme ?? theme, 56 | timezone_offset: settings.timezone_offset ?? timezone_offset, 57 | developer_mode: settings.developer_mode ?? developer_mode, 58 | message_display_compact: settings.message_display_compact ?? message_display_compact 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/README.md: -------------------------------------------------------------------------------- 1 | # API Objects 🧱 2 | 3 | Welcome, to the folder of endless structs and enums! 4 | Here, a struct for almost every Discord object that can be 5 | sent to or received from the API exists, with enums for the 6 | public struct properties that are fitting to be turned into an enum. 7 | 8 | Changing any struct or enum can have a snowball effect since 9 | each struct is usually referenced by 5 others. 10 | 11 | Most of the files in here are close to or longer than 100 12 | lines, so take care! 13 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/REST/LogOut.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogOut.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 15/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct LogOut: Codable { 11 | let provider: String? 12 | let voip_provider: String? 13 | 14 | func encode(to encoder: Encoder) throws { 15 | var container = encoder.container(keyedBy: CodingKeys.self) 16 | // Encoding containers directly so nil optionals get encoded as "null" and not just removed 17 | try container.encode(provider, forKey: .provider) 18 | try container.encode(voip_provider, forKey: .voip_provider) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/REST/MessageReadAck.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageReadAck.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 28/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MessageReadAck: Codable { 11 | public let token: String? 12 | } 13 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/REST/NewMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewMessage.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 25/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct NewAttachment: Codable { 11 | public let id: String // Will not be a valid snowflake for new attachments 12 | public let filename: String 13 | 14 | public init(id: String, filename: String) { 15 | self.id = id 16 | self.filename = filename 17 | } 18 | } 19 | 20 | public struct NewMessage: Encodable { 21 | public let content: String? 22 | public let tts: Bool? 23 | public let embeds: [Embed]? 24 | public let allowed_mentions: AllowedMentions? 25 | public let message_reference: MessageReference? 26 | public let components: [Component]? 27 | public let sticker_ids: [Snowflake]? 28 | public let attachments: [NewAttachment]? 29 | public let nonce: Nonce? 30 | // file[n] // Handle file uploading later 31 | // attachments 32 | // let payload_json: Codable? // Handle this later 33 | public let flags: Int? 34 | 35 | public init(content: String?, nonce: Nonce = .init(), tts: Bool? = false, embeds: [Embed]? = nil, allowed_mentions: AllowedMentions? = nil, message_reference: MessageReference? = nil, components: [Component]? = nil, sticker_ids: [Snowflake]? = nil, attachments: [NewAttachment]? = nil, flags: Int? = nil) { 36 | self.content = content 37 | self.tts = tts 38 | self.embeds = embeds 39 | self.allowed_mentions = allowed_mentions 40 | self.message_reference = message_reference 41 | self.components = components 42 | self.sticker_ids = sticker_ids 43 | self.attachments = attachments 44 | self.flags = flags 45 | self.nonce = nonce 46 | } 47 | 48 | enum CodingKeys: CodingKey { 49 | case content 50 | case tts 51 | case embeds 52 | case allowed_mentions 53 | case message_reference 54 | case components 55 | case sticker_ids 56 | case attachments 57 | case flags 58 | case nonce 59 | } 60 | 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.container(keyedBy: CodingKeys.self) 63 | 64 | try container.encodeIfPresent(content, forKey: .content) 65 | try container.encodeIfPresent(tts, forKey: .tts) 66 | try container.encodeIfPresent(embeds, forKey: .embeds) 67 | try container.encodeIfPresent(allowed_mentions, forKey: .allowed_mentions) 68 | try container.encodeIfPresent(message_reference, forKey: .message_reference) 69 | try container.encodeIfPresent(sticker_ids, forKey: .sticker_ids) 70 | try container.encodeIfPresent(attachments, forKey: .attachments) 71 | try container.encodeIfPresent(flags, forKey: .flags) 72 | try container.encodeIfPresent(nonce, forKey: .nonce) 73 | 74 | // Same workaround to encode array of protocols 75 | if let components = components { 76 | var componentContainer = container.nestedUnkeyedContainer(forKey: .components) 77 | for component in components { 78 | try componentContainer.encode(component) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Objects/REST/ResolvedInvite.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResolvedInvite.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 10/7/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum InviteTargetType: Int, Codable { 11 | case stream = 1 12 | case embeddedApplication = 2 13 | } 14 | 15 | /// Invite 16 | /// 17 | /// Represents a code that when used, adds a user to a guild or group DM channel. 18 | public struct Invite: Decodable { 19 | /// The invite code (unique ID) 20 | public let code: String 21 | // The guild this invite is for 22 | // public let guild: Guild? 23 | /// The channel this invite is for 24 | public let channel: Channel? 25 | /// The type of target for this voice channel invite 26 | public let target_type: InviteTargetType? 27 | /// The user whose stream to display for this voice channel stream invite 28 | public let target_user: User? 29 | /// The user who created the invite 30 | public let inviter: User? 31 | /// Approximate count of online members 32 | /// 33 | /// > Returned from ``DiscordREST/resolveInvite(inviteID:inputValue:withCounts:withExpiration:)`` 34 | /// (`GET /invites/{inviteID}` endpoint) when `with_counts` is true 35 | public let approximate_member_count: Int? 36 | /// Approximate count of total members 37 | /// 38 | /// > Returned from ``DiscordREST/resolveInvite(inviteID:inputValue:withCounts:withExpiration:)``) 39 | /// > (`GET /invites/{inviteID}` endpoint) when `with_counts` is true 40 | public let approximate_presence_count: Int? 41 | /// The embedded application to open for this voice channel embedded application invite 42 | public let target_application: PartialApplication? 43 | /// The expiration date of this invite 44 | /// 45 | /// > Returned from ``DiscordREST/resolveInvite(inviteID:inputValue:withCounts:withExpiration:)`` 46 | /// > (`GET /invites/{inviteID}` endpoint) when `with_expiration` is true 47 | public let expires_at: Date? 48 | /// Guild scheduled event data 49 | /// 50 | /// > Only included if `guild_scheduled_event_id` contains a valid guild scheduled event id 51 | public let guild_scheduled_event: GuildScheduledEvent? 52 | } 53 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIAchievements.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Achievements 7 | /// 8 | /// > GET: `/applications/{application.id}/achievements` 9 | func getAchievements( 10 | _ applicationId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "applications/\(applicationId)/achievements" 14 | ) 15 | } 16 | /// Get Achievement 17 | /// 18 | /// > GET: `/applications/{application.id}/achievements/{achievement.id}` 19 | func getAchievement( 20 | _ applicationId: Snowflake, 21 | _ achievementId: Snowflake 22 | ) async throws -> T { 23 | return try await getReq( 24 | path: "applications/\(applicationId)/achievements/\(achievementId)" 25 | ) 26 | } 27 | /// Create Achievement 28 | /// 29 | /// > POST: `/applications/{application.id}/achievements` 30 | func createAchievement( 31 | _ applicationId: Snowflake, 32 | _ body: B 33 | ) async throws -> T { 34 | return try await postReq( 35 | path: "applications/\(applicationId)/achievements", 36 | body: body 37 | ) 38 | } 39 | /// Update Achievement 40 | /// 41 | /// > PATCH: `/applications/{application.id}/achievements/{achievement.id}` 42 | func updateAchievement( 43 | _ applicationId: Snowflake, 44 | _ achievementId: Snowflake, 45 | _ body: B 46 | ) async throws { 47 | try await patchReq( 48 | path: "applications/\(applicationId)/achievements/\(achievementId)", 49 | body: body 50 | ) 51 | } 52 | /// Delete Achievement 53 | /// 54 | /// > DELETE: `/applications/{application.id}/achievements/{achievement.id}` 55 | func deleteAchievement( 56 | _ applicationId: Snowflake, 57 | _ achievementId: Snowflake 58 | ) async throws { 59 | try await deleteReq( 60 | path: "applications/\(applicationId)/achievements/\(achievementId)" 61 | ) 62 | } 63 | /// Update User Achievement 64 | /// 65 | /// > PUT: `/users/{user.id}/applications/{application.id}/achievements/{achievement.id}` 66 | func updateUserAchievement( 67 | _ userId: Snowflake, 68 | _ applicationId: Snowflake, 69 | _ achievementId: Snowflake, 70 | _ body: B 71 | ) async throws -> T { 72 | return try await putReq( 73 | path: "users/\(userId)/applications/\(applicationId)/achievements/\(achievementId)", 74 | body: body 75 | ) 76 | } 77 | /// Get User Achievements 78 | /// 79 | /// > GET: `/users/@me/applications/{application.id}/achievements` 80 | func getUserAchievements( 81 | _ applicationId: Snowflake 82 | ) async throws -> T { 83 | return try await getReq( 84 | path: "users/@me/applications/\(applicationId)/achievements" 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIApplicationRoleConnectionMetadata.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Application Role Connection Metadata Records 7 | /// 8 | /// > GET: `/applications/{application.id}/role-connections/metadata` 9 | func getApplicationRoleConnectionMetadataRecords( 10 | _ applicationId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "applications/\(applicationId)/role-connections/metadata" 14 | ) 15 | } 16 | /// Update Application Role Connection Metadata Records 17 | /// 18 | /// > PUT: `/applications/{application.id}/role-connections/metadata` 19 | func updateApplicationRoleConnectionMetadataRecords( 20 | _ applicationId: Snowflake, 21 | _ body: B 22 | ) async throws -> T { 23 | return try await putReq( 24 | path: "applications/\(applicationId)/role-connections/metadata", 25 | body: body 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIAuditLog.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Guild Audit Log 7 | /// 8 | /// > GET: `/guilds/{guild.id}/audit-logs` 9 | func getGuildAuditLog( 10 | _ guildId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "guilds/\(guildId)/audit-logs" 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIAutoModeration.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// List Auto Moderation Rules for Guild 7 | /// 8 | /// > GET: `/guilds/{guild.id}/auto-moderation/rules` 9 | func listAutoModerationRulesforGuild( 10 | _ guildId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "guilds/\(guildId)/auto-moderation/rules" 14 | ) 15 | } 16 | /// Get Auto Moderation Rule 17 | /// 18 | /// > GET: `/guilds/{guild.id}/auto-moderation/rules/{auto_moderation_rule.id}` 19 | func getAutoModerationRule( 20 | _ guildId: Snowflake, 21 | _ auto_moderation_ruleId: Snowflake 22 | ) async throws -> T { 23 | return try await getReq( 24 | path: "guilds/\(guildId)/auto-moderation/rules/\(auto_moderation_ruleId)" 25 | ) 26 | } 27 | /// Create Auto Moderation Rule 28 | /// 29 | /// > POST: `/guilds/{guild.id}/auto-moderation/rules` 30 | func createAutoModerationRule( 31 | _ guildId: Snowflake, 32 | _ body: B 33 | ) async throws -> T { 34 | return try await postReq( 35 | path: "guilds/\(guildId)/auto-moderation/rules", 36 | body: body 37 | ) 38 | } 39 | /// Edit Auto Moderation Rule 40 | /// 41 | /// > PATCH: `/guilds/{guild.id}/auto-moderation/rules/{auto_moderation_rule.id}` 42 | func editAutoModerationRule( 43 | _ guildId: Snowflake, 44 | _ auto_moderation_ruleId: Snowflake, 45 | _ body: B 46 | ) async throws { 47 | try await patchReq( 48 | path: "guilds/\(guildId)/auto-moderation/rules/\(auto_moderation_ruleId)", 49 | body: body 50 | ) 51 | } 52 | /// Delete Auto Moderation Rule 53 | /// 54 | /// > DELETE: `/guilds/{guild.id}/auto-moderation/rules/{auto_moderation_rule.id}` 55 | func deleteAutoModerationRule( 56 | _ guildId: Snowflake, 57 | _ auto_moderation_ruleId: Snowflake 58 | ) async throws { 59 | try await deleteReq( 60 | path: "guilds/\(guildId)/auto-moderation/rules/\(auto_moderation_ruleId)" 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APICurrentUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICurrentUser.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 7/3/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// API endpoints for everything related to the current user only 11 | /// Most (all) endpoints here aren't documented and were found 12 | /// from reverse engineering, observation and speculation. 13 | public extension DiscordREST { 14 | // MARK: Get Current User DMs 15 | // GET /users/@me/channels 16 | func getDMs() async throws -> [DecodeThrowable] { 17 | return try await getReq(path: "users/@me/channels") 18 | } 19 | 20 | // MARK: Change Current User Password 21 | // PATCH /users/@me 22 | // Fields: new_password, password 23 | // Returns: User 24 | func changeCurUserPW( 25 | oldPW: String, 26 | newPW: String 27 | ) async -> User? { 28 | // Patch isn't implemented yet 29 | 30 | return nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIEmoji.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// List Guild Emojis 7 | /// 8 | /// > GET: `/guilds/{guild.id}/emojis` 9 | func listGuildEmojis( 10 | _ guildId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "guilds/\(guildId)/emojis" 14 | ) 15 | } 16 | /// Get Guild Emoji 17 | /// 18 | /// > GET: `/guilds/{guild.id}/emojis/{emoji.id}` 19 | func getGuildEmoji( 20 | _ guildId: Snowflake, 21 | _ emojiId: Snowflake 22 | ) async throws -> T { 23 | return try await getReq( 24 | path: "guilds/\(guildId)/emojis/\(emojiId)" 25 | ) 26 | } 27 | /// Create Guild Emoji 28 | /// 29 | /// > POST: `/guilds/{guild.id}/emojis` 30 | func createGuildEmoji( 31 | _ guildId: Snowflake, 32 | _ body: B 33 | ) async throws -> T { 34 | return try await postReq( 35 | path: "guilds/\(guildId)/emojis", 36 | body: body 37 | ) 38 | } 39 | /// Edit Guild Emoji 40 | /// 41 | /// > PATCH: `/guilds/{guild.id}/emojis/{emoji.id}` 42 | func editGuildEmoji( 43 | _ guildId: Snowflake, 44 | _ emojiId: Snowflake, 45 | _ body: B 46 | ) async throws { 47 | try await patchReq( 48 | path: "guilds/\(guildId)/emojis/\(emojiId)", 49 | body: body 50 | ) 51 | } 52 | /// Delete Guild Emoji 53 | /// 54 | /// > DELETE: `/guilds/{guild.id}/emojis/{emoji.id}` 55 | func deleteGuildEmoji( 56 | _ guildId: Snowflake, 57 | _ emojiId: Snowflake 58 | ) async throws { 59 | try await deleteReq( 60 | path: "guilds/\(guildId)/emojis/\(emojiId)" 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIGateway.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Gateway 7 | /// 8 | /// > GET: `/gateway` 9 | func getGateway() async throws -> T { 10 | return try await getReq( 11 | path: "gateway" 12 | ) 13 | } 14 | /// Get Gateway Bot 15 | /// 16 | /// > GET: `/gateway/bot` 17 | func getGatewayBot() async throws -> T { 18 | return try await getReq( 19 | path: "gateway/bot" 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIGuildScheduledEvent.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// List Scheduled Events for Guild 7 | /// 8 | /// > GET: `/guilds/{guild.id}/scheduled-events` 9 | func listScheduledEventsforGuild( 10 | _ guildId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "guilds/\(guildId)/scheduled-events" 14 | ) 15 | } 16 | /// Create Guild Scheduled Event 17 | /// 18 | /// > POST: `/guilds/{guild.id}/scheduled-events` 19 | func createGuildScheduledEvent( 20 | _ guildId: Snowflake, 21 | _ body: B 22 | ) async throws -> T { 23 | return try await postReq( 24 | path: "guilds/\(guildId)/scheduled-events", 25 | body: body 26 | ) 27 | } 28 | /// Get Guild Scheduled Event 29 | /// 30 | /// > GET: `/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}` 31 | func getGuildScheduledEvent( 32 | _ guildId: Snowflake, 33 | _ guild_scheduled_eventId: Snowflake 34 | ) async throws -> T { 35 | return try await getReq( 36 | path: "guilds/\(guildId)/scheduled-events/\(guild_scheduled_eventId)" 37 | ) 38 | } 39 | /// Edit Guild Scheduled Event 40 | /// 41 | /// > PATCH: `/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}` 42 | func editGuildScheduledEvent( 43 | _ guildId: Snowflake, 44 | _ guild_scheduled_eventId: Snowflake, 45 | _ body: B 46 | ) async throws { 47 | try await patchReq( 48 | path: "guilds/\(guildId)/scheduled-events/\(guild_scheduled_eventId)", 49 | body: body 50 | ) 51 | } 52 | /// Delete Guild Scheduled Event 53 | /// 54 | /// > DELETE: `/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}` 55 | func deleteGuildScheduledEvent( 56 | _ guildId: Snowflake, 57 | _ guild_scheduled_eventId: Snowflake 58 | ) async throws { 59 | try await deleteReq( 60 | path: "guilds/\(guildId)/scheduled-events/\(guild_scheduled_eventId)" 61 | ) 62 | } 63 | /// Get Guild Scheduled Event Users 64 | /// 65 | /// > GET: `/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}/users` 66 | func getGuildScheduledEventUsers( 67 | _ guildId: Snowflake, 68 | _ guild_scheduled_eventId: Snowflake 69 | ) async throws -> T { 70 | return try await getReq( 71 | path: "guilds/\(guildId)/scheduled-events/\(guild_scheduled_eventId)/users" 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIGuildTemplate.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Guild Template 7 | /// 8 | /// > GET: `/guilds/templates/{template.code}` 9 | func getGuildTemplate( 10 | _ templateCode: String 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "guilds/templates/\(templateCode)" 14 | ) 15 | } 16 | /// Create Guild from Guild Template 17 | /// 18 | /// > POST: `/guilds/templates/{template.code}` 19 | func createGuildfromGuildTemplate( 20 | _ templateCode: String, 21 | _ body: B 22 | ) async throws -> T { 23 | return try await postReq( 24 | path: "guilds/templates/\(templateCode)", 25 | body: body 26 | ) 27 | } 28 | /// Get Guild Templates 29 | /// 30 | /// > GET: `/guilds/{guild.id}/templates` 31 | func getGuildTemplates( 32 | _ guildId: Snowflake 33 | ) async throws -> T { 34 | return try await getReq( 35 | path: "guilds/\(guildId)/templates" 36 | ) 37 | } 38 | /// Create Guild Template 39 | /// 40 | /// > POST: `/guilds/{guild.id}/templates` 41 | func createGuildTemplate( 42 | _ guildId: Snowflake, 43 | _ body: B 44 | ) async throws -> T { 45 | return try await postReq( 46 | path: "guilds/\(guildId)/templates", 47 | body: body 48 | ) 49 | } 50 | /// Sync Guild Template 51 | /// 52 | /// > PUT: `/guilds/{guild.id}/templates/{template.code}` 53 | func syncGuildTemplate( 54 | _ guildId: Snowflake, 55 | _ templateCode: String, 56 | _ body: B 57 | ) async throws -> T { 58 | return try await putReq( 59 | path: "guilds/\(guildId)/templates/\(templateCode)", 60 | body: body 61 | ) 62 | } 63 | /// Edit Guild Template 64 | /// 65 | /// > PATCH: `/guilds/{guild.id}/templates/{template.code}` 66 | func editGuildTemplate( 67 | _ guildId: Snowflake, 68 | _ templateCode: String, 69 | _ body: B 70 | ) async throws { 71 | try await patchReq( 72 | path: "guilds/\(guildId)/templates/\(templateCode)", 73 | body: body 74 | ) 75 | } 76 | /// Delete Guild Template 77 | /// 78 | /// > DELETE: `/guilds/{guild.id}/templates/{template.code}` 79 | func deleteGuildTemplate( 80 | _ guildId: Snowflake, 81 | _ templateCode: String 82 | ) async throws { 83 | try await deleteReq( 84 | path: "guilds/\(guildId)/templates/\(templateCode)" 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIInvite.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Current User Guild Member 7 | /// 8 | /// `GET /invites/{inviteID}` 9 | /// 10 | /// Get guild member object for current user in a guild 11 | /// 12 | /// > Example URL: 13 | /// > 14 | /// > `https://canary.discord.com/api/v9/invites/dosjopkqwef?inputValue=dosjopkqwef&with_counts=true&with_expiration=true` 15 | func resolveInvite( 16 | inviteID: String, 17 | inputValue: String, 18 | withCounts: Bool = true, 19 | withExpiration: Bool = true 20 | ) async throws -> Invite { 21 | return try await getReq(path: "invites/\(inviteID)", query: [ 22 | URLQueryItem(name: "with_counts", value: String(withCounts)), 23 | URLQueryItem(name: "with_expiration", value: String(withExpiration)) 24 | ]) 25 | } 26 | /// Get Invite 27 | /// 28 | /// > GET: `/invites/{invite.code}` 29 | func getInvite( 30 | _ inviteCode: String 31 | ) async throws -> T { 32 | return try await getReq( 33 | path: "invites/\(inviteCode)" 34 | ) 35 | } 36 | /// Delete Invite 37 | /// 38 | /// > DELETE: `/invites/{invite.code}` 39 | func deleteInvite( 40 | _ inviteCode: String 41 | ) async throws { 42 | try await deleteReq( 43 | path: "invites/\(inviteCode)" 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APILobbies.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Create Lobby 7 | /// 8 | /// > POST: `/lobbies` 9 | func createLobby(_ body: B) async throws -> T { 10 | return try await postReq( 11 | path: "lobbies", 12 | body: body 13 | ) 14 | } 15 | /// Update Lobby 16 | /// 17 | /// > PATCH: `/lobbies/{lobby.id}` 18 | func updateLobby( 19 | _ lobbyId: Snowflake, 20 | _ body: B 21 | ) async throws { 22 | try await patchReq( 23 | path: "lobbies/\(lobbyId)", 24 | body: body 25 | ) 26 | } 27 | /// Delete Lobby 28 | /// 29 | /// > DELETE: `/lobbies/{lobby.id}` 30 | func deleteLobby( 31 | _ lobbyId: Snowflake 32 | ) async throws { 33 | try await deleteReq( 34 | path: "lobbies/\(lobbyId)" 35 | ) 36 | } 37 | /// Update Lobby Member 38 | /// 39 | /// > PATCH: `/lobbies/{lobby.id}/members/{user.id}` 40 | func updateLobbyMember( 41 | _ lobbyId: Snowflake, 42 | _ userId: Snowflake, 43 | _ body: B 44 | ) async throws { 45 | try await patchReq( 46 | path: "lobbies/\(lobbyId)/members/\(userId)", 47 | body: body 48 | ) 49 | } 50 | /// Create Lobby Search 51 | /// 52 | /// > POST: `/lobbies/search` 53 | func createLobbySearch(_ body: B) async throws -> T { 54 | return try await postReq( 55 | path: "lobbies/search", 56 | body: body 57 | ) 58 | } 59 | /// Send Lobby Data 60 | /// 61 | /// > POST: `/lobbies/{lobby.id}/send` 62 | func sendLobbyData( 63 | _ lobbyId: Snowflake, 64 | _ body: B 65 | ) async throws -> T { 66 | return try await postReq( 67 | path: "lobbies/\(lobbyId)/send", 68 | body: body 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIMultipartFormBody.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIMultipartFormBody.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 14/5/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension DiscordREST { 11 | static func createMultipartBody( 12 | with payloadJson: Data?, 13 | boundary: String, 14 | attachments: [URL] 15 | ) -> Data { 16 | var body = Data() 17 | 18 | for (num, attachment) in attachments.enumerated() { 19 | guard let name = try? attachment.resourceValues(forKeys: [URLResourceKey.nameKey]).name else { 20 | continue 21 | } 22 | guard let attachmentData = try? Data(contentsOf: attachment) else { 23 | DiscordREST.log.error("Could not get data of attachment #\(num)") 24 | continue 25 | } 26 | 27 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 28 | body.append( 29 | "Content-Disposition: form-data; name=\"files[\(num)]\"; filename=\"\(name)\"\r\n".data(using: .utf8)! 30 | ) 31 | body.append("Content-Type: \(attachment.mimeType)\r\n\r\n".data(using: .utf8)!) 32 | body.append(attachmentData) 33 | body.append("\r\n".data(using: .utf8)!) 34 | } 35 | 36 | if let payloadJson = payloadJson { 37 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 38 | body.append("Content-Disposition: form-data; name=\"payload_json\"\r\nContent-Type: application/json\r\n\r\n".data(using: .utf8)!) 39 | body.append(payloadJson) 40 | body.append("\r\n".data(using: .utf8)!) 41 | } 42 | 43 | body.append("--\(boundary)--\r\n".data(using: .utf8)!) 44 | return body 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIOAuth2.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Current Bot Application Information 7 | /// 8 | /// > GET: `/oauth2/applications/@me` 9 | func getCurrentBotApplicationInformation() async throws -> T { 10 | return try await getReq( 11 | path: "oauth2/applications/@me" 12 | ) 13 | } 14 | /// Get Current Authorization Information 15 | /// 16 | /// > GET: `/oauth2/@me` 17 | func getCurrentAuthorizationInformation() async throws -> T { 18 | return try await getReq( 19 | path: "oauth2/@me" 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIReceivingandResponding.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Create Interaction Response 7 | /// 8 | /// > POST: `/interactions/{interaction.id}/{interaction.token}/callback` 9 | func createInteractionResponse( 10 | _ interactionId: Snowflake, 11 | _ interactionToken: String, 12 | _ body: B 13 | ) async throws -> T { 14 | return try await postReq( 15 | path: "interactions/\(interactionId)/\(interactionToken)/callback", 16 | body: body 17 | ) 18 | } 19 | /// Get Original Interaction Response 20 | /// 21 | /// > GET: `/webhooks/{application.id}/{interaction.token}/messages/@original` 22 | func getOriginalInteractionResponse( 23 | _ applicationId: Snowflake, 24 | _ interactionToken: String 25 | ) async throws -> T { 26 | return try await getReq( 27 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/@original" 28 | ) 29 | } 30 | /// Edit Original Interaction Response 31 | /// 32 | /// > PATCH: `/webhooks/{application.id}/{interaction.token}/messages/@original` 33 | func editOriginalInteractionResponse( 34 | _ applicationId: Snowflake, 35 | _ interactionToken: String, 36 | _ body: B 37 | ) async throws { 38 | try await patchReq( 39 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/@original", 40 | body: body 41 | ) 42 | } 43 | /// Delete Original Interaction Response 44 | /// 45 | /// > DELETE: `/webhooks/{application.id}/{interaction.token}/messages/@original` 46 | func deleteOriginalInteractionResponse( 47 | _ applicationId: Snowflake, 48 | _ interactionToken: String 49 | ) async throws { 50 | try await deleteReq( 51 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/@original" 52 | ) 53 | } 54 | /// Create Followup Message 55 | /// 56 | /// > POST: `/webhooks/{application.id}/{interaction.token}` 57 | func createFollowupMessage( 58 | _ applicationId: Snowflake, 59 | _ interactionToken: String, 60 | _ body: B 61 | ) async throws -> T { 62 | return try await postReq( 63 | path: "webhooks/\(applicationId)/\(interactionToken)", 64 | body: body 65 | ) 66 | } 67 | /// Get Followup Message 68 | /// 69 | /// > GET: `/webhooks/{application.id}/{interaction.token}/messages/{message.id}` 70 | func getFollowupMessage( 71 | _ applicationId: Snowflake, 72 | _ interactionToken: String, 73 | _ messageId: Snowflake 74 | ) async throws -> T { 75 | return try await getReq( 76 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/\(messageId)" 77 | ) 78 | } 79 | /// Edit Followup Message 80 | /// 81 | /// > PATCH: `/webhooks/{application.id}/{interaction.token}/messages/{message.id}` 82 | func editFollowupMessage( 83 | _ applicationId: Snowflake, 84 | _ interactionToken: String, 85 | _ messageId: Snowflake, 86 | _ body: B 87 | ) async throws { 88 | try await patchReq( 89 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/\(messageId)", 90 | body: body 91 | ) 92 | } 93 | /// Delete Followup Message 94 | /// 95 | /// > DELETE: `/webhooks/{application.id}/{interaction.token}/messages/{message.id}` 96 | func deleteFollowupMessage( 97 | _ applicationId: Snowflake, 98 | _ interactionToken: String, 99 | _ messageId: Snowflake 100 | ) async throws { 101 | try await deleteReq( 102 | path: "webhooks/\(applicationId)/\(interactionToken)/messages/\(messageId)" 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIStageInstance.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Create Stage Instance 7 | /// 8 | /// > POST: `/stage-instances` 9 | func createStageInstance(_ body: B) async throws -> T { 10 | return try await postReq( 11 | path: "stage-instances", 12 | body: body 13 | ) 14 | } 15 | /// Get Stage Instance 16 | /// 17 | /// > GET: `/stage-instances/{channel.id}` 18 | func getStageInstance( 19 | _ channelId: Snowflake 20 | ) async throws -> T { 21 | return try await getReq( 22 | path: "stage-instances/\(channelId)" 23 | ) 24 | } 25 | /// Edit Stage Instance 26 | /// 27 | /// > PATCH: `/stage-instances/{channel.id}` 28 | func editStageInstance( 29 | _ channelId: Snowflake, 30 | _ body: B 31 | ) async throws { 32 | try await patchReq( 33 | path: "stage-instances/\(channelId)", 34 | body: body 35 | ) 36 | } 37 | /// Delete Stage Instance 38 | /// 39 | /// > DELETE: `/stage-instances/{channel.id}` 40 | func deleteStageInstance( 41 | _ channelId: Snowflake 42 | ) async throws { 43 | try await deleteReq( 44 | path: "stage-instances/\(channelId)" 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APISticker.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | private struct StickerPacks: Codable, GatewayData { 6 | public let sticker_packs: [StickerPack] 7 | } 8 | 9 | public extension DiscordREST { 10 | /// Get Sticker 11 | /// 12 | /// > GET: `/stickers/{sticker.id}` 13 | func getSticker( 14 | _ stickerId: Snowflake 15 | ) async throws -> T { 16 | return try await getReq( 17 | path: "stickers/\(stickerId)" 18 | ) 19 | } 20 | /// List Nitro Sticker Packs 21 | /// 22 | /// > GET: `/sticker-packs` 23 | func listNitroStickerPacks() async throws -> [StickerPack] { 24 | let stickerPacks: StickerPacks = try await getReq( 25 | path: "sticker-packs" 26 | ) 27 | return stickerPacks.sticker_packs 28 | } 29 | /// List Guild Stickers 30 | /// 31 | /// > GET: `/guilds/{guild.id}/stickers` 32 | func listGuildStickers( 33 | _ guildId: Snowflake 34 | ) async throws -> T { 35 | return try await getReq( 36 | path: "guilds/\(guildId)/stickers" 37 | ) 38 | } 39 | /// Get Guild Sticker 40 | /// 41 | /// > GET: `/guilds/{guild.id}/stickers/{sticker.id}` 42 | func getGuildSticker( 43 | _ guildId: Snowflake, 44 | _ stickerId: Snowflake 45 | ) async throws -> T { 46 | return try await getReq( 47 | path: "guilds/\(guildId)/stickers/\(stickerId)" 48 | ) 49 | } 50 | /// Create Guild Sticker 51 | /// 52 | /// > POST: `/guilds/{guild.id}/stickers` 53 | func createGuildSticker( 54 | _ guildId: Snowflake, 55 | _ body: B 56 | ) async throws -> T { 57 | return try await postReq( 58 | path: "guilds/\(guildId)/stickers", 59 | body: body 60 | ) 61 | } 62 | /// Edit Guild Sticker 63 | /// 64 | /// > PATCH: `/guilds/{guild.id}/stickers/{sticker.id}` 65 | func editGuildSticker( 66 | _ guildId: Snowflake, 67 | _ stickerId: Snowflake, 68 | _ body: B 69 | ) async throws { 70 | try await patchReq( 71 | path: "guilds/\(guildId)/stickers/\(stickerId)", 72 | body: body 73 | ) 74 | } 75 | /// Delete Guild Sticker 76 | /// 77 | /// > DELETE: `/guilds/{guild.id}/stickers/{sticker.id}` 78 | func deleteGuildSticker( 79 | _ guildId: Snowflake, 80 | _ stickerId: Snowflake 81 | ) async throws { 82 | try await deleteReq( 83 | path: "guilds/\(guildId)/stickers/\(stickerId)" 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIStore.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Entitlements 7 | /// 8 | /// > GET: `/applications/{application.id}/entitlements` 9 | func getEntitlements( 10 | _ applicationId: Snowflake 11 | ) async throws -> T { 12 | return try await getReq( 13 | path: "applications/\(applicationId)/entitlements" 14 | ) 15 | } 16 | /// Get Entitlement 17 | /// 18 | /// > GET: `/applications/{application.id}/entitlements/{entitlement.id}` 19 | func getEntitlement( 20 | _ applicationId: Snowflake, 21 | _ entitlementId: Snowflake 22 | ) async throws -> T { 23 | return try await getReq( 24 | path: "applications/\(applicationId)/entitlements/\(entitlementId)" 25 | ) 26 | } 27 | /// Get SKUs 28 | /// 29 | /// > GET: `/applications/{application.id}/skus` 30 | func getSKUs( 31 | _ applicationId: Snowflake 32 | ) async throws -> T { 33 | return try await getReq( 34 | path: "applications/\(applicationId)/skus" 35 | ) 36 | } 37 | /// Consume SKU 38 | /// 39 | /// > POST: `/applications/{application.id}/entitlements/{entitlement.id}/consume` 40 | func consumeSKU( 41 | _ applicationId: Snowflake, 42 | _ entitlementId: Snowflake, 43 | _ body: B 44 | ) async throws -> T { 45 | return try await postReq( 46 | path: "applications/\(applicationId)/entitlements/\(entitlementId)/consume", 47 | body: body 48 | ) 49 | } 50 | /// Delete Test Entitlement 51 | /// 52 | /// > DELETE: `/applications/{application.id}/entitlements/{entitlement.id}` 53 | func deleteTestEntitlement( 54 | _ applicationId: Snowflake, 55 | _ entitlementId: Snowflake 56 | ) async throws { 57 | try await deleteReq( 58 | path: "applications/\(applicationId)/entitlements/\(entitlementId)" 59 | ) 60 | } 61 | /// Create Purchase Discount 62 | /// 63 | /// > PUT: `/store/skus/{sku.id}/discounts/{user.id}` 64 | func createPurchaseDiscount( 65 | _ skuId: Snowflake, 66 | _ userId: Snowflake, 67 | _ body: B 68 | ) async throws -> T { 69 | return try await putReq( 70 | path: "store/skus/\(skuId)/discounts/\(userId)", 71 | body: body 72 | ) 73 | } 74 | /// Delete Purchase Discount 75 | /// 76 | /// > DELETE: `/store/skus/{sku.id}/discounts/{user.id}` 77 | func deletePurchaseDiscount( 78 | _ skuId: Snowflake, 79 | _ userId: Snowflake 80 | ) async throws { 81 | try await deleteReq( 82 | path: "store/skus/\(skuId)/discounts/\(userId)" 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIUser.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// Get Current User 7 | /// 8 | /// `GET /users/@me` 9 | func getCurrentUser() async throws -> User { 10 | return try await getReq(path: "users/@me") 11 | } 12 | 13 | /// Get User 14 | /// 15 | /// `GET /users/{user.id}` 16 | /// 17 | /// - Parameter user: ID of user to retrieve 18 | func getUser(user: Snowflake) async throws -> User { 19 | return try await getReq(path: "users/\(user)") 20 | } 21 | 22 | /// Get Profile 23 | /// 24 | /// `GET /users/{user.id}` 25 | /// > Warning: This is an undocumented endpoint 26 | /// 27 | /// - Parameters: 28 | /// - user: ID of user to retrieve profile 29 | /// - mutualGuilds: If the user's mutual guilds with the current user should be returned as well 30 | /// - guildID: The ID of the guild the action that triggered profile retrival was carried out in. Pass `nil` 31 | /// if the action was carried out in a DM channel. 32 | func getProfile( 33 | user: Snowflake, 34 | mutualGuilds: Bool = false, 35 | guildID: Snowflake? = nil 36 | ) async throws -> UserProfile { 37 | var query = [URLQueryItem(name: "with_mutual_guilds", value: String(mutualGuilds))] 38 | if let guildID = guildID { 39 | query.append(URLQueryItem(name: "guild_id", value: guildID)) 40 | } 41 | return try await getReq(path: "users/\(user)/profile", query: query) 42 | } 43 | 44 | /// Modify Current User 45 | /// 46 | /// TODO: Patch not yet implemented 47 | 48 | /// Get Current User Guilds 49 | /// 50 | /// `GET /users/@me/guilds` 51 | func getGuilds( 52 | before: Snowflake? = nil, 53 | after: Snowflake? = nil, 54 | limit: Int = 200 55 | ) async throws -> [PartialGuild] { 56 | return try await getReq(path: "users/@me/guilds") 57 | } 58 | 59 | /// Get Current User Guild Member 60 | /// 61 | /// `GET /users/@me/guilds/{guild.id}/member` 62 | /// 63 | /// Get guild member object for current user in a guild 64 | func getGuildMember(guild: Snowflake) async throws -> Member { 65 | return try await getReq(path: "users/@me/guilds/\(guild)/member") 66 | } 67 | 68 | /// Leave Guild 69 | /// 70 | /// > DELETE: `/users/@me/guilds/{guild.id}` 71 | func leaveGuild(guild: Snowflake) async throws { 72 | return try await deleteReq(path: guild) 73 | } 74 | 75 | /// Log out 76 | /// 77 | /// `POST /auth/logout` 78 | /// > Warning: This is an undocumented endpoint 79 | /// 80 | /// - Parameters: 81 | /// - provider: Unknown, always observed to be nil 82 | /// - voipProvider: Unknown, always observed to be nil 83 | func logOut(provider: String? = nil, voipProvider: String? = nil) async throws { 84 | try await postReq(path: "auth/logout", body: LogOut(provider: provider, voip_provider: voipProvider)) 85 | setToken(token: nil) 86 | } 87 | /// Edit Current User 88 | /// 89 | /// > PATCH: `/users/@me` 90 | func editCurrentUser(_ body: B) async throws { 91 | try await patchReq( 92 | path: "users/@me", 93 | body: body 94 | ) 95 | } 96 | /// Create DM 97 | /// 98 | /// > POST: `/users/@me/channels` 99 | func createDM(_ body: B) async throws -> T { 100 | return try await postReq( 101 | path: "users/@me/channels", 102 | body: body 103 | ) 104 | } 105 | /// Create Group DM 106 | /// 107 | /// > POST: `/users/@me/channels` 108 | func createGroupDM(_ body: B) async throws -> T { 109 | return try await postReq( 110 | path: "users/@me/channels", 111 | body: body 112 | ) 113 | } 114 | /// Get User Connections 115 | /// 116 | /// > GET: `/users/@me/connections` 117 | func getUserConnections() async throws -> T { 118 | return try await getReq( 119 | path: "users/@me/connections" 120 | ) 121 | } 122 | /// Get User Application Role Connection 123 | /// 124 | /// > GET: `/users/@me/applications/{application.id}/role-connection` 125 | func getUserApplicationRoleConnection( 126 | _ applicationId: Snowflake 127 | ) async throws -> T { 128 | return try await getReq( 129 | path: "users/@me/applications/\(applicationId)/role-connection" 130 | ) 131 | } 132 | /// Update User Application Role Connection 133 | /// 134 | /// > PUT: `/users/@me/applications/{application.id}/role-connection` 135 | func updateUserApplicationRoleConnection( 136 | _ applicationId: Snowflake, 137 | _ body: B 138 | ) async throws -> T { 139 | return try await putReq( 140 | path: "users/@me/applications/\(applicationId)/role-connection", 141 | body: body 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/APIVoice.swift: -------------------------------------------------------------------------------- 1 | // NOTE: This file is auto-generated 2 | 3 | import Foundation 4 | 5 | public extension DiscordREST { 6 | /// List Voice Regions 7 | /// 8 | /// > GET: `/voice/regions` 9 | func listVoiceRegions() async throws -> T { 10 | return try await getReq( 11 | path: "voice/regions" 12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/REST/README.md: -------------------------------------------------------------------------------- 1 | # REST API 😴 2 | 3 | Here are the library files to get and carry out operations 4 | with Discord's HTTP REST API. 5 | 6 | ### File Structure 7 | 8 | A struct called DiscordAPI contains all functions currently 9 | implemented to interface with the API, make requests etc. 10 | Files prefixed with "API" contain extensions to this class 11 | for separation of related APIs into multiple files. 12 | 13 | ## APIs Implemented 14 | 15 | TODO ✨ 16 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/DecodeThrowable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableThrowable.swift 3 | // DiscordAPI 4 | // 5 | // Created by Vincent Kwok on 9/3/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Utility type wrapper for handling JSON decoding errors in arrays 11 | /// 12 | /// Wrapping any type with this wrapper allows array items with JSON 13 | /// decoding errors to be removed, instead of the whole struct failing 14 | /// to decode. Use a compactMap with `try? result.get()` to remove 15 | /// items that failed to decode. 16 | public struct DecodeThrowable: Decodable { 17 | /// Decoded result, use `try? .get()` to retrive the value 18 | /// or nil if decoding failed 19 | public let result: Result 20 | 21 | public func unwrap() throws -> T { 22 | try result.get() 23 | } 24 | 25 | public init(_ wrapped: T) { 26 | result = .success(wrapped) 27 | } 28 | 29 | public init(from decoder: Decoder) throws { 30 | result = Result(catching: { try T(from: decoder) }) 31 | } 32 | } 33 | 34 | public extension Array { 35 | func compactUnwrap() -> [T] where Element == DecodeThrowable { 36 | self.compactMap { try? $0.unwrap() } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/DiscordRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscordRange.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 14/3/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DiscordRange: Codable, CustomStringConvertible, CustomDebugStringConvertible { 11 | public var description: String { 12 | closedRange.description 13 | } 14 | public var debugDescription: String { 15 | closedRange.debugDescription 16 | } 17 | 18 | public let start: Int 19 | public let end: Int 20 | 21 | public var closedRange: ClosedRange { start...end } 22 | 23 | public init(start: Int, end: Int) { 24 | self.start = start 25 | self.end = end 26 | } 27 | 28 | public init(from decoder: any Decoder) throws { 29 | var container = try decoder.unkeyedContainer() 30 | self.start = try container.decode(Int.self) 31 | self.end = try container.decode(Int.self) 32 | guard container.isAtEnd else { 33 | throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Unexpected elements at end of range")) 34 | } 35 | } 36 | 37 | public func encode(to encoder: any Encoder) throws { 38 | var container = encoder.unkeyedContainer() 39 | try container.encode(start) 40 | try container.encode(end) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/EventDispatch.swift: -------------------------------------------------------------------------------- 1 | /// EventDispatch.swift 2 | 3 | import Foundation 4 | 5 | /// `EventDispatch` is a helper class that can be used to implement 6 | /// event publishing/subscribing in Swift. 7 | /// 8 | /// Allows multiple handlers to subscribe to an event, and for event 9 | /// data to be notified to all handlers at once. Makes subscribing to 10 | /// events extremely convenient. 11 | /// 12 | /// Changes were made to improve style, support Swift 5, 13 | /// as well as optimise some portions. Adapted from: 14 | /// [swift-event-dispatch](https://github.com/gongzhang/) 15 | public class EventDispatch: EventDispatchProtocol { 16 | public typealias HandlerIdentifier = Int 17 | 18 | private typealias Handler = (Event) -> Void 19 | private var handlerIds = [HandlerIdentifier]() 20 | private var handlers = [Handler]() 21 | private var lastId: HandlerIdentifier = 0 22 | 23 | private let evtQueue: DispatchQueue 24 | 25 | /// Inits an instance of ``EventDispatch`` 26 | /// 27 | /// Set the event type by using generics, for example: 28 | /// 29 | /// ```swift 30 | /// let dispatch = EventDispatch() // Any type is supported... 31 | /// dispatch.addHandler { data in 32 | /// print("Event data: \(data)") 33 | /// } 34 | /// dispatch.notify(true) 35 | /// 36 | /// let anotherDispatch = EventDispatch<(Int, Bool)>() // ...including tuples 37 | /// anotherDispatch.notify((10, false)) 38 | /// ``` 39 | /// 40 | /// All handlers are run asynchronously on an unique `DispatchQueue` 41 | /// per instance of ``EventDispatch``, with a randomly generated UUID 42 | /// string for the label. 43 | public init() { 44 | evtQueue = DispatchQueue(label: UUID().uuidString, qos: .userInteractive, attributes: .concurrent, target: .main) 45 | } 46 | 47 | /// Register a handler closure to be called when the event is notified 48 | /// 49 | /// - Parameters: 50 | /// - handler: A closure that is called with the event when this `EventDispatch` 51 | /// is notified 52 | /// 53 | /// - Returns: A `HandlerIdentifier` that can be passed to `removeHandler()` 54 | /// to remove this handler 55 | public func addHandler(handler: @escaping (Event) -> Void) -> HandlerIdentifier { 56 | lastId += 1 57 | handlerIds.append(lastId) 58 | handlers.append(handler) 59 | return lastId 60 | } 61 | 62 | /// Similar to addHandler(), but removes the handler after it's notified 63 | /// 64 | /// - Parameters: 65 | /// - handler: A closure that is called with the event when this `EventDispatch` 66 | /// is notified. This closure will only be called _once_. 67 | public func handleOnce(handler: @escaping (Event) -> Void) { 68 | var id: HandlerIdentifier! 69 | id = addHandler { [weak self] event in 70 | handler(event) 71 | _ = self?.removeHandler(handler: id) 72 | } 73 | } 74 | 75 | /// Removes a handler with a given identifier 76 | /// 77 | /// - Parameters: 78 | /// - handler: The identifier of the handler, returned from `addHandler()` 79 | /// 80 | /// - Returns: `True` if the handler exists and was removed 81 | public func removeHandler(handler: HandlerIdentifier) -> Bool { 82 | if let index = handlerIds.firstIndex(of: handler) { 83 | handlerIds.remove(at: index) 84 | _ = handlers.remove(at: index) 85 | return true 86 | } else { 87 | return false 88 | } 89 | } 90 | 91 | /// Notify all handlers of this `EventDispatch` with event data 92 | /// 93 | /// This method will immediately notify all registered handlers 94 | /// asynchronously with the event data it is called with. 95 | /// 96 | /// - Parameters: 97 | /// - event: The event data to notify handlers with 98 | public func notify(event: Event) { 99 | let copiedHandlers = handlers 100 | for handler in copiedHandlers { 101 | evtQueue.async { handler(event) } 102 | } 103 | } 104 | } 105 | 106 | public protocol EventDispatchProtocol { 107 | associatedtype EventType 108 | func notify(event: EventType) 109 | } 110 | 111 | public extension EventDispatchProtocol where EventType: Equatable { 112 | /// Notify all handlers of this `EventDispatch` with event data, 113 | /// only if the 2 event data passed to this method are different 114 | /// 115 | /// - Parameters: 116 | /// - old: The old event data 117 | /// - new: The new event data, handlers will be notified with 118 | /// this if `new != old` 119 | func notifyIfChanged(old: EventType, new: EventType) { 120 | if old != new { 121 | notify(event: new) 122 | } 123 | } 124 | } 125 | 126 | public extension EventDispatchProtocol where EventType == Void { 127 | func notify() { 128 | notify(event: ()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/HashedAsset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 1/6/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias HashedAsset = String 11 | 12 | public extension HashedAsset { 13 | enum AssetFormat: String { 14 | case jpeg = "jpg" 15 | case png = "png" 16 | case webp = "webp" 17 | case gif = "gif" 18 | } 19 | 20 | private static func joinPaths(with format: AssetFormat, _ paths: String...) -> URL { 21 | var base = URL(string: DiscordKitConfig.default.cdnURL)! 22 | 23 | for path in paths { base.appendPathComponent(path) } 24 | base.appendPathExtension(format.rawValue) 25 | 26 | return base 27 | } 28 | } 29 | 30 | public extension HashedAsset { 31 | // TODO: Validate requested format 32 | 33 | /// Returns the avatar URL of a user 34 | /// 35 | /// > This resource might be animated. It is animated if the hash begins with `a_`, and will 36 | /// > be available in GIF format as well. 37 | /// 38 | /// - Parameters: 39 | /// - userID: ID of user 40 | /// - format: Format of banner (PNG, JPEG, WebP, GIF) 41 | /// - size: Size of asset, a power of 2 from 16 to 4096 42 | func avatarURL( 43 | of userID: Snowflake, 44 | with format: AssetFormat = .png, 45 | size: Int? = nil 46 | ) -> URL { 47 | return HashedAsset.joinPaths(with: format, "avatars", userID, self) 48 | .setSize(size: size) 49 | } 50 | 51 | /// Returns the banner URL of a guild or user 52 | /// 53 | /// > This resource might be animated. It is animated if the hash begins with `a_`, and will 54 | /// > be available in GIF format as well. 55 | /// 56 | /// - Parameters: 57 | /// - id: ID of guild or user 58 | /// - format: Format of banner (PNG, JPEG, WebP, GIF) 59 | /// - size: Size of asset, a power of 2 from 16 to 4096 60 | func bannerURL( 61 | of id: Snowflake, 62 | with format: AssetFormat = .png, 63 | size: Int? = nil 64 | ) -> URL { 65 | return HashedAsset.joinPaths(with: format, "banners", id, self) 66 | .setSize(size: size) 67 | } 68 | 69 | /// Returns the icon URL of a guild 70 | /// 71 | /// > This resource might be animated. It is animated if the hash begins with `a_`, and will 72 | /// > be available in GIF format as well. 73 | /// 74 | /// - Parameters: 75 | /// - id: ID of guild 76 | /// - format: Format of icon (PNG, JPEG, WebP, GIF) 77 | /// - size: Size of asset, a power of 2 from 16 to 4096 78 | func guildIconURL( 79 | of guildID: Snowflake, 80 | with format: AssetFormat = .png, 81 | size: Int? = nil 82 | ) -> URL { 83 | return HashedAsset.joinPaths(with: format, "icons", guildID, self) 84 | .setSize(size: size) 85 | } 86 | 87 | /// Returns the icon URL of a sticker pack banner 88 | /// 89 | /// > This resource will not be animated. 90 | /// 91 | /// - Parameters: 92 | /// - format: Format of banner (PNG, JPEG, WebP) 93 | /// - size: Size of asset, a power of 2 from 16 to 4096 94 | func stickerPackBannerURL( 95 | with format: AssetFormat = .png, 96 | size: Int? = nil 97 | ) -> URL { 98 | return HashedAsset.joinPaths(with: format, "app-assets", "710982414301790216", "store", self) 99 | .setSize(size: size) 100 | } 101 | } 102 | 103 | public extension HashedAsset { 104 | /// Get the default avatar image of a provided user discriminator 105 | /// 106 | /// - Parameter discriminator: User discriminator string 107 | static func defaultAvatar(of discriminator: String) -> URL { 108 | return HashedAsset.joinPaths( 109 | with: .png, 110 | "embed", "avatars", String((Int(discriminator) ?? 0) % 5) 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/HybridSnowflake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HybridSnowflake.swift 3 | // 4 | // 5 | // Created by Vincent Kwok on 16/2/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A holder that can hold either representation of [Snowflakes](https://discord.com/developers/docs/reference#snowflakes) 11 | /// 12 | /// This is used in cases where the API is known to arbitrarily return 13 | /// either representations of Snowflakes. 14 | public enum HybridSnowflake: Codable { 15 | /// A snowflake represented as an integer 16 | case int(Int) 17 | 18 | /// A snowflake represented as a string 19 | case string(Snowflake) 20 | 21 | public func encode(to encoder: Encoder) throws { 22 | var container = encoder.singleValueContainer() 23 | switch self { 24 | case .int(let val): 25 | try container.encode(val) 26 | case .string(let val): 27 | try container.encode(val) 28 | } 29 | } 30 | 31 | public init(from decoder: Decoder) throws { 32 | let container = try decoder.singleValueContainer() 33 | if let val = try? container.decode(String.self) { 34 | self = .string(val) 35 | } else { 36 | self = .int(try container.decode(Int.self)) 37 | } 38 | } 39 | 40 | public var stringValue: Snowflake { 41 | switch self { 42 | case .string(let str): return str 43 | case .int(let int): return String(int) 44 | } 45 | } 46 | public var intValue: Int { 47 | switch self { 48 | case .string(let str): return Int(str) ?? 0 49 | case .int(let int): return int 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/DiscordKitCore/Utils/NullEncodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NullEncoder.swift 3 | // From https://stackoverflow.com/a/62312021/ 4 | // 5 | // 6 | // Created by Vincent Kwok on 14/10/22. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Explicitly include a value even when nil when encoded 12 | @propertyWrapper 13 | public struct NullEncodable: Encodable where T: Encodable { 14 | public let wrappedValue: T? 15 | 16 | public init(wrappedValue: T?) { 17 | self.wrappedValue = wrappedValue 18 | } 19 | 20 | public func encode(to encoder: Encoder) throws { 21 | var container = encoder.singleValueContainer() 22 | switch wrappedValue { 23 | case .some(let value): try container.encode(value) 24 | case .none: try container.encodeNil() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/DiscordKitCommonTests/PermissionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionTests.swift 3 | // 4 | // 5 | // Created by Charlene Campbell on 5/30/22. 6 | // 7 | 8 | import XCTest 9 | import DiscordKitCore 10 | 11 | class PermissionTests: XCTestCase { 12 | func testPermissionsDecode() { 13 | XCTAssertEqual( 14 | Permissions([.viewChannel, .addReactions, .banMembers]), 15 | try JSONDecoder().decode(Permissions.self, from: "\"1092\"".data(using: .utf8)!) 16 | ) 17 | XCTAssertEqual( 18 | Permissions([]), 19 | try JSONDecoder().decode(Permissions.self, from: "\"\"".data(using: .utf8)!) 20 | ) 21 | XCTAssertThrowsError( 22 | try JSONDecoder().decode(Permissions.self, from: "1092".data(using: .utf8)!) 23 | ) 24 | } 25 | 26 | func testPermissionsEncode() { 27 | XCTAssertEqual( 28 | "\"3072\"", 29 | String(data: try JSONEncoder().encode(Permissions([.viewChannel, .sendMessages])), encoding: .utf8) 30 | ) 31 | } 32 | } 33 | --------------------------------------------------------------------------------