├── .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 |
--------------------------------------------------------------------------------