{"id":13750690,"url":"https://github.com/DiscordBM/DiscordBM","last_synced_at":"2025-05-09T16:31:30.999Z","repository":{"id":59403927,"uuid":"533492888","full_name":"DiscordBM/DiscordBM","owner":"DiscordBM","description":"A Multiplatform Swift Discord Library, Primarily For Making Bots","archived":false,"fork":false,"pushed_at":"2024-04-12T16:44:47.000Z","size":2440,"stargazers_count":62,"open_issues_count":8,"forks_count":7,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-04-14T01:33:20.343Z","etag":null,"topics":["bot","discord","discord-api","server-side-swift","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DiscordBM.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"github":null,"patreon":null,"open_collective":"discordbm","ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null}},"created_at":"2022-09-06T20:31:41.000Z","updated_at":"2024-04-15T11:20:38.158Z","dependencies_parsed_at":"2023-09-21T23:25:13.776Z","dependency_job_id":"53dfb802-5c22-4b2d-bdbd-e36104971717","html_url":"https://github.com/DiscordBM/DiscordBM","commit_stats":{"total_commits":773,"total_committers":6,"mean_commits":"128.83333333333334","dds":"0.0077619663648124115","last_synced_commit":"ddce0d02aa46102499eb713c9b4a8efd3ad6fa87"},"previous_names":["discordbm/discordbm","mahdibm/discordbm"],"tags_count":81,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DiscordBM%2FDiscordBM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DiscordBM%2FDiscordBM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DiscordBM%2FDiscordBM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DiscordBM%2FDiscordBM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DiscordBM","download_url":"https://codeload.github.com/DiscordBM/DiscordBM/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224377235,"owners_count":17301112,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","discord","discord-api","server-side-swift","swift"],"created_at":"2024-08-03T08:00:44.353Z","updated_at":"2025-05-09T16:31:30.980Z","avatar_url":"https://github.com/DiscordBM.png","language":"Swift","funding_links":["https://opencollective.com/discordbm"],"categories":["Swift","Frameworks with plugins"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://user-images.githubusercontent.com/54685446/201329617-9fd91ab0-35c2-42c2-8963-47b68c6a490a.png\" alt=\"DiscordBM\"\u003e\n    \u003cbr\u003e\n    \u003ca href=\"https://www.swift.org/sswg/\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/sswg-sandbox-lightgrey.svg\" alt=\"SSWG Incubation Status: Sandbox\"\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://discord.gg/kxfs5n7HVE\"\u003e\n        \u003cimg src=\"https://dcbadge.vercel.app/api/server/kxfs5n7HVE?style=flat\" alt=\"DiscordBM Server\"\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/DiscordBM/DiscordBM/actions/workflows/tests.yml\"\u003e\n        \u003cimg src=\"https://github.com/DiscordBM/DiscordBM/actions/workflows/tests.yml/badge.svg\" alt=\"Tests Badge\"\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://github.com/DiscordBM/DiscordBM/actions/workflows/integration-tests.yml\"\u003e\n        \u003cimg src=\"https://github.com/DiscordBM/DiscordBM/actions/workflows/integration-tests.yml/badge.svg\" alt=\"Integration Tests Badge\"\u003e\n    \u003ca href=\"https://github.com/DiscordBM/DiscordBM\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/dynamic/json?url=https://rapi.mahdibm.com/v1/loc/DiscordBM/count\u0026query=$.count\u0026label=Swift%20lines\" alt=\"Swift lines of code\"\u003e\n    \u003c/a\u003e\n    \u003c/a\u003e\n    \u003ca href=\"https://swift.org\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/swift-6.1%20%2F%206.0%20%2F%205.10-brightgreen.svg\" alt=\"Latest/Minimum Swift Version\"\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\n## Notable Features\n* Everything with async/await. Full integration with the latest Server-Side Swift packages.\n* Access the full Discord API for bots, except Voice (for now).\n* Connect to the Discord Gateway and receive all events easily.\n* Send requests to the Discord API using library's Discord client.\n* Hard-typed APIs. All Gateway events and API responses have their own type and can be decoded easily.\n* Abstractions for easier testability.\n\n## Showcase\nVapor community's [Penny bot](https://github.com/vapor/penny-bot) serves as a good example of [utilizing this library](https://github.com/vapor/penny-bot/blob/main/Sources/Penny/Services/DiscordService/DiscordService.swift#L1).  \nUnfortunately Penny isn't a good project to study how to use `DiscordBM`, due to its complexity. For now, this README is your best friend.\n\n## How To Use\n\n### Initializing a Gateway Manager\n\nFirst you need to initialize a `BotGatewayManager` instance, then tell it to connect and start using it.   \n\n```swift\nimport DiscordBM\n\nlet bot = await BotGatewayManager(\n    token: \u003c#Your Bot Token#\u003e,\n    presence: .init( /// Set up bot's initial presence\n        /// Will show up as \"Playing Fortnite\"\n        activities: [.init(name: \"Fortnite\", type: .game)], \n        status: .online,\n        afk: false\n    ),\n    /// Add all the intents you want\n    /// You can also use `Gateway.Intent.unprivileged` or `Gateway.Intent.allCases`\n    intents: [.guildMessages, .messageContent]\n)\n```\nSee the [GatewayConnection tests](https://github.com/DiscordBM/DiscordBM/blob/main/Tests/IntegrationTests/GatwayConnection.swift) or [Vapor community's Penny bot](https://github.com/vapor/penny-bot/blob/main/Sources/Penny/MainService/PennyService.swift) for real-world examples.\n\n\u003e [!Warning]   \n\u003e In a production app you should use [**environment variables**](https://swiftonserver.com/using-environment-variables-in-swift/) to load your Bot Token.   \n\u003e Avoid hard-coding your Bot Token to reduce the chances of leaking it.   \n\n### Initializing a Gateway Manager With Vapor\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\nWith Vapor, use Vapor `Application`'s `EventLoopGroup` and `HTTPClient`:\n```swift\nimport DiscordBM\nimport Vapor\n\nlet app: Application = \u003c#Your Vapor Application#\u003e\nlet bot = await BotGatewayManager(\n    eventLoopGroup: app.eventLoopGroup, /// Use Vapor's `EventLoopGroup`\n    httpClient: app.http.client.shared, /// Use Vapor's `HTTPClient`\n    token: \u003c#Your Bot Token#\u003e,\n    presence: .init( /// Set up bot's initial presence\n        /// Will show up as \"Playing Fortnite\"\n        activities: [.init(name: \"Fortnite\", type: .game)],\n        status: .online,\n        afk: false\n    ),\n    /// Add all the intents you want\n    /// You can also use `Gateway.Intent.unprivileged` or `Gateway.Intent.allCases`\n    intents: [.guildMessages, .messageContent]\n)\n```\n\n\u003c/details\u003e\n\n### Using The Gateway Manager\n\u003e [!Note] \n\u003e For your app's entry point, you should use a type with the [`@main` attribute](https://www.hackingwithswift.com/swift/5.3/atmain) like below.    \n```swift\n@main\nstruct EntryPoint {\n    static func main() async throws {\n        /// Make an instance like above\n        let bot: BotGatewayManager = \u003c#GatewayManager You Made In Previous Steps#\u003e\n\n        /// You can also wrap this task-group in the `run()` function of a `Service`, if you're using `ServiceLifecycle`:\n        /// https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle\n        await withTaskGroup(of: Void.self) { taskGroup in\n            taskGroup.addTask {\n                await bot.connect()\n            }\n\n            taskGroup.addTask {\n                /// Handle each event in the `bot.events` async stream\n                /// This stream will never end, therefore preventing your executable from exiting\n                for await event in await bot.events {\n                    /// You can move the above `taskGroup.addTask {` to down here, for more concurrency.\n                    await EventHandler(event: event, client: bot.client).handleAsync()\n                }\n            }\n        }\n    }\n}\n\n/// To keep things cleaner, use a type conforming to \n/// `GatewayEventHandler` to handle your Gateway events.\nstruct EventHandler: GatewayEventHandler {\n    let event: Gateway.Event\n    let client: any DiscordClient\n\n    /// Each Gateway payload has its own function. \n    /// See `GatewayEventHandler` for the full list.\n    /// This function will only be called upon receiving `MESSAGE_CREATE` events.\n    func onMessageCreate(_ payload: Gateway.MessageCreate) async throws {\n        print(\"NEW MESSAGE!\", payload)\n\n        /// Use `client` to send requests to Discord\n        let response = try await client.createMessage(\n            channelId: payload.channel_id,\n            payload: .init(content: \"Got a message: '\\(payload.content)'\")\n        )\n            \n        /// Easily decode the response to the correct type\n        /// `message` will be of type `DiscordChannel.Message`.\n        let message = try response.decode()\n    }\n}\n```\n\u003e [!Note]  \n\u003e On a successful connection, you will **always** see a `NOTICE` log indicating `connection is established`.   \n\n\u003e [!Note]  \n\u003e By default, `DiscordBM` automatically handles HTTP rate-limits and you don't need to worry about them.\n\n### Mindset\nThe way you can make sense of the library is to think of it as a direct implementation of the Discord API.   \nIn most cases, the library doesn't try to abstract away Discord's stuff.   \n\n* If something is related to the Gateway, you should find it near `GatewayManager`. \n* If there is a HTTP request you want to make, you'll need to use `DiscordClient`.\n* You should read Discord documentation's related notes when you want to use something of this library.   \n  Everything in the library has its related Discord documentation section linked near it.\n\n### Finding Your Bot Token\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n  \nIn [Discord developer portal](https://discord.com/developers/applications):\n![Finding Bot Token](https://user-images.githubusercontent.com/54685446/200565393-ea31c2ad-fd3a-44a1-9789-89460ab5d1a9.png)\n\n\u003c/details\u003e\n\n### Application (including Slash) Commands\n\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordBM` comes with full support for all kinds of \"interactions\" such as slash commands, modals, autocomplete etc... and gives you full control over how you want to use them using type-safe APIs.    \n\u003e You can see Penny as an example of using all kinds of commands in production.    \nPenny registers the commands [here](https://github.com/vapor/penny-bot/blob/main/Sources/Penny/CommandsManager.swift) and responds to them [here](https://github.com/vapor/penny-bot/blob/main/Sources/Penny/Handlers/InteractionHandler.swift).   \n\nIn this example you'll only make 2 simple slash commands, so you can get started:   \n\nIn your `EntryPoint.main()`:\n```swift\n/// Make a list of `Payloads.ApplicationCommandCreate`s that you want to register\n/// `DiscordCommand` is an enum that has the full info of your commands. See below\nlet commands = DiscordCommand.allCases.map { command in\n    return Payloads.ApplicationCommandCreate(\n        name: command.rawValue,\n        description: command.description,\n        options: command.options\n    )\n}\n\n/// You only need to do this once on startup. This updates all your commands to the new ones.\ntry await bot.client\n    .bulkSetApplicationCommands(payload: commands)\n    .guardSuccess() /// Throw an error if not successful\n\n/// Use the events-stream later since the for-loop blocks the function\nfor await event in await bot.events {\n    await EventHandler(event: event, client: bot.client).handleAsync()\n}\n```\nIn your `EventHandler`:\n```swift\n/// Use `onInteractionCreate(_:)` for handling interactions.\nstruct EventHandler: GatewayEventHandler {\n    let event: Gateway.Event\n    let client: any DiscordClient\n    let logger = Logger(label: \"EventHandler\")\n\n    /// Handle Interactions.\n    func onInteractionCreate(_ interaction: Interaction) async throws {\n        /// You only have 3 second to respond, so it's better to send\n        /// the response right away, and edit the response later.\n        /// This will show a loading indicator to users.\n        try await client.createInteractionResponse(\n            id: interaction.id,\n            token: interaction.token,\n            payload: .deferredChannelMessageWithSource()\n        ).guardSuccess()\n\n        /// Delete this if you want. Just here so you notice the loading indicator :)\n        try await Task.sleep(for: .seconds(1))\n\n        /// Handle the interaction data\n        switch interaction.data {\n        case let .applicationCommand(applicationCommand):\n            switch DiscordCommand(rawValue: applicationCommand.name) {\n            case .echo:\n                if let echo = applicationCommand.option(named: \"text\")?.value?.asString {\n                    /// Edits the interaction response.\n                    /// This response is intentionally too fancy just so you see what's possible :)\n                    try await client.updateOriginalInteractionResponse(\n                        token: interaction.token,\n                        payload: Payloads.EditWebhookMessage(\n                            content: \"Hello, You wanted me to echo something!\",\n                            embeds: [Embed(\n                                title: \"This is an embed\",\n                                description: \"\"\"\n                                    You sent this, so I'll echo it to you!\n\n                                    \u003e \\(DiscordUtils.escapingSpecialCharacters(echo))\n                                    \"\"\",\n                                timestamp: Date(),\n                                color: .init(value: .random(in: 0 ..\u003c (1 \u003c\u003c 24) )),\n                                footer: .init(text: \"Footer!\"),\n                                author: .init(name: \"Authored by DiscordBM!\"),\n                                fields: [\n                                    .init(name: \"field name!\", value: \"field value!\")\n                                ]\n                            )],\n                            components: [[.button(.init(\n                                label: \"Open DiscordBM!\",\n                                url: \"https://github.com/DiscordBM/DiscordBM\"\n                            ))]]\n                        )\n                    ).guardSuccess()\n                } else {\n                    try await client.updateOriginalInteractionResponse(\n                        token: interaction.token,\n                        payload: Payloads.EditWebhookMessage(\n                            content: \"Hello, You wanted me to echo something!\",\n                            embeds: [Embed(\n                                title: \"This is an embed\",\n                                description: \"\"\"\n                                    You sent this, so I'll echo it to you but there was nothing!\n                                    \"\"\",\n                                timestamp: Date().addingTimeInterval(90),\n                                color: .green,\n                                footer: .init(text: \"Footer!\"),\n                                author: .init(name: \"Authored by DiscordBM!\"),\n                                fields: [\n                                    .init(name: \"field name!\", value: \"field value!\")\n                                ]\n                            )]\n                        )\n                    ).guardSuccess()\n                }\n            case .link:\n                /// `DiscordBM` has some \"require\" functions for easier unwrapping of\n                /// application commands. These \"require\" functions will either give you\n                /// what you want, or throw an error.\n                /// See the full list below.\n                let subcommandOption = try (applicationCommand.options?.first).requireValue()\n                let subcommandName = subcommandOption.name\n                let subcommand = try LinkSubCommand(rawValue: subcommandName).requireValue()\n\n                let id = try (subcommandOption.options?.first).requireValue().requireString()\n                let name = subcommand.rawValue.capitalized\n\n                try await client.updateOriginalInteractionResponse(\n                    token: interaction.token,\n                    payload: Payloads.EditWebhookMessage(\n                        content: \"Hi, did you wanted me to link your accounts?\",\n                        embeds: [.init(\n                            description: \"Will link a \\(name) account with id '\\(id)'\",\n                            color: .yellow\n                        )]\n                    )\n                ).guardSuccess()\n            case .none: break\n            }\n        default: break\n        }\n    }\n}\n```\nIn a new file like `DiscordCommand.swift`, to keep things organized: \n```swift\n// MARK: - Define a nice clean enum for your commands\nenum DiscordCommand: String, CaseIterable {\n    case echo\n    case link\n\n    /// The description of the command that Discord users will see.\n    var description: String? {\n        switch self {\n        case .echo:\n            return \"Echos what you say\"\n        case .link:\n            return \"Links your accounts \"\n        }\n    }\n\n    /// The options of the command that Discord users will have.\n    var options: [ApplicationCommand.Option]? {\n        switch self {\n        case .echo:\n            return [ApplicationCommand.Option(\n                type: .string,\n                name: \"text\",\n                description: \"What to echo :)\"\n            )]\n        case .link:\n            return LinkSubCommand.allCases.map { subCommand in\n                return ApplicationCommand.Option(\n                    type: .subCommand,\n                    name: subCommand.rawValue,\n                    description: subCommand.description,\n                    options: subCommand.options\n                )\n            }\n        }\n    }\n}\n\n// MARK: - You can use enums for subcommands too \nenum LinkSubCommand: String, CaseIterable {\n    case discord\n    case github\n\n    /// The description of the subcommand that Discord users will see.\n    var description: String {\n        switch self {\n        case .discord:\n            return \"Link your Discord account\"\n        case .github:\n            return \"Link your Github account\"\n        }\n    }\n\n    /// The options of the subcommand that Discord users will have.\n    var options: [ApplicationCommand.Option] {\n        switch self {\n        case .discord: return [ApplicationCommand.Option(\n            type: .string,\n            name: \"id\",\n            description: \"Your Discord account ID\",\n            required: true\n        )]\n        case .github: return [ApplicationCommand.Option(\n            type: .string,\n            name: \"id\",\n            description: \"Your Github account ID\",\n            required: true\n        )]\n        }\n    }\n}\n```\n\n#### Interaction Parsing Helpers\n* `StringIntDoubleBool` has:\n  * `requireString() throws -\u003e String`\n  * `requireInt() throws -\u003e Int`\n  * `requireDouble() throws -\u003e Double`\n  * `requireBool() throws -\u003e Bool`\n* `Interaction.ApplicationCommand.Option` has all the `StringIntDoubleBool` functions for unwrapping an option's `value` .\n* `Interaction.ApplicationCommand.Option` and related types (like `[Option]`) have:\n  * `option(named: String) -\u003e Option?`\n  * `requireOption(named: String) throws -\u003e Option`\n* `[Interaction.ActionRow]` and `[Interaction.ActionRow.Component]` have:\n  * `component(customId: String) -\u003e Interaction.ActionRow.Component?`\n  * `requireComponent(customId: String) throws -\u003e Interaction.ActionRow.Component`\n* `Interaction.ActionRow.Component` has:\n  * `requireButton() throws -\u003e Button`\n  * `requireStringSelect() throws -\u003e StringSelectMenu`\n  * `requireTextInput() throws -\u003e TextInput`\n  * `requireUserSelect() throws -\u003e SelectMenu`\n  * `requireRoleSelect() throws -\u003e SelectMenu`\n  * `requireMentionableSelect() throws -\u003e SelectMenu`\n  * `requireChannelSelect() throws -\u003e ChannelSelectMenu`\n* `Interaction.Data` has:\n  * `requireApplicationCommand() throws -\u003e ApplicationCommand`\n  * `requireMessageComponent() throws -\u003e MessageComponent`\n  * `requireModalSubmit() throws -\u003e ModalSubmit`\n* Swift's `Optional` has a `requireValue() throws` function overload (only in `DiscordBM`).                                             \n\n\u003c/details\u003e\n\n### Sending Attachments\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n  \n`DiscordBM` has support for sending files as attachments.   \n\n\u003e It's usually better to send a link to your media to Discord, instead of sending the actual file everytime.   \n\n```swift\n/// Raw data of anything like an image\nlet image: ByteBuffer = \u003c#Your Image Buffer#\u003e\n\n/// Example 1\ntry await bot.client.createMessage(\n    channelId: \u003c#Channel ID#\u003e,\n    payload: .init(\n        content: \"A message with an attachment!\",\n        files: [.init(data: image, filename: \"pic.png\")],\n        attachments: [.init(index: 0, description: \"Picture of something secret :)\")]\n        ///                 ~~~~~~~^\n        /// `0` is the index of the attachment in the `files` array.\n    )\n)\n\n/// Example 2\ntry await bot.client.createMessage(\n    channelId: \u003c#Channel ID#\u003e,\n    payload: .init(\n        embeds: [.init(\n            title: \"An embed with an attachment!\",\n            image: .init(url: .attachment(name: \"penguin.png\"))\n            ///                          ~~~~~~~^ \n            /// `penguin.png` is the name of the attachment in the `files` array.   \n        )],\n        files: [.init(data: image, filename: \"penguin.png\")]\n    )\n)\n```\nTake a look at `testMultipartPayload()` in [/Tests/DiscordClientTests](https://github.com/DiscordBM/DiscordBM/blob/main/Tests/IntegrationTests/DiscordClient.swift) to see how you can send media in a real-world situation.\n\n\u003c/details\u003e\n\n### Discord Utils\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordBM` contains some utility functions for working with Discord's text-message format.   \n\nThe mention helpers:\n```swift\nlet userMention = DiscordUtils.mention(id: \u003c#User ID of type UserSnowflake#\u003e)\nlet channelMention = DiscordUtils.mention(id: \u003c#Channel ID of type ChannelSnowflake#\u003e)\nlet roleMention = DiscordUtils.mention(id: \u003c#Role ID of type RoleSnowflake#\u003e)\nlet slashCommandMention = DiscordUtils.slashCommand(name: \"help\", id: \u003c#Command ID#\u003e)\n\n/// Then:\n\n/// Will look like `Hi @UserName!` in Discord\nlet userMessage = \"Hi \\(userMention)!\"\n\n/// Will look like `Welcome to #ChannelName!` in Discord\nlet channelMessage = \"Welcome to \\(channelMention)!\"\n\n/// Will look like `Hello @RoleName!` in Discord\nlet roleMessage = \"Hello \\(roleMention)!\"\n\n/// Will look like `Use this command: /help` in Discord\nlet slashCommandMessage = \"Use this command: \\(slashCommandMention)\"\n```\n\nAnd the emoji helpers:\n```swift\nlet emoji = DiscordUtils.customEmoji(name: \"Peepo_Happy\", id: \u003c#Emoji ID#\u003e)\nlet animatedEmoji = DiscordUtils.customAnimatedEmoji(name: \"Peepo_Money\", id: \u003c#Emoji ID#\u003e)\n\n/// Then:\n\n/// Will look like `Are you happy now? EMOJI` where emoji is like https://cdn.discordapp.com/emojis/832181382595870730.webp\nlet emojiMessage = \"Are you happy now? \\(emoji)\"\n\n/// Will look like `Here comes the moneeeey EMOJI` where emoji is like https://cdn.discordapp.com/emojis/836533376285671424.gif\nlet animatedEmojiMessage = \"Here comes the moneeeey \\(emojiMessage)\"\n```\n\nPlus the **end-user-localized** timestamp helpers:\n```swift\nlet timestamp = DiscordUtils.timestamp(date: Date())\nlet anotherTimestamp = DiscordUtils.timestamp(unixTimestamp: 1 \u003c\u003c 31, style: .relativeTime)\n\n/// Then:\n\n/// Will look like `Time when message was sent: 20 April 2021 16:20` in Discord\nlet timestampMessage = \"Time when message was sent: \\(timestamp)\"\n\n/// Will look like `I'm a time traveler. I will be born: in 15 years` in Discord\nlet anotherTimestampMessage = \"I'm a time traveler. I will be born: \\(anotherTimestamp)\"\n```\n\nAnd a function to escape special characters from user input.    \nFor example if you type `**BOLD TEXT**` in Discord, it'll look like **BOLD TEXT**, but using this function, it'll instead look like the original `**BOLD TEXT**` input.\n```swift\nlet escaped = DiscordUtils.escapingSpecialCharacters(\"**BOLD TEXT**\")\n\n/// Will look like `Does this look bold to you?! **BOLD TEXT**` in Discord. Won't actually look bold.\nlet escapedMessage = \"Does this look bold to you?! \\(escaped)\"\n```\n\u003c/details\u003e\n\n### Discord Cache\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordBM` has the ability to cache Gateway events in-memory, and keep the data in sync with Discord:\n```swift\nlet cache = await DiscordCache(\n    /// The `GatewayManager`/`bot` to cache the events from. \n    gatewayManager: \u003c#GatewayManager You Made In Previous Steps#\u003e,\n    /// What intents to cache their related Gateway events. \n    /// This does not affect what events you receive from Discord.\n    /// The intents you enter here must have been enabled in your `GatewayManager`.\n    /// With `.all`, `DiscordCache` will cache all events.\n    intents: [.guilds, .guildMembers],\n    /// In big guilds/servers, Discord only sends your own member/presence info.\n    /// You need to request the rest of the members, and `DiscordCache` can do that for you.\n    /// Must have `guildMembers` and `guildPresences` intents enabled depending on what you want.\n    requestAllMembers: .enabled,\n    /// What messages to cache.\n    messageCachingPolicy: .saveEditHistoryAndDeleted\n)\n\n/// Access the cached stuff:\nif let aGuild = await cache.guilds[\u003c#Guild ID#\u003e] {\n    print(\"Guild name is:\", aGuild.name)\n} else {\n    print(\"Guild not found\")\n}\n```\n\n\u003c/details\u003e\n\n### Checking Permissions \u0026 Roles\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordBM` has some best-effort functions for checking permissions and roles.   \nFYI, in interactions, the [member field](https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-structure) already contains the resolved permissions (`Interaction.member.permissions`).\n\n\u003e You need a `DiscordCache` with intents containing `.guilds` \u0026 `.guildMembers` and also `requestAllMembers: .enabled`.   \n\n```swift\nlet cache: DiscordCache = \u003c#DiscordCache You Made In Previous Steps#\u003e\n\n/// Get the guild.\nguard let guild = await cache.guilds[\u003c#Guild ID#\u003e] else { return }\n\n/// Check if the user has `.viewChannel` \u0026 `.readMessageHistory` permissions in a channel.\nlet hasPermission = guild.userHasPermissions(\n    userId: \u003c#User ID#\u003e,\n    channelId: \u003c#Channel ID#\u003e, \n    permissions: [.viewChannel, .readMessageHistory]\n)\n\n/// Check if a user has the `.banMembers` guild-wide permission.\nlet hasGuildPermission = guild.userHasGuildPermission(\n    userId: \u003c#User ID#\u003e,\n    permission: .banMembers\n)\n\n/// Check if a user has a role.\nlet hasRole = guild.userHasRole(\n    userId: \u003c#User ID#\u003e,\n    roleId: \u003c#Role ID#\u003e\n)\n```\n\n\u003c/details\u003e\n\n### Sharding\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\nSharding is a way of splitting up your bot's load accross different `GatewayManager`s. It is useful if:\n* You want to implement zero-down-time scaling/updating.\n* You have too many guilds to be handeled by just 1 `GatewayManager`.\n\n\u003e Discord says sharding is required for bots with 2500 and more guilds. For more info, refer to the [Discord docs](https://discord.com/developers/docs/topics/gateway#sharding)\n\nTo enable sharding, simply replace your `BotGatewayManager` with `ShardingGatewayManager`:\n```swift\nlet bot = await ShardingGatewayManager(\n    eventLoopGroup: httpClient.eventLoopGroup,\n    httpClient: httpClient,\n    token: \u003c#Your Bot Token#\u003e,\n    presence: .init( /// Set up bot's initial presence\n        /// Will show up as \"Playing Fortnite\"\n        activities: [.init(name: \"Fortnite\", type: .game)], \n        status: .online,\n        afk: false\n    ),\n    /// Add all the intents you want\n    /// You can also use `Gateway.Intent.unprivileged` or `Gateway.Intent.allCases`\n    intents: [.guildMessages, .messageContent]\n)\n```\nAnd that's it! You've already enabled sharding. `DiscordBM` will create as many `BotGatewayManager`s as Discord suggests under the hood of `ShardingGatewayManager`, and will automatically handle them. \n\n\u003e `ShardingGatewayManager` might still only create 1 `BotGatewayManager` if that's what Discord suggests.\n\n`ShardingGatewayManager` takes a few more options than `BotGatewayManager` to customize how you want to perform sharding:\n```swift\nlet bot: any GatewayManager = await ShardingGatewayManager(\n    eventLoopGroup: httpClient.eventLoopGroup,\n    httpClient: httpClient,\n    configuration: .init(\n        /// You can request an exact amount of shard counts using `.exact(\u003cnumber\u003e)`.\n        /// Defaults to `.automatic` which means it will ask Discord for a suggestion of how many shards to spin up.\n        shardCount: .exact(\u003c#number#\u003e),\n        /// This is an opportunity to customize what shard takes care of which intents.\n        /// By default, all intents are passed to all shards.\n        makeIntents: { (indexOfShard: Int, totalShardCount: Int) -\u003e [Gateway.Intent] in\n            /// return a value of type `[Gateway.Intent]` based on `indexOfShard` and `totalShardCount`.\n        }\n    ),\n    ...\n)\n```\n\u003c/details\u003e\n\n## Related Projects\n\n### Discord Logger\n\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordLogger` enables you to send your logs to Discord with beautiful formatting and a lot of customization options.\nRead more about it at https://github.com/DiscordBM/DiscordLogger.\n\n\u003c/details\u003e\n\n### React-To-Role\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\nReact-To-Role helps you assign roles to members when they react to a message.\nRead more about it at https://github.com/DiscordBM/DiscordReactToRole.\n\n\u003c/details\u003e\n\n## Testability\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\n`DiscordBM` comes with tools to make testing your app easier.   \n* You can type-erase your `BotGatewayManager`s using the `GatewayManager` protocol so you can override your gateway manager with a mocked implementation in tests.   \n* You can also do the same for `DefaultDiscordClient` and type-erase it using the `DiscordClient` protocol so you can provide a mocked implementation when testing.\n\n\u003c/details\u003e\n\n## Implementation Details\n\n### Default Discord Client\n\u003cdetails\u003e\n  \u003csummary\u003e Click to expand \u003c/summary\u003e\n\nThese are some general implementation detail notes about the `DefaultDiscordClient`.   \nGenerally, the `DefaultDiscordClient` will try to be as smart as possible with minimal compromise.\n\n\u003e I'll refer to `DefaultDiscordClient` as \"DDC\" to be more concise.\n\n#### Rate Limits\n`DiscordBM` comes with a `HTTPRateLimiter` type that keeps track of the `x-ratelimit` headers.    \nThis, in conjunction with `ClientConfiguration`'s `RetryPolicy`, helps `DiscordBM` to recover from what that can otherwise be a `429 Too Many Requests` error from Discord.   \nThe behavior specified below is enabled by default.\n\n* Before each request, DDC will ask the rate-limiter if the headers allow a request.\n* The rate-limiter will respond with `yes, you can`, `no, you can't` or `yes, but you must wait x seconds first, otherwise no`.\n* If the response is `yes`, the DDC will continue performing the request.\n* If the response is `no`, the DDC will throw a \"rate-limited\" error.\n* If the response is `yes, but you must wait x seconds first, otherwise no`, then the DDC will look at the `retryPolicy` of its `configuration`.\n* The DDC will act like there has been a `429` error, and will ask the `retryPolicy` if it's possible to retry such a failure, and under what circumstances.\n* The `retryPolicy` may specify that `429` requests can be retried `basedOnHeaders` if not longer than `maxAllowed` seconds.\n* The DDC will wait as long as `x seconds` which the rate-limiter specified, then will perform the request. This only happens if the `x seconds` is not longer than the `maxAllowed`.\n* In any other cases other than specified above, the DDC will fail with a \"rate-limited\" error.\n\n#### Concurrent Requests\n`ClientConfiguration`s `CachingBehavior` has the ability to avoid multiple concurrent requests with the same \"cacheable identity\".   \n* You can enable caching using DDC's `configuration.cachingBehavior` through the initializers by passing `cachingBehavior: .minimal`, `.enabled` or the `.custom` static functions.  \n* As an example, if you have caching enabled for a cacheable endpoint, and you make 10 concurrent requests to the endpoint with the same parameters, the DDC will only perform 1 of those requests, and let the other 9 requests use the cached value. \n\n\u003c/details\u003e\n\n## How To Add DiscordBM To Your Project\n\nTo use the `DiscordBM` library in a SwiftPM project, \nadd the following line to the dependencies in your `Package.swift` file:\n\n```swift\n.package(url: \"https://github.com/DiscordBM/DiscordBM\", from: \"1.0.0\"),\n```\n\nInclude `DiscordBM` as a dependency for your targets:\n\n```swift\n.target(name: \"\u003ctarget\u003e\", dependencies: [\n    .product(name: \"DiscordBM\", package: \"DiscordBM\"),\n]),\n```\n\nFinally, add `import DiscordBM` to your source code.\n\n## Versioning\n`DiscordBM` will try to follow Semantic Versioning 2.0.0, with exceptions:    \n* To keep `DiscordBM` up to date with Discord API's frequent changes, `DiscordBM` will **add** any new properties to any types in **minor** versions, even if it's technically a breaking change.   \n* `DiscordBM` tries to minimize the effect of this requirement. For example most enums have an underscored case named `__undocumented` which asks users to use non-exhaustive switch statements, preventing future code-breakages.  \n\n## Contribution \u0026 Support\n* If you need help with anything, ask in [DiscordBM's Discord server](https://discord.gg/kxfs5n7HVE).\n* Any contribution is more than welcome. You can find me in the server to discuss your ideas.    \n* If there is missing/upcoming feature, you can make an issue/PR for it with a link to the related Discord docs page or the related issue/PR in [Discord docs repository](https://github.com/discord/discord-api-docs).   \n* Passing the `linux-integration` tests is not required for PRs because of token/access problems.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDiscordBM%2FDiscordBM","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDiscordBM%2FDiscordBM","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDiscordBM%2FDiscordBM/lists"}