Skip to content

Commit

Permalink
React-to-Role (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
MahdiBM committed Feb 12, 2023
1 parent 9de2566 commit 4dda552
Show file tree
Hide file tree
Showing 32 changed files with 3,414 additions and 285 deletions.
84 changes: 67 additions & 17 deletions README.md
Expand Up @@ -84,8 +84,8 @@ let bot = BotGatewayManager(
/// Make an instance like above
let bot: BotGatewayManager = ...

/// Add event handlers
Task {
/// Add event handlers
await bot.addEventHandler { event in
switch event.data {
case let .messageCreate(message):
Expand Down Expand Up @@ -201,19 +201,18 @@ import Logging

/// Configure the Discord Logging Manager.
DiscordGlobalConfiguration.logManager = DiscordLogManager(
httpClient: HTTP_CLIENT_YOU_MADE_IN_PREVIOUS_STEPS,
configuration: .init(fallbackLogger: Logger(
label: "DiscordBMFallback",
factory: StreamLogHandler.standardOutput(label:metadataProvider:)
))
httpClient: HTTP_CLIENT_YOU_MADE_IN_PREVIOUS_STEPS
)

/// Bootstrap the `LoggingSystem`. After this, all your `Logger`s will automagically start using `DiscordLogHandler`.
LoggingSystem.bootstrapWithDiscordLogger(
/// The address to send the logs to. You can easily create a webhook using Discord client apps.
address: try .url(WEBHOOK_URL),
makeMainLogHandler: StreamLogHandler.standardOutput(label:metadataProvider:)
)
Task {
/// Bootstrap the `LoggingSystem`. After this, all your `Logger`s will automagically start using `DiscordLogHandler`.
await LoggingSystem.bootstrapWithDiscordLogger(
/// The address to send the logs to.
/// You can easily create a webhook using Discord client apps.
address: try .url(WEBHOOK_URL),
makeMainLogHandler: StreamLogHandler.standardOutput(label:metadataProvider:)
)
}
/// Make sure you haven't called `LoggingSystem.bootstrap` anywhere else, because you can only call it once.
/// For example Vapor's templates use `LoggingSystem.bootstrap` on boot, and you need to remove that.
```
Expand All @@ -225,10 +224,6 @@ Read `DiscordLogManager.Configuration.init` documentation for full info.
DiscordGlobalConfiguration.logManager = DiscordLogManager(
httpClient: HTTP_CLIENT_YOU_MADE_IN_PREVIOUS_STEPS,
configuration: .init(
fallbackLogger: Logger(
label: "DiscordBMFallback",
factory: StreamLogHandler.standardOutput(label:metadataProvider:)
),
aliveNotice: .init(
address: try .url(WEBHOOK_URL),
/// If nil, DiscordLogger will only send 1 "I'm alive" notice, on boot.
Expand All @@ -244,7 +239,7 @@ DiscordGlobalConfiguration.logManager = DiscordLogManager(
.critical: .role("970723029262942248"),
],
extraMetadata: [.warning, .error, .critical],
disabledLogLevels: [.debug, .trace],
disabledLogLevels: [.debug, .trace],
disabledInDebug: true
)
)
Expand Down Expand Up @@ -301,6 +296,61 @@ print("Guild name is:", aGuild.name)

</details>

### React-To-Role
<details>
<summary> Click to expand </summary>

`DiscordBM` can automatically assign a role to members when they react to a message with specific emojis:

```swift
let handler = try await ReactToRoleHandler(
gatewayManager: GatewayManager_YOU_MADE_IN_PREVIOUS_STEPS,
/// Your DiscordCache. This is not necessary (you can pass `nil`)
/// Only helpful if the cache has `guilds` and/or `guildMembers` intents enabled
cache: cache,
/// The role-creation payload
role: .init(
name: "cool-gang",
color: .green
),
guildId: THE_GUILD_ID_OF_THE_MESSAGE_YOU_CREATED,
channelId: THE_CHANNEL_ID_OF_THE_MESSAGE_YOU_CREATED,
messageId: THE_MESSAGE_ID_OF_THE_MESSAGE_YOU_CREATED,
/// The list of reactions to get the role for
reactions: [.unicodeEmoji("🐔")]
)
```

After this, anyone reacting with `🐔` to the message will be assigned the role.
There are a bunch more options, take a look at the other `ReactToRoleHandler` initializers for more info.

#### Behavior
The handler will:
* Verify the message exists at all, and throws an error in the initializer if not.
* React to the message as the bot-user with all the reactions you specified.
* Re-create the role if it's removed or doesn't exist.
* Stop working if you use `await handler.stop()`.
* Re-start working again if you use `try await handler.start()`.

#### Persistence
If you need to persist the handler somewhere:
* You only need to persist handler's `configuration`, which is `Codable`.
* You need to update the configuration you saved, whenever it's changed.
To become notified of configuration changes, you should use the `onConfigurationChanged` parameter in initializers:

```swift
let handler = try await ReactToRoleHandler(
.
.
.
onConfigurationChanged: { configuration in
await saveToDatabase(configuration: configuration)
}
)
```

</details>

## Testability
<details>
<summary> Click to expand </summary>
Expand Down
33 changes: 28 additions & 5 deletions Sources/DiscordClient/DefaultDiscordClient.swift
Expand Up @@ -59,8 +59,31 @@ public struct DefaultDiscordClient: DiscordClient {
}

func checkRateLimitsAllowRequest(to endpoint: Endpoint) async throws {
if await !rateLimiter.shouldRequest(to: endpoint) {
switch await rateLimiter.shouldRequest(to: endpoint) {
case .true: return
case .false:
throw DiscordClientError.rateLimited(url: "\(endpoint.urlDescription)")
case let .after(after):
if let backoff = configuration.retryPolicy?.backoff,
case let .basedOnHeaders(maxAllowed, _, _) = backoff {
if let maxAllowed = maxAllowed {
if after <= maxAllowed {
logger.warning("HTTP bucket is exhausted. Will wait \(after) seconds before making the request")
let nanos = UInt64(after * 1_000_000_000)
try await Task.sleep(nanoseconds: nanos)
await rateLimiter.addGlobalRateLimitRecord()
} else {
throw DiscordClientError.rateLimited(url: "\(endpoint.urlDescription)")
}
} else {
logger.warning("HTTP bucket is exhausted. Will wait \(after) seconds before making the request")
let nanos = UInt64(after * 1_000_000_000)
try await Task.sleep(nanoseconds: nanos)
await rateLimiter.addGlobalRateLimitRecord()
}
} else {
throw DiscordClientError.rateLimited(url: "\(endpoint.urlDescription)")
}
}
}

Expand Down Expand Up @@ -450,12 +473,12 @@ public struct ClientConfiguration {
case basedOnHeaders(
maxAllowed: Double?,
retryIfGreater: Bool = false,
else: Backoff?
else: Backoff? = nil
)

public static var `default`: Backoff {
.basedOnHeaders(
maxAllowed: 10,
maxAllowed: 5,
retryIfGreater: false,
else: .exponential(base: 0.2, coefficient: 0.5, rate: 2, upToTimes: 10)
)
Expand Down Expand Up @@ -520,7 +543,7 @@ public struct ClientConfiguration {
/// Only retries status code 429, 500 and 502 once.
@inlinable
public static var `default`: RetryPolicy {
RetryPolicy(statuses: [.tooManyRequests, .internalServerError, .badGateway])
RetryPolicy()
}

/// - Parameters:
Expand All @@ -529,7 +552,7 @@ public struct ClientConfiguration {
/// - backoff: The backoff configuration, to wait a some amount of time
/// _after_ a failed request.
public init(
statuses: Set<HTTPResponseStatus>,
statuses: Set<HTTPResponseStatus> = [.tooManyRequests, .internalServerError, .badGateway],
maxRetries: Int = 1,
backoff: Backoff? = .default
) {
Expand Down

0 comments on commit 4dda552

Please sign in to comment.