Skip to content

Commit

Permalink
Adds support for auto-deferring responses (#14)
Browse files Browse the repository at this point in the history
* fix: wrong function name

* add: better and generalized response interface

* add: auto-defer command responses

* fix: readme

* fix: missing delegate for NexusMiddlewareEventCore

* fix: ephemeral for deferred in auto-defer not working

* add: additional measure against random defers whilst scheduled defer
  • Loading branch information
ShindouMihou authored Feb 24, 2023
1 parent 8c06dea commit 8ac5933
Show file tree
Hide file tree
Showing 18 changed files with 523 additions and 599 deletions.
76 changes: 71 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ one Discord bot per application.
4. [Deferred Middleware Responses](#-deferred-middleware-responses)
5. [Basic Subcommand Router](#-basic-subcommand-router)
6. [Option Validation](#-option-validation)
7. [Command Synchronizations](#-command-synchronizations)
7. [Auto-deferring Responses](#-auto-deferring-responses)
8. [Command Synchronizations](#-command-synchronizations)

#### 💭 Preparation

Expand Down Expand Up @@ -74,6 +75,8 @@ DiscordApiBuilder()
> Before we start, a fair warning, it is never recommended to use the `event.interaction.respondLater()` methods of Javacord when using
> Nexus because we have special handling for middlewares that requires coordination between the command and the middleware. It is better
> to use `event.respondLater()` or `event.respondLaterAsEphemeral()` instead.
>
> You can even use auto-deferring instead which handles these cases for you, read more at [Auto-deferring Responses](#-auto-deferring-responses)
Nexus offers a simple, and straightforward manner of designing commands, but before we can continue designing commands, let us first understand a few
fundamental rules that Nexus enforces:
Expand Down Expand Up @@ -107,7 +110,16 @@ object PingCommand: NexusHandler {

override fun onEvent(event: NexusCommandEvent) {
val server = event.server.orElseThrow()
event.respondNowWith("Hello ${server.name}!")
// There are two ways to respond in Nexus, one is auto-deferring and the other is manual response.
// the example shown here demonstrates auto-defer responses.
event.autoDefer(ephemeral = false) {
return@autoDefer NexusMessage.with {
setContent("Hello ${server.name}")
}
}

// The example below demonstrates manual response wherein it is up to you to manually respond or not.
// event.respondNowWith("Hello ${server.name}!")
}
}
```
Expand Down Expand Up @@ -176,16 +188,17 @@ You can even configure more properties such as whether to make the deferred resp
defer by setting either of the properties:
```kotlin
// Default values
Nexus.configuration.interceptors.autoDeferMiddlewaresInMilliseconds = 2500
Nexus.configuration.global.autoDeferAfterMilliseconds = 2350
Nexus.configuration.interceptors.autoDeferAsEphemeral = true
```

> **Warning**
>
> As stated above, it is your responsibility to use deferred responses in the commands after enabling this. Nexus
> will not defer your command responses automatically, you should use methods such as `event.respondLater()` or `event.respondLaterAsEphemeral()`
> to handle these cases. Although, these methods may return non-ephemeral or ephemeral depending on the `autoDeferAsEphemeral`
> value or depending on how you deferred it manually.
> to handle these cases, otherwise, you can have Nexus auto-defer for you.
>
> To learn more about auto-deferring responses, you can read [Auto-deferring Responses](#-auto-deferring-responses).
#### 💭 Interceptor Repositories

Expand Down Expand Up @@ -269,6 +282,59 @@ but it suits most developers' needs and prevents a lot of potential code duplica
To learn more about how to use the option validation, you can check our example:
- [Option Validators](examples/option_validators)

#### 💭 Auto-deferring Responses

Nexus supports auto-deferring of responses in both middlewares and commands, but before that, we have to understand a
thing with slash commands and Nexus, and that is the three-second response requirement before defer. In Nexus, there are two core
features that can haggle up that three-second response requirement and that are:
1. Middlewares
2. Your actual command itself

And to solve an issue where the developer does not know which feature exactly causes an auto-defer, Nexus introduces auto-deferring, but
it requires you to enable the feature on both middlewares and the command itself. To enable auto-defer in middlewares, you can check
the [Deferred Middleware Responses](#-deferred-middleware-responses) section.

To enable auto-deferring in commands themselves, you have to use the `event.autoDefer(ephemeral, function)` method instead of the
other related methods. It is recommended to actually use this especially when you have long-running middlewares because this will
also take care of handling when a middleware actually requests for deferred response.

An example of how this looks is:
```kotlin
override fun onEvent(event: NexusCommandEvent) {
event.autoDefer(ephemeral = true) {
// ... imagine something long running task
return@autoDefer NexusMessage.with {
setContent("Hello!")
}
}
}
```

If you want to receive the response from Discord, it is possible by actually handling the response of the `autoDefer` method:
```kotlin
override fun onEvent(event: NexusCommandEvent) {
event.autoDefer(ephemeral = true) {
// ... imagine something long running task
return@autoDefer NexusMessage.with {
setContent("Hello!")
}
}.thenAccept { response ->
// Updater is only available if the message went through deferred response.
val updater = response.updater
// Using `getOrRequestMessage` actually calls `InteractionOriginalResponseUpdater.update()` if the interaction
// answered non-deferred since Javacord or Discord does not offer getting the actual message immediately from
// the response.
val message = response.getOrRequestMessage()
}
}
```

To configure when to defer, you can configure the following setting:
```kotlin
// Default values
Nexus.configuration.global.autoDeferAfterMilliseconds = 2350
```

#### 💭 Command Synchronizations

Nexus brings together a simple and straightforward method of synchronization for commands. To synchronize commands, all you
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/pw/mihou/nexus/Nexus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ object Nexus: SlashCommandCreateListener, ButtonClickListener {
@JvmStatic
val synchronizer = NexusSynchronizer()

@get:JvmSynthetic
internal val launch get() = configuration.launch

@JvmStatic
internal val launcher get() = configuration.launch.launcher

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package pw.mihou.nexus.configuration.modules

import pw.mihou.nexus.features.command.facade.NexusCommandEvent
import pw.mihou.nexus.features.messages.facade.NexusMessage
import pw.mihou.nexus.features.messages.NexusMessage

class NexusCommonsInterceptorsMessageConfiguration internal constructor() {

@set:JvmName("setRatelimitedMessage")
@get:JvmName("getRatelimitedMessage")
@Volatile
var ratelimited: (event: NexusCommandEvent, remainingSeconds: Long) -> NexusMessage = { _, remainingSeconds ->
NexusMessage.fromEphemereal(
"**SLOW DOWN***!"
+ "\n"
+ "You are executing commands too fast, please try again in $remainingSeconds seconds."
)
NexusMessage.with(true) {
this.setContent("***SLOW DOWN***!\nYou are executing commands too fast, please try again in $remainingSeconds seconds.")
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ class NexusGlobalConfiguration internal constructor() {
* whether they have a local inheritance class.
*/
@JvmField @Volatile var inheritance: Any? = null

/**
* When in an automatic defer situation, the framework will automatically defer the response when the time has
* surpassed the specified amount. You can specify this to any value less than 3,000 but the default value should
* be more than enough even when considering network latencies.
*/
@JvmField @Volatile var autoDeferAfterMilliseconds: Long = 2350
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ package pw.mihou.nexus.configuration.modules

class NexusInterceptorsConfiguration internal constructor() {
/**
* Sets whether to defer middleware responses when the middlewares have reached the
* [autoDeferMiddlewaresInMilliseconds] milliseconds.
* Sets whether to defer middleware responses when the middlewares have reached processing time beyond
* [NexusGlobalConfiguration.autoDeferAfterMilliseconds].
* This is only possible in middlewares because middlewares uses a custom method of responding.
*
* WARNING: You have to write your command to also use deferred responses. It is solely your responsibility to
* ensure that whichever commands uses middlewares that would take longer than the specified [autoDeferMiddlewaresInMilliseconds]
* ensure that whichever commands uses middlewares that would take longer than the specified [NexusGlobalConfiguration.autoDeferAfterMilliseconds]
* has to use deferred responses.
*/
@Volatile
@set:JvmName("setAutoDeferMiddlewareResponses")
@get:JvmName("autoDeferMiddlewareResponses")
var autoDeferMiddlewareResponses = false

@Volatile
@set:JvmName("setAutoDeferMiddlewaresInMilliseconds")
@get:JvmName("autoDeferMiddlewaresInMilliseconds")
var autoDeferMiddlewaresInMilliseconds = 2500L

@Volatile
@set:JvmName("setAutoDeferAsEphemeral")
@get:JvmName("autoDeferAsEphemeral")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package pw.mihou.nexus.configuration.modules

import pw.mihou.nexus.core.threadpool.NexusThreadPool
import java.util.concurrent.TimeUnit

class NexusLaunchConfiguration internal constructor() {
@JvmField var launcher: NexusLaunchWrapper = NexusLaunchWrapper { task ->
NexusThreadPool.executorService.submit { task.run() }
}
@JvmField var scheduler: NexusScheduledLaunchWrapper = NexusScheduledLaunchWrapper { timeInMillis, task ->
NexusThreadPool.scheduledExecutorService.schedule(task::run, timeInMillis, TimeUnit.MILLISECONDS)
}
}

fun interface NexusLaunchWrapper {
fun launch(task: NexusLaunchTask)
}

fun interface NexusScheduledLaunchWrapper {
fun launch(timeInMillis: Long, task: NexusLaunchTask)
}

fun interface NexusLaunchTask {
fun run()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import pw.mihou.nexus.features.command.interceptors.core.NexusCommandInterceptor
import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore
import pw.mihou.nexus.features.command.validation.OptionValidation
import pw.mihou.nexus.features.command.validation.result.ValidationResult
import pw.mihou.nexus.features.messages.core.NexusMessageCore
import pw.mihou.nexus.features.messages.NexusMessage
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
Expand Down Expand Up @@ -41,28 +42,29 @@ object NexusCommandDispatcher {
val future = CompletableFuture.supplyAsync {
NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.middlewares(middlewares))
}
NexusThreadPool.scheduledExecutorService.schedule({
val timeUntil = Instant.now().toEpochMilli() -
event.interaction.creationTimestamp.minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds).toEpochMilli()
Nexus.launch.scheduler.launch(timeUntil) {
if (future.isDone) {
return@schedule
return@launch
}
nexusEvent.respondLaterAsEphemeralIf(Nexus.configuration.interceptors.autoDeferAsEphemeral)
.exceptionally(ExceptionLogger.get())
}, Nexus.configuration.interceptors.autoDeferMiddlewaresInMilliseconds, TimeUnit.MILLISECONDS)
}
future.join()
} else {
NexusCommandInterceptorCore.execute(nexusEvent, NexusCommandInterceptorCore.middlewares(middlewares))
}

if (middlewareGate != null) {
val middlewareResponse = middlewareGate.response() as NexusMessageCore?
val middlewareResponse = middlewareGate.response()
if (middlewareResponse != null) {
val updaterFuture = nexusEvent.updater.get()
if (updaterFuture != null) {
val updater = updaterFuture.join()
(middlewareResponse.convertTo(updater) as InteractionOriginalResponseUpdater).update()
.exceptionally(ExceptionLogger.get())
middlewareResponse.into(updater).update().exceptionally(ExceptionLogger.get())
} else {
middlewareResponse.convertTo(nexusEvent.respondNow()).respond().exceptionally(ExceptionLogger.get())
middlewareResponse.into(nexusEvent.respondNow()).respond().exceptionally(ExceptionLogger.get())
}
}
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package pw.mihou.nexus.features.command.core

import org.javacord.api.entity.message.MessageFlag
import org.javacord.api.event.interaction.SlashCommandCreateEvent
import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater
import org.javacord.api.util.logging.ExceptionLogger
import pw.mihou.nexus.Nexus
import pw.mihou.nexus.features.command.facade.NexusCommand
import pw.mihou.nexus.features.command.facade.NexusCommandEvent
import pw.mihou.nexus.features.command.responses.NexusAutoResponse
import pw.mihou.nexus.features.messages.NexusMessage
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Function

class NexusCommandEventCore(private val event: SlashCommandCreateEvent, private val command: NexusCommand) : NexusCommandEvent {
private val store: Map<String, Any> = HashMap()
Expand All @@ -14,6 +21,49 @@ class NexusCommandEventCore(private val event: SlashCommandCreateEvent, private
override fun getBaseEvent() = event
override fun getCommand() = command
override fun store() = store
override fun autoDefer(ephemeral: Boolean, response: Function<Void?, NexusMessage>): CompletableFuture<NexusAutoResponse> {
if (updater.get() == null) {
val timeUntil = Instant.now().toEpochMilli() - event.interaction.creationTimestamp
.minusMillis(Nexus.configuration.global.autoDeferAfterMilliseconds)
.toEpochMilli()

Nexus.launch.scheduler.launch(timeUntil) {
if (updater.get() == null) {
respondLaterAsEphemeralIf(ephemeral).exceptionally(ExceptionLogger.get())
}
}
}
val future = CompletableFuture<NexusAutoResponse>()
Nexus.launcher.launch {
try {
val message = response.apply(null)
val updater = updater.get()
if (updater == null) {
val responder = respondNow()
if (ephemeral) {
responder.setFlags(MessageFlag.EPHEMERAL)
}
message.into(responder).respond()
.thenAccept { r -> future.complete(NexusAutoResponse(r, null)) }
.exceptionally { exception ->
future.completeExceptionally(exception)
return@exceptionally null
}
} else {
val completedUpdater = updater.join()
message.into(completedUpdater).update()
.thenAccept { r -> future.complete(NexusAutoResponse(null, r)) }
.exceptionally { exception ->
future.completeExceptionally(exception)
return@exceptionally null
}
}
} catch (exception: Exception) {
future.completeExceptionally(exception)
}
}
return future
}

override fun respondLater(): CompletableFuture<InteractionOriginalResponseUpdater> {
return updater.updateAndGet { interaction.respondLater() }!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import pw.mihou.nexus.features.command.facade.NexusCommand
import pw.mihou.nexus.features.command.facade.NexusCommandEvent
import pw.mihou.nexus.features.command.facade.NexusMiddlewareEvent
import pw.mihou.nexus.features.command.interceptors.core.NexusMiddlewareGateCore
import pw.mihou.nexus.features.messages.facade.NexusMessage
import pw.mihou.nexus.features.command.responses.NexusAutoResponse
import pw.mihou.nexus.features.messages.NexusMessage
import java.util.concurrent.CompletableFuture
import java.util.function.Function

class NexusMiddlewareEventCore(val event: NexusCommandEvent, private val gate: NexusMiddlewareGateCore): NexusMiddlewareEvent {
override fun getBaseEvent(): SlashCommandCreateEvent = event.baseEvent
Expand All @@ -16,6 +18,9 @@ class NexusMiddlewareEventCore(val event: NexusCommandEvent, private val gate: N

override fun respondLaterAsEphemeral(): CompletableFuture<InteractionOriginalResponseUpdater> = event.respondLaterAsEphemeral()
override fun store(): MutableMap<String, Any> = event.store()
override fun autoDefer(ephemeral: Boolean, response: Function<Void, NexusMessage>): CompletableFuture<NexusAutoResponse> {
return event.autoDefer(ephemeral, response)
}

override fun next() {
gate.next()
Expand Down
Loading

0 comments on commit 8ac5933

Please sign in to comment.