diff --git a/core/src/main/kotlin/at/xirado/jdui/Context.kt b/core/src/main/kotlin/at/xirado/jdui/Context.kt index 948965e..d75a939 100644 --- a/core/src/main/kotlin/at/xirado/jdui/Context.kt +++ b/core/src/main/kotlin/at/xirado/jdui/Context.kt @@ -79,12 +79,15 @@ class Context( return value } + fun copy(includeParent: Boolean = true): Context { + val parent = if (includeParent) this.parent?.copy() else null + val newContext = Context(parent) + newContext.provideAll(this) + return newContext + } + companion object { - fun copyOf(other: Context) = Context().apply { - other.lock.read { - context.putAll(other.context) - } - } + fun copyOf(other: Context, includeParent: Boolean = true) = other.copy(includeParent) } } diff --git a/core/src/main/kotlin/at/xirado/jdui/JDUIListener.kt b/core/src/main/kotlin/at/xirado/jdui/JDUIListener.kt index f7c0fc5..15e9ae1 100644 --- a/core/src/main/kotlin/at/xirado/jdui/JDUIListener.kt +++ b/core/src/main/kotlin/at/xirado/jdui/JDUIListener.kt @@ -38,13 +38,10 @@ class JDUIListener(internal val config: JDUIConfig) : EventListener { // private val modalInteractionHandler = ModalInteractionHandler(this) override fun onEvent(event: GenericEvent) { - coroutineScope.launch { - when (event) { - is StatusChangeEvent -> handleStatusChange(event) - is ShutdownEvent -> handleShutdown(event) - is GenericComponentInteractionCreateEvent -> componentInteractionHandler.handleComponentEvent(event) -// is ModalInteractionEvent -> modalInteractionHandler.onModalEvent(event) - } + when (event) { + is StatusChangeEvent -> handleStatusChange(event) + is ShutdownEvent -> handleShutdown(event) + is GenericComponentInteractionCreateEvent -> componentInteractionHandler.handleComponentEvent(event) } } diff --git a/core/src/main/kotlin/at/xirado/jdui/component/Component.kt b/core/src/main/kotlin/at/xirado/jdui/component/Component.kt index ef7991f..a7c6d48 100644 --- a/core/src/main/kotlin/at/xirado/jdui/component/Component.kt +++ b/core/src/main/kotlin/at/xirado/jdui/component/Component.kt @@ -1,11 +1,14 @@ package at.xirado.jdui.component +import at.xirado.jdui.state.interaction.ViewComponentInteraction import at.xirado.jdui.view.ViewDSL import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent import kotlin.reflect.KClass import kotlin.reflect.KType import net.dv8tion.jda.api.components.Component as JDAComponent +typealias ComponentCallback = suspend ViewComponentInteraction.() -> Unit + @ViewDSL abstract class Component { internal abstract val type: KType @@ -19,11 +22,11 @@ abstract class StatelessComponent : Component() { internal abstract fun buildComponent(uniqueId: Int): T } -abstract class StatefulActionComponent : StatefulComponent() { - internal abstract val callback: suspend E.() -> Unit - internal abstract val callbackClazz: KClass +abstract class StatefulActionComponent : StatefulComponent() { + internal abstract val callback: suspend ViewComponentInteraction.() -> Unit + internal abstract val eventClazz: KClass - internal abstract suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) + internal abstract suspend fun processInteraction(interaction: ViewComponentInteraction) } abstract class ParentComponent : Component() { diff --git a/core/src/main/kotlin/at/xirado/jdui/component/message/Button.kt b/core/src/main/kotlin/at/xirado/jdui/component/message/Button.kt index 6cbaf13..209113c 100644 --- a/core/src/main/kotlin/at/xirado/jdui/component/message/Button.kt +++ b/core/src/main/kotlin/at/xirado/jdui/component/message/Button.kt @@ -1,20 +1,21 @@ package at.xirado.jdui.component.message +import at.xirado.jdui.component.ComponentCallback import at.xirado.jdui.component.StatefulActionComponent import at.xirado.jdui.component.StatelessComponent -import net.dv8tion.jda.api.components.button.ButtonStyle +import at.xirado.jdui.state.interaction.ViewComponentInteraction +import net.dv8tion.jda.api.components.buttons.ButtonStyle import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent -import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent import kotlin.reflect.typeOf -import net.dv8tion.jda.api.components.button.Button as JDAButton +import net.dv8tion.jda.api.components.buttons.Button as JDAButton class ActionButton( var style: ButtonStyle, var label: String?, var emoji: Emoji?, var disabled: Boolean, - override val callback: suspend ButtonInteractionEvent.() -> Unit, + override val callback: ComponentCallback, ) : StatefulActionComponent() { override fun buildComponent(id: String, uniqueId: Int): JDAButton { return JDAButton.of(style, id, label, emoji) @@ -22,12 +23,12 @@ class ActionButton( .withUniqueId(uniqueId) } - override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) { - callback(event as ButtonInteractionEvent) + override suspend fun processInteraction(interaction: ViewComponentInteraction) { + callback(interaction) } override val type = typeOf() - override val callbackClazz = ButtonInteractionEvent::class + override val eventClazz = ButtonInteractionEvent::class } class LinkButton( @@ -50,7 +51,7 @@ fun button( label: String? = null, emoji: Emoji? = null, disabled: Boolean = false, - callback: suspend ButtonInteractionEvent.() -> Unit, + callback: ComponentCallback, ): ActionButton { require(style != ButtonStyle.LINK) { "Cannot use ButtonStyle.LINK here. Use link() instead" } return ActionButton(style, label, emoji, disabled, callback) diff --git a/core/src/main/kotlin/at/xirado/jdui/component/message/SelectMenu.kt b/core/src/main/kotlin/at/xirado/jdui/component/message/SelectMenu.kt index 0d12e04..f4dd5a7 100644 --- a/core/src/main/kotlin/at/xirado/jdui/component/message/SelectMenu.kt +++ b/core/src/main/kotlin/at/xirado/jdui/component/message/SelectMenu.kt @@ -1,14 +1,15 @@ package at.xirado.jdui.component.message +import at.xirado.jdui.component.ComponentCallback import at.xirado.jdui.component.StatefulActionComponent -import net.dv8tion.jda.api.components.selects.SelectOption +import at.xirado.jdui.state.interaction.ViewComponentInteraction +import net.dv8tion.jda.api.components.selections.SelectOption import net.dv8tion.jda.api.entities.channel.ChannelType import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent -import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent import kotlin.reflect.typeOf -import net.dv8tion.jda.api.components.selects.EntitySelectMenu as JDAEntitySelectMenu -import net.dv8tion.jda.api.components.selects.StringSelectMenu as JDAStringSelectMenu +import net.dv8tion.jda.api.components.selections.EntitySelectMenu as JDAEntitySelectMenu +import net.dv8tion.jda.api.components.selections.StringSelectMenu as JDAStringSelectMenu private typealias StringCallback = suspend StringSelectInteractionEvent.() -> Unit private typealias EntityCallback = suspend EntitySelectInteractionEvent.() -> Unit @@ -18,7 +19,7 @@ class StringSelectMenu( var range: IntRange, var placeholder: String?, var disabled: Boolean, - override val callback: StringCallback + override val callback: ComponentCallback ) : StatefulActionComponent() { override fun buildComponent(id: String, uniqueId: Int): JDAStringSelectMenu { return JDAStringSelectMenu.create(id) @@ -30,12 +31,12 @@ class StringSelectMenu( .build() } - override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) { - callback(event as StringSelectInteractionEvent) + override suspend fun processInteraction(interaction: ViewComponentInteraction) { + callback(interaction) } override val type = typeOf() - override val callbackClazz = StringSelectInteractionEvent::class + override val eventClazz = StringSelectInteractionEvent::class } class EntitySelectMenu( @@ -44,7 +45,7 @@ class EntitySelectMenu( var range: IntRange, var placeholder: String?, var disabled: Boolean, - override val callback: EntityCallback, + override val callback: ComponentCallback, ) : StatefulActionComponent() { override fun buildComponent(id: String, uniqueId: Int): JDAEntitySelectMenu { return JDAEntitySelectMenu.create(id, targets) @@ -56,12 +57,12 @@ class EntitySelectMenu( .build() } - override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) { - callback(event as EntitySelectInteractionEvent) + override suspend fun processInteraction(interaction: ViewComponentInteraction) { + callback(interaction) } override val type = typeOf() - override val callbackClazz = EntitySelectInteractionEvent::class + override val eventClazz = EntitySelectInteractionEvent::class } fun stringSelect( @@ -69,7 +70,7 @@ fun stringSelect( range: IntRange = 1..1, placeholder: String? = null, disabled: Boolean = false, - callback: StringCallback + callback: ComponentCallback ): StringSelectMenu { return StringSelectMenu(options, range, placeholder, disabled, callback) } @@ -80,7 +81,7 @@ fun entitySelect( range: IntRange = 1..1, placeholder: String? = null, disabled: Boolean = false, - callback: EntityCallback + callback: ComponentCallback ) : EntitySelectMenu { return EntitySelectMenu(targets, channelTypes, range, placeholder, disabled, callback) } \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/component/message/TextDisplay.kt b/core/src/main/kotlin/at/xirado/jdui/component/message/TextDisplay.kt index 2857cca..f0e09e8 100644 --- a/core/src/main/kotlin/at/xirado/jdui/component/message/TextDisplay.kt +++ b/core/src/main/kotlin/at/xirado/jdui/component/message/TextDisplay.kt @@ -8,7 +8,7 @@ class TextDisplay( var content: String ) : StatelessComponent() { override fun buildComponent(uniqueId: Int): JDATextDisplay { - return JDATextDisplay.create(content).withUniqueId(uniqueId) + return JDATextDisplay.of(content).withUniqueId(uniqueId) } override val type = typeOf() diff --git a/core/src/main/kotlin/at/xirado/jdui/handler/ComponentInteractionHandler.kt b/core/src/main/kotlin/at/xirado/jdui/handler/ComponentInteractionHandler.kt index 2d60cd8..01ba006 100644 --- a/core/src/main/kotlin/at/xirado/jdui/handler/ComponentInteractionHandler.kt +++ b/core/src/main/kotlin/at/xirado/jdui/handler/ComponentInteractionHandler.kt @@ -1,129 +1,25 @@ package at.xirado.jdui.handler import at.xirado.jdui.JDUIListener -import at.xirado.jdui.component.message.container -import at.xirado.jdui.component.message.text -import at.xirado.jdui.crypto.decryptChaCha -import at.xirado.jdui.state.ViewState -import at.xirado.jdui.state.createViewState -import at.xirado.jdui.utils.decode -import at.xirado.jdui.utils.mergeCustomIds -import at.xirado.jdui.view.definition.function.view -import at.xirado.jdui.view.metadata.EncryptedViewStateMetadata -import at.xirado.jdui.view.populateMessageContext -import at.xirado.jdui.view.replyView -import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.sync.withLock -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.protobuf.ProtoBuf +import at.xirado.jdui.state.interaction.ViewComponentInteraction +import kotlinx.coroutines.launch +import net.dv8tion.jda.api.entities.Message import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent -import net.dv8tion.jda.api.utils.messages.MessageCreateData -import net.dv8tion.jda.api.utils.messages.MessageEditData -private val log = KotlinLogging.logger { } +private val componentsV2Flag = Message.MessageFlag.IS_COMPONENTS_V2.value.toLong() internal class ComponentInteractionHandler(private val jdui: JDUIListener) { - private val config = jdui.config + private val coroutineScope = jdui.coroutineScope - suspend fun handleComponentEvent(event: GenericComponentInteractionCreateEvent) { - val id = event.message.components.mergeCustomIds() + fun handleComponentEvent(event: GenericComponentInteractionCreateEvent) { + if ((event.message.flagsRaw and componentsV2Flag) == 0L) + return - when { - id.startsWith("j1:") -> handleStatefulView(id, event) - id.startsWith("j2:") -> handleStatelessView(id, event) - else -> return - } - } - - @OptIn(ExperimentalSerializationApi::class) - private suspend fun handleStatefulView(componentId: String, event: GenericComponentInteractionCreateEvent) { - val data = componentId.substringAfter("j1:") - - val decodedData = decode(data) - - val metadataEncrypted: EncryptedViewStateMetadata = ProtoBuf.decodeFromByteArray(decodedData) - - val secret = config.secret - val metadata = metadataEncrypted.decrypt(secret) - val id = metadata.id - - log.debug { "Handling component interaction for view $id" } - val cachedState = jdui.messageCache.getIfPresent(id) - - if (cachedState != null) { - val message = updateMessage(event, cachedState) - return event.editMessage(MessageEditData.fromCreateData(message)) - .populateMessageContext(cachedState.messageContext) - .queue() - } - - if (metadata.metadata.sourceData == null) { - return event.replyView(view { - compose { - +container(accentColor = 0xFF0000) { - +text("This action timed out!") - } - } - }, ephemeral = true).queue() - } - - val state = createViewState(jdui, metadata) - - val message = updateMessage(event, state) - event.editMessage(MessageEditData.fromCreateData(message)) - .populateMessageContext(state.messageContext) - .queue() - } - - @OptIn(ExperimentalSerializationApi::class) - private suspend fun handleStatelessView(componentId: String, event: GenericComponentInteractionCreateEvent) { - val id = componentId.substringAfter("j2:").toLong() - val cachedState = jdui.messageCache.getIfPresent(id) - - log.debug { "Handling component interaction for view $id" } - if (cachedState != null) { - val message = updateMessage(event, cachedState) - return event.editMessage(MessageEditData.fromCreateData(message)) - .populateMessageContext(cachedState.messageContext) - .queue() - } - - log.debug { "Getting state from db: $id" } - - val persistence = jdui.config.persistenceConfig - ?: throw IllegalStateException("No PersistenceConfig was provided!") - - val retrievedState = persistence.retrieveState(id) - ?: throw IllegalStateException("No such view with id $id") - - val secret = config.secret - val encryptedMetadata: EncryptedViewStateMetadata = ProtoBuf.decodeFromByteArray(retrievedState.data) - - val metadata = encryptedMetadata.decrypt(secret) - - if (metadata.metadata.sourceData == null) { - return event.replyView(view { - compose { - +container(accentColor = 0xFF0000) { - +text("This action timed out!") - } - } - }, ephemeral = true).queue() - } - - val state = createViewState(jdui, metadata) - val message = updateMessage(event, state) - - event.editMessage(MessageEditData.fromCreateData(message)) - .populateMessageContext(state.messageContext) - .queue() - } + val now = System.currentTimeMillis() - private suspend fun updateMessage(event: GenericComponentInteractionCreateEvent, state: ViewState): MessageCreateData { - return state.mutex.withLock { - state.handleComponentInteraction(event) - state.composeMessage() + coroutineScope.launch { + val interaction = ViewComponentInteraction.fromEvent(jdui, event, now) + interaction?.process() } } } \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/state/UserStateCollection.kt b/core/src/main/kotlin/at/xirado/jdui/state/UserStateCollection.kt index 9fbc23b..85aba4d 100644 --- a/core/src/main/kotlin/at/xirado/jdui/state/UserStateCollection.kt +++ b/core/src/main/kotlin/at/xirado/jdui/state/UserStateCollection.kt @@ -21,7 +21,7 @@ internal class UserStateCollection( return if (index > unpackedUserState.lastIndex) { - userState[property] = property.default + userState[property] = property.default() return } diff --git a/core/src/main/kotlin/at/xirado/jdui/state/UserStateProperty.kt b/core/src/main/kotlin/at/xirado/jdui/state/UserStateProperty.kt index fff3dd3..34e3c69 100644 --- a/core/src/main/kotlin/at/xirado/jdui/state/UserStateProperty.kt +++ b/core/src/main/kotlin/at/xirado/jdui/state/UserStateProperty.kt @@ -7,7 +7,7 @@ import kotlin.reflect.KProperty class UserStateProperty internal constructor( internal val index: Int, internal val property: KProperty, - internal val default: T, + internal val default: () -> T, internal val state: ViewState, ): ReadWriteProperty { internal val serializer = serializer(property.returnType) diff --git a/core/src/main/kotlin/at/xirado/jdui/state/ViewState.kt b/core/src/main/kotlin/at/xirado/jdui/state/ViewState.kt index f9a27a5..ce25100 100644 --- a/core/src/main/kotlin/at/xirado/jdui/state/ViewState.kt +++ b/core/src/main/kotlin/at/xirado/jdui/state/ViewState.kt @@ -4,19 +4,15 @@ import at.xirado.jdui.Context import at.xirado.jdui.JDUIListener import at.xirado.jdui.component.* import at.xirado.jdui.config.ViewData -import at.xirado.jdui.crypto.encryptChaCha import at.xirado.jdui.utils.encode import at.xirado.jdui.utils.splitIntoParts -import at.xirado.jdui.utils.toBytes import at.xirado.jdui.view.definition.ViewDefinition import at.xirado.jdui.view.metadata.* -import at.xirado.jdui.view.metadata.EncryptedViewStateMetadata -import at.xirado.jdui.view.metadata.MessageContext -import at.xirado.jdui.view.metadata.ViewStateMetadata import at.xirado.jdui.view.metadata.source.ClassViewSourceData import at.xirado.jdui.view.metadata.source.FunctionViewSourceData import at.xirado.jdui.view.metadata.source.ViewSourceCache import at.xirado.jdui.view.metadata.source.ViewSourceData +import at.xirado.jdui.view.middleware.ViewMiddleware import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -41,7 +37,8 @@ class ViewState internal constructor( internal var componentIndex: ComponentIndex? = null internal var mutex = Mutex() internal val context = Context(listener.context) - internal val messageContext = MessageContext(WeakReference(listener.jda)) + internal val messageContext = MessageContext(this) + internal val middleware: MutableList = mutableListOf() private var userState = metadata.metadata.sourceData?.let { if (supportUserState) UserStateCollection(metadata.metadata.userState) else null @@ -164,7 +161,8 @@ class ViewState internal constructor( return userState.setUserState(property, value) } - internal suspend fun handleComponentInteraction(event: GenericComponentInteractionCreateEvent) { + @Suppress("UNCHECKED_CAST") + internal suspend fun getActionComponentFromEvent(event: E): StatefulActionComponent<*, E> { val uniqueId = event.uniqueId val componentIndex = getComponentIndex() @@ -175,14 +173,32 @@ class ViewState internal constructor( throw IllegalStateException("Component is not an action component. Broken view state?") val thisEventClazz = event::class - val componentEventClazz = component.callbackClazz + val componentEventClazz = component.eventClazz if (thisEventClazz != componentEventClazz) throw IllegalStateException("Expected event of type $componentEventClazz but got $thisEventClazz. Broken view state?") - component.processInteraction(event) + return component as StatefulActionComponent<*, E> } -} + + override fun toString() = metadata.toString() +} + +internal suspend fun getViewStateByDiscordCustomIds(jdui: JDUIListener, mergedCustomIds: String): ViewState? { + return if (mergedCustomIds.startsWith("j2:")) { + val id = mergedCustomIds.removePrefix("j2:").toLong() + jdui.messageCache.getIfPresent(id)?.let { return it } + val metadata = retrieveViewMetadataFromDb(jdui, id) + createViewState(jdui, metadata.decrypt(jdui.config.secret)) + } else if (mergedCustomIds.startsWith("j1:")) { + val data = mergedCustomIds.removePrefix("j1:") + val encryptedMetadata = decodeViewState(data) + jdui.messageCache.getIfPresent(encryptedMetadata.id)?.let { return it } + createViewState(jdui, encryptedMetadata.decrypt(jdui.config.secret)) + } else { + null + } +} internal suspend fun createViewState( jdui: JDUIListener, @@ -191,7 +207,7 @@ internal suspend fun createViewState( context: Context? = null, ): ViewState { val sourceData = metadata.metadata.sourceData - ?: throw IllegalStateException("Metadata does not contain SourceData!") + ?: throw IllegalStateException("Cannot create view state with no sourceData!") val source = ViewSourceCache.getViewSource(sourceData) val definition = source.create() diff --git a/core/src/main/kotlin/at/xirado/jdui/state/ViewUserState.kt b/core/src/main/kotlin/at/xirado/jdui/state/ViewUserState.kt index 69ddfeb..9a41a80 100644 --- a/core/src/main/kotlin/at/xirado/jdui/state/ViewUserState.kt +++ b/core/src/main/kotlin/at/xirado/jdui/state/ViewUserState.kt @@ -5,7 +5,7 @@ import at.xirado.jdui.view.definition.ViewDefinition import kotlin.properties.PropertyDelegateProvider import kotlin.reflect.KProperty -fun ViewDefinition.state(default: T): PropertyDelegateProvider> { +fun ViewDefinition.state(default: () -> T): PropertyDelegateProvider> { val viewState = this.state val userState = this.userState @@ -20,6 +20,6 @@ fun ViewDefinition.state(default: T): PropertyDelegateProvider View.state(default: T): PropertyDelegateProvider> { +fun View.state(default: () -> T): PropertyDelegateProvider> { return withDefinition { it.state(default) } } \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/state/interaction/ComponentCallbackResult.kt b/core/src/main/kotlin/at/xirado/jdui/state/interaction/ComponentCallbackResult.kt new file mode 100644 index 0000000..1639e09 --- /dev/null +++ b/core/src/main/kotlin/at/xirado/jdui/state/interaction/ComponentCallbackResult.kt @@ -0,0 +1,13 @@ +package at.xirado.jdui.state.interaction + +import net.dv8tion.jda.api.utils.messages.MessageCreateData + +sealed interface ComponentCallbackResult + +// Update the message (default behaviour) +object UpdateMessage : ComponentCallbackResult +// Do not respond to the interaction +object DoNothing : ComponentCallbackResult + +class SendFollowup(val function: suspend () -> MessageCreateData, val ephemeral: Boolean) : ComponentCallbackResult + diff --git a/core/src/main/kotlin/at/xirado/jdui/state/interaction/ViewComponentInteraction.kt b/core/src/main/kotlin/at/xirado/jdui/state/interaction/ViewComponentInteraction.kt new file mode 100644 index 0000000..5244671 --- /dev/null +++ b/core/src/main/kotlin/at/xirado/jdui/state/interaction/ViewComponentInteraction.kt @@ -0,0 +1,118 @@ +package at.xirado.jdui.state.interaction + +import at.xirado.jdui.Context +import at.xirado.jdui.JDUIListener +import at.xirado.jdui.component.StatefulActionComponent +import at.xirado.jdui.state.ViewState +import at.xirado.jdui.state.getViewStateByDiscordCustomIds +import at.xirado.jdui.utils.await +import at.xirado.jdui.utils.tryMergeIds +import at.xirado.jdui.view.ViewDSL +import at.xirado.jdui.view.createFunctionViewState +import at.xirado.jdui.view.definition.function.ViewDefinitionFunction +import at.xirado.jdui.view.populateMessageContext +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock +import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent +import net.dv8tion.jda.api.utils.messages.MessageCreateData +import net.dv8tion.jda.api.utils.messages.MessageEditData +import kotlin.reflect.KFunction + +internal val allowedCustomIdPrefixes = setOf("j1:", "j2:") + +@ViewDSL +class ViewComponentInteraction private constructor( + internal val state: ViewState, + val event: E, + internal val receivedTimestamp: Long, +) { + private val mutex = state.mutex + private var isInitialized = false + + internal var result: ComponentCallbackResult = UpdateMessage + + private suspend fun initialize(): StatefulActionComponent<*, E> { + val component = getComponentFromEvent() + isInitialized = true + return component + } + + internal suspend fun process() = mutex.withLock { + if (isInitialized) + throw IllegalStateException("Can only call process() once!") + + val middlewares = state.middleware + + for (middleware in middlewares) { + if (!middleware.processEvent(event)) + return@withLock + } + + val component = initialize() + + state.messageContext.provideInteractionHook(event.hook) + + component.processInteraction(this) + val result = this.result + + when (result) { + is UpdateMessage -> { + val message = state.composeMessage() + event.editMessage(MessageEditData.fromCreateData(message)) + .populateMessageContext(state.messageContext) + .queue() + } + is SendFollowup -> { + val ephemeral = result.ephemeral + event.deferReply(ephemeral).queue() + state.coroutineScope.launch { + val function = result.function + val messageData = function() + event.hook.sendMessage(messageData).await() + } + } + is DoNothing -> {} + else -> TODO("Unsupported") + } + } + + fun doNothing() { + result = DoNothing + } + + private suspend fun getComponentFromEvent() = state.getActionComponentFromEvent(event) + + internal companion object { + internal suspend fun fromEvent( + jdui: JDUIListener, + event: E, + receivedTimestamp: Long, + ): ViewComponentInteraction? { + val id = event.message.components.tryMergeIds(allowedCustomIdPrefixes) + ?: return null + val state = getViewStateByDiscordCustomIds(jdui, id) ?: return null + + return ViewComponentInteraction(state, event, receivedTimestamp) + } + } +} + +fun ViewComponentInteraction<*>.sendFollowup( + function: KFunction, + ephemeral: Boolean = true, + inheritContext: Boolean = false, + context: Context? = null, +) { + val messageFunction: suspend () -> MessageCreateData = { + val stateContext = when { + inheritContext && context == null -> Context.copyOf(state.context) + inheritContext && context != null -> Context.copyOf(state.context).also { it.provideAll(context) } + else -> context + } + + val state = createFunctionViewState(event.jda, function, stateContext) + state.composeMessage() + } + + result = SendFollowup(messageFunction, ephemeral) +} diff --git a/core/src/main/kotlin/at/xirado/jdui/utils/Base65536.kt b/core/src/main/kotlin/at/xirado/jdui/utils/Base65536.kt index 356f618..6949644 100644 --- a/core/src/main/kotlin/at/xirado/jdui/utils/Base65536.kt +++ b/core/src/main/kotlin/at/xirado/jdui/utils/Base65536.kt @@ -10,7 +10,7 @@ private val B2: Map = mapOf( 5376 to -1, 13312 to 0, 13568 to 1, 13824 to 2, 14080 to 3, 14336 to 4, 14592 to 5, 14848 to 6, 15104 to 7, 15360 to 8, 15616 to 9, 15872 to 10, 16128 to 11, 16384 to 12, 16640 to 13, 16896 to 14, 17152 to 15, 17408 to 16, 17664 to 17, 17920 to 18, 18176 to 19, 18432 to 20, 18688 to 21, 18944 to 22, 19200 to 23, 19456 to 24, 19968 to 25, 20224 to 26, 20480 to 27, 20736 to 28, 20992 to 29, 21248 to 30, 21504 to 31, 21760 to 32, 22016 to 33, 22272 to 34, 22528 to 35, 22784 to 36, 23040 to 37, 23296 to 38, 23552 to 39, 23808 to 40, 24064 to 41, 24320 to 42, 24576 to 43, 24832 to 44, 25088 to 45, 25344 to 46, 25600 to 47, 25856 to 48, 26112 to 49, 26368 to 50, 26624 to 51, 26880 to 52, 27136 to 53, 27392 to 54, 27648 to 55, 27904 to 56, 28160 to 57, 28416 to 58, 28672 to 59, 28928 to 60, 29184 to 61, 29440 to 62, 29696 to 63, 29952 to 64, 30208 to 65, 30464 to 66, 30720 to 67, 30976 to 68, 31232 to 69, 31488 to 70, 31744 to 71, 32000 to 72, 32256 to 73, 32512 to 74, 32768 to 75, 33024 to 76, 33280 to 77, 33536 to 78, 33792 to 79, 34048 to 80, 34304 to 81, 34560 to 82, 34816 to 83, 35072 to 84, 35328 to 85, 35584 to 86, 35840 to 87, 36096 to 88, 36352 to 89, 36608 to 90, 36864 to 91, 37120 to 92, 37376 to 93, 37632 to 94, 37888 to 95, 38144 to 96, 38400 to 97, 38656 to 98, 38912 to 99, 39168 to 100, 39424 to 101, 39680 to 102, 39936 to 103, 40192 to 104, 40448 to 105, 41216 to 106, 41472 to 107, 41728 to 108, 42240 to 109, 67072 to 110, 73728 to 111, 73984 to 112, 74240 to 113, 77824 to 114, 78080 to 115, 78336 to 116, 78592 to 117, 82944 to 118, 83200 to 119, 92160 to 120, 92416 to 121, 131072 to 122, 131328 to 123, 131584 to 124, 131840 to 125, 132096 to 126, 132352 to 127, 132608 to 128, 132864 to 129, 133120 to 130, 133376 to 131, 133632 to 132, 133888 to 133, 134144 to 134, 134400 to 135, 134656 to 136, 134912 to 137, 135168 to 138, 135424 to 139, 135680 to 140, 135936 to 141, 136192 to 142, 136448 to 143, 136704 to 144, 136960 to 145, 137216 to 146, 137472 to 147, 137728 to 148, 137984 to 149, 138240 to 150, 138496 to 151, 138752 to 152, 139008 to 153, 139264 to 154, 139520 to 155, 139776 to 156, 140032 to 157, 140288 to 158, 140544 to 159, 140800 to 160, 141056 to 161, 141312 to 162, 141568 to 163, 141824 to 164, 142080 to 165, 142336 to 166, 142592 to 167, 142848 to 168, 143104 to 169, 143360 to 170, 143616 to 171, 143872 to 172, 144128 to 173, 144384 to 174, 144640 to 175, 144896 to 176, 145152 to 177, 145408 to 178, 145664 to 179, 145920 to 180, 146176 to 181, 146432 to 182, 146688 to 183, 146944 to 184, 147200 to 185, 147456 to 186, 147712 to 187, 147968 to 188, 148224 to 189, 148480 to 190, 148736 to 191, 148992 to 192, 149248 to 193, 149504 to 194, 149760 to 195, 150016 to 196, 150272 to 197, 150528 to 198, 150784 to 199, 151040 to 200, 151296 to 201, 151552 to 202, 151808 to 203, 152064 to 204, 152320 to 205, 152576 to 206, 152832 to 207, 153088 to 208, 153344 to 209, 153600 to 210, 153856 to 211, 154112 to 212, 154368 to 213, 154624 to 214, 154880 to 215, 155136 to 216, 155392 to 217, 155648 to 218, 155904 to 219, 156160 to 220, 156416 to 221, 156672 to 222, 156928 to 223, 157184 to 224, 157440 to 225, 157696 to 226, 157952 to 227, 158208 to 228, 158464 to 229, 158720 to 230, 158976 to 231, 159232 to 232, 159488 to 233, 159744 to 234, 160000 to 235, 160256 to 236, 160512 to 237, 160768 to 238, 161024 to 239, 161280 to 240, 161536 to 241, 161792 to 242, 162048 to 243, 162304 to 244, 162560 to 245, 162816 to 246, 163072 to 247, 163328 to 248, 163584 to 249, 163840 to 250, 164096 to 251, 164352 to 252, 164608 to 253, 164864 to 254, 165120 to 255 ) -fun encode(input: ByteArray): String { +internal fun encode(input: ByteArray): String { val writer = StringBuilder() val length = input.size var x = 0 @@ -25,22 +25,26 @@ fun encode(input: ByteArray): String { return writer.toString() } -fun decode(input: String): ByteArray { +internal fun decode(input: String): ByteArray { val outputStream = ByteArrayOutputStream() var done = false val codePoints = input.codePoints() for (codePoint in codePoints) { val b1 = codePoint and 0xFF - val b2 = B2[codePoint - b1] ?: throw IllegalArgumentException("Invalid base65536 code point: $codePoint") + val b2 = B2[codePoint - b1] ?: throw DecodeException("Invalid base65536 code point: $codePoint") val bytes = if (b2 == -1) byteArrayOf(b1.toByte()) else byteArrayOf(b1.toByte(), b2.toByte()) if (bytes.size == 1) { - if (done) { - throw IllegalArgumentException("base65536 sequence continued after final byte") - } + if (done) + throw DecodeException("base65536 sequence continued after final byte") done = true } outputStream.write(bytes) } return outputStream.toByteArray() -} \ No newline at end of file +} + +class DecodeException( + message: String, + cause: Throwable? = null, +) : Exception(message, cause) \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/utils/JDA.kt b/core/src/main/kotlin/at/xirado/jdui/utils/JDA.kt index 05147c2..1bd9775 100644 --- a/core/src/main/kotlin/at/xirado/jdui/utils/JDA.kt +++ b/core/src/main/kotlin/at/xirado/jdui/utils/JDA.kt @@ -3,26 +3,50 @@ package at.xirado.jdui.utils import net.dv8tion.jda.api.components.ActionComponent import net.dv8tion.jda.api.components.Component import net.dv8tion.jda.api.components.actionrow.ActionRow -import net.dv8tion.jda.api.components.button.Button import net.dv8tion.jda.api.components.container.Container import net.dv8tion.jda.api.components.section.Section -fun Collection.mergeCustomIds(): String { +fun Collection.tryMergeIds(allowedPrefixes: Collection): String? { val sb = StringBuilder() - forEach { component -> + val result = walkUntil { component -> + if (component !is ActionComponent) + return@walkUntil true + + val id = component.customId ?: return@walkUntil true + + if (sb.isEmpty() && allowedPrefixes.none { id.startsWith(it) }) + return@walkUntil false + + sb.append(id) + true + } + + return if (!result || sb.isEmpty()) + null + else + sb.toString() +} + +internal fun Collection.walkUntil(block: (Component) -> Boolean): Boolean { + for (component in this) { + if (!block(component)) return false + when (component) { - is ActionRow -> sb.append(component.components.mergeCustomIds()) - is Container -> sb.append(component.components.mergeCustomIds()) - is ActionComponent -> component.customId?.let { sb.append(it) } + is ActionRow -> { + if (!component.components.walkUntil(block)) + return false + } + is Container -> { + if (!component.components.walkUntil(block)) + return false + } is Section -> { - if (component.accessory is Button) { - sb.append(component.accessory.asButton().customId) - } - component.contentComponents.mergeCustomIds() + if (!block(component.accessory)) return false + if (!component.contentComponents.walkUntil(block)) + return false } } } - - return sb.toString() + return true } \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/view/JDA.kt b/core/src/main/kotlin/at/xirado/jdui/view/JDA.kt index 11e75f7..1efe1ab 100644 --- a/core/src/main/kotlin/at/xirado/jdui/view/JDA.kt +++ b/core/src/main/kotlin/at/xirado/jdui/view/JDA.kt @@ -7,10 +7,12 @@ import at.xirado.jdui.view.definition.ViewDefinition import at.xirado.jdui.view.definition.function.ViewDefinitionFunction import at.xirado.jdui.view.metadata.MessageContext import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.entities.WebhookClient import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel import net.dv8tion.jda.api.interactions.InteractionHook import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback import net.dv8tion.jda.api.requests.RestAction +import net.dv8tion.jda.api.utils.messages.MessageCreateData import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -90,6 +92,38 @@ suspend fun MessageChannel.sendView(definition: ViewDefinition, context: Context return sendMessage(message).populateMessageContext(messageContext) } +// Webhooks + +suspend fun WebhookClient.sendView( + function: KFunction, + context: Context? = null, +): RestAction { + val state = createFunctionViewState(jda, function, context) + val messageContext = state.messageContext + + val message = state.composeMessage() + return sendMessage(message).useComponentsV2().populateMessageContext(this, messageContext) +} + +suspend inline fun WebhookClient.sendView(context: Context? = null) = sendView(T::class, context) + +suspend fun WebhookClient.sendView(clazz: KClass, context: Context? = null): RestAction { + val state = createClassViewState(jda, clazz, context) + val messageContext = state.messageContext + + val message: MessageCreateData = state.composeMessage() + return sendMessage(message).useComponentsV2().populateMessageContext(this, messageContext) +} + +suspend fun WebhookClient.sendView(definition: ViewDefinition, context: Context? = null): RestAction { + val instance = getInstance(jda) + val state = createViewState(instance, definition, context = context) + val messageContext = state.messageContext + + val message = state.composeMessage() + return sendMessage(message).useComponentsV2().populateMessageContext(this, messageContext) +} + @JvmName("populateMessageContextInteractionHook") internal fun RestAction.populateMessageContext(context: MessageContext) = onSuccess { context.provideInteractionHook(it) @@ -98,4 +132,14 @@ internal fun RestAction.populateMessageContext(context: Message @JvmName("populateMessageContextInteractionMessage") private fun RestAction.populateMessageContext(context: MessageContext) = onSuccess { context.provideMessageSource(it.channelIdLong, it.idLong) +} + +@JvmName("populateMessageContextWebhookMessage") +private fun RestAction.populateMessageContext(webhookClient: WebhookClient, context: MessageContext) = onSuccess { + if (webhookClient is InteractionHook) + context.provideInteractionHook(webhookClient) + else + context.provideWebhookClient(webhookClient, it.idLong) + +// context.provideMessageSource(it.channelIdLong, it.idLong) } \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/view/ViewMessageContainer.kt b/core/src/main/kotlin/at/xirado/jdui/view/ViewMessageContainer.kt deleted file mode 100644 index 0633b28..0000000 --- a/core/src/main/kotlin/at/xirado/jdui/view/ViewMessageContainer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package at.xirado.jdui.view - -import at.xirado.jdui.state.ViewState -import at.xirado.jdui.view.definition.ViewDefinition - -class ViewMessageContainer internal constructor( - private val state: ViewState, - definition: ViewDefinition -) { - private val views = mutableListOf(definition) - -} \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/view/metadata/MessageContext.kt b/core/src/main/kotlin/at/xirado/jdui/view/metadata/MessageContext.kt index 2196f3f..cb1b886 100644 --- a/core/src/main/kotlin/at/xirado/jdui/view/metadata/MessageContext.kt +++ b/core/src/main/kotlin/at/xirado/jdui/view/metadata/MessageContext.kt @@ -1,8 +1,8 @@ package at.xirado.jdui.view.metadata +import at.xirado.jdui.state.ViewState import at.xirado.jdui.utils.await import io.github.oshai.kotlinlogging.KotlinLogging -import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.entities.Message import net.dv8tion.jda.api.entities.WebhookClient import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel @@ -10,14 +10,12 @@ import net.dv8tion.jda.api.interactions.InteractionHook import net.dv8tion.jda.api.requests.RestAction import net.dv8tion.jda.api.utils.messages.MessageEditData import okio.withLock -import java.lang.ref.WeakReference import java.util.concurrent.locks.ReentrantLock -private val log = KotlinLogging.logger { } - internal class MessageContext( - private val jda: WeakReference + private val viewState: ViewState, ) { + private val jda = viewState.listener.jda private val lock = ReentrantLock() private var webhook: WebhookMessageSource? = null @@ -34,7 +32,8 @@ internal class MessageContext( if (hook.hasCallbackResponse() && messageSource == null) { hook.callbackResponse.message?.let { - provideMessageSource(it.channelIdLong, it.idLong) + if (Message.MessageFlag.EPHEMERAL !in it.flags) + provideMessageSource(it.channelIdLong, it.idLong) } } } @@ -88,7 +87,7 @@ internal class MessageContext( messageData: MessageEditData ): RestAction? { val messageSource = this.messageSource ?: return null - val jda = this.jda.get() ?: return null + val jda = this.jda val channel = jda.getChannelById(MessageChannel::class.java, messageSource.channelId) ?: return null diff --git a/core/src/main/kotlin/at/xirado/jdui/view/metadata/ViewStateMetadata.kt b/core/src/main/kotlin/at/xirado/jdui/view/metadata/ViewStateMetadata.kt index fee68a4..b0e48c9 100644 --- a/core/src/main/kotlin/at/xirado/jdui/view/metadata/ViewStateMetadata.kt +++ b/core/src/main/kotlin/at/xirado/jdui/view/metadata/ViewStateMetadata.kt @@ -1,8 +1,10 @@ package at.xirado.jdui.view.metadata +import at.xirado.jdui.JDUIListener import at.xirado.jdui.config.Secret import at.xirado.jdui.crypto.decryptChaCha import at.xirado.jdui.crypto.encryptChaCha +import at.xirado.jdui.utils.decode import at.xirado.jdui.utils.toBytes import at.xirado.jdui.view.metadata.source.ViewSourceData import kotlinx.serialization.ExperimentalSerializationApi @@ -55,3 +57,20 @@ internal class DecryptedViewStateMetadata( } } +@OptIn(ExperimentalSerializationApi::class) +internal fun decodeViewState(encodedData: String): EncryptedViewStateMetadata { + val decodedData = decode(encodedData) + return ProtoBuf.decodeFromByteArray(decodedData) +} + +@OptIn(ExperimentalSerializationApi::class) +internal suspend fun retrieveViewMetadataFromDb(jdui: JDUIListener, id: Long): EncryptedViewStateMetadata { + val persistence = jdui.config.persistenceConfig + ?: throw IllegalStateException("JDUI does not have a PersistenceConfig!") + + val retrievedState = persistence.retrieveState(id) + ?: throw IllegalStateException("No such view with id $id") + + return ProtoBuf.decodeFromByteArray(retrievedState.data) +} + diff --git a/core/src/main/kotlin/at/xirado/jdui/view/middleware/ViewMiddleware.kt b/core/src/main/kotlin/at/xirado/jdui/view/middleware/ViewMiddleware.kt new file mode 100644 index 0000000..870d931 --- /dev/null +++ b/core/src/main/kotlin/at/xirado/jdui/view/middleware/ViewMiddleware.kt @@ -0,0 +1,17 @@ +package at.xirado.jdui.view.middleware + +import at.xirado.jdui.view.View +import at.xirado.jdui.view.definition.ViewDefinition +import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent + +interface ViewMiddleware { + suspend fun processEvent(event: GenericComponentInteractionCreateEvent): Boolean +} + +fun ViewDefinition.middleware(middleware: ViewMiddleware) { + state.middleware += middleware +} + +fun View.middleware(middleware: ViewMiddleware) { + withDefinition { it.state.middleware += middleware } +} \ No newline at end of file diff --git a/core/src/main/kotlin/at/xirado/jdui/view/middleware/WhitelistedUsers.kt b/core/src/main/kotlin/at/xirado/jdui/view/middleware/WhitelistedUsers.kt new file mode 100644 index 0000000..91f72e4 --- /dev/null +++ b/core/src/main/kotlin/at/xirado/jdui/view/middleware/WhitelistedUsers.kt @@ -0,0 +1,26 @@ +package at.xirado.jdui.view.middleware + +import net.dv8tion.jda.api.components.container.Container +import net.dv8tion.jda.api.components.textdisplay.TextDisplay +import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder + +private val NOT_WHITELISTED_MESSAGE = MessageCreateBuilder() + .useComponentsV2() + .addComponents( + Container.of( + TextDisplay.of("You cannot interact with this message!") + ).withAccentColor(0xFF0000) + ) + .build() + +class WhitelistedUsers(val userIds: Collection) : ViewMiddleware { + override suspend fun processEvent(event: GenericComponentInteractionCreateEvent): Boolean { + val isWhitelisted = event.user.idLong in userIds + + if (!isWhitelisted) + event.reply(NOT_WHITELISTED_MESSAGE).setEphemeral(true).queue() + + return isWhitelisted + } +} \ No newline at end of file diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/CatView.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/CatView.kt index 1dada14..e1f3ff7 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/CatView.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/CatView.kt @@ -14,7 +14,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import net.dv8tion.jda.api.components.button.ButtonStyle.SECONDARY +import net.dv8tion.jda.api.components.buttons.ButtonStyle.SECONDARY import net.dv8tion.jda.api.components.mediagallery.MediaGalleryItem import okhttp3.OkHttpClient import okhttp3.Request @@ -26,7 +26,7 @@ private const val CAT_URL = "https://api.thecatapi.com/v1/images/search?limit=10 class CatView : View() { private val httpClient: OkHttpClient by context - private val cats: MutableList by state(mutableListOf()) + private val cats: MutableList by state { mutableListOf() } private var currentCatJob: Deferred? = null diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/ComponentTest.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/ComponentTest.kt index eae3236..d2fccc7 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/ComponentTest.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/ComponentTest.kt @@ -4,10 +4,10 @@ import at.xirado.jdui.component.message.* import at.xirado.jdui.component.row import at.xirado.jdui.state.state import at.xirado.jdui.view.definition.function.view -import net.dv8tion.jda.api.components.button.ButtonStyle +import net.dv8tion.jda.api.components.buttons.ButtonStyle import net.dv8tion.jda.api.components.mediagallery.MediaGalleryItem -import net.dv8tion.jda.api.components.selects.EntitySelectMenu -import net.dv8tion.jda.api.components.selects.SelectOption +import net.dv8tion.jda.api.components.selections.EntitySelectMenu +import net.dv8tion.jda.api.components.selections.SelectOption import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.utils.FileUpload @@ -20,8 +20,8 @@ private val emojis = mapOf( ) fun componentTest() = view { - var description: String? by state(null) - var emojiString: String by state("") + var description: String? by state { null } + var emojiString: String by state { "" } val file = { val bytes = "Hello World".byteInputStream() @@ -68,7 +68,7 @@ fun componentTest() = view { +row { val options = emojis.map { SelectOption.of(it.key, it.value).withEmoji(Emoji.fromUnicode(it.value)) } +stringSelect(options, placeholder = "Select an emoji") { - selectedOptions.forEach { emojiString += it.value } + event.selectedOptions.forEach { emojiString += it.value } } } +row { diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/CounterView.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/CounterView.kt index 5957e41..f3e1351 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/CounterView.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/CounterView.kt @@ -6,7 +6,7 @@ import at.xirado.jdui.component.row import at.xirado.jdui.state.state import at.xirado.jdui.view.definition.function.view import kotlinx.serialization.Serializable -import net.dv8tion.jda.api.components.button.ButtonStyle +import net.dv8tion.jda.api.components.buttons.ButtonStyle @Serializable data class LastUpdate( @@ -15,9 +15,9 @@ data class LastUpdate( ) fun counterView() = view { - var counter: Int by state(0) - var step: Int by state(1) - var lastUpdate: LastUpdate? by state(null) + var counter: Int by state { 0 } + var step: Int by state { 1 } + var lastUpdate: LastUpdate? by state { null } compose { +text( @@ -30,7 +30,7 @@ fun counterView() = view { +row { +button(ButtonStyle.SECONDARY, "Increment by $step") { counter += step - lastUpdate = LastUpdate(user.name, step) + lastUpdate = LastUpdate(event.user.name, step) } +button(ButtonStyle.PRIMARY, "-1", disabled = step == 1) { diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/MultipleMenusView.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/MultipleMenusView.kt index 8a8bd92..9c2d77e 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/MultipleMenusView.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/MultipleMenusView.kt @@ -10,14 +10,14 @@ import at.xirado.jdui.view.View import at.xirado.jdui.view.compose import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import net.dv8tion.jda.api.components.button.ButtonStyle +import net.dv8tion.jda.api.components.buttons.ButtonStyle import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.utils.TimeFormat class MultipleMenusView : View() { - private var isConfirmationView by state(false) - private var isLoading by state(false) - private var data: String? by state(null) + private var isConfirmationView by state { false } + private var isLoading by state { false } + private var data: String? by state { null } override suspend fun createView() = compose { +container(0x4287f5) { diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/StateTest.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/StateTest.kt index 7081887..d58b0f1 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/StateTest.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/StateTest.kt @@ -6,13 +6,13 @@ import at.xirado.jdui.component.message.text import at.xirado.jdui.component.row import at.xirado.jdui.state.state import at.xirado.jdui.view.definition.function.view -import net.dv8tion.jda.api.components.button.ButtonStyle.* +import net.dv8tion.jda.api.components.buttons.ButtonStyle.* fun stateTest() = view { - var someInt by state(0) - var someString by state("") - var someNullableInt: Int? by state(null) - var someNullableString: String? by state(null) + var someInt by state { 0 } + var someString by state { "" } + var someNullableInt: Int? by state { null } + var someNullableString: String? by state { null } compose { +container(0x00ff00) { diff --git a/example/src/main/kotlin/at/xirado/jdui/example/view/UrbanDictionaryView.kt b/example/src/main/kotlin/at/xirado/jdui/example/view/UrbanDictionaryView.kt index f735ce4..7b6ea80 100644 --- a/example/src/main/kotlin/at/xirado/jdui/example/view/UrbanDictionaryView.kt +++ b/example/src/main/kotlin/at/xirado/jdui/example/view/UrbanDictionaryView.kt @@ -9,7 +9,7 @@ import dev.minn.jda.ktx.util.await import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import net.dv8tion.jda.api.components.button.ButtonStyle +import net.dv8tion.jda.api.components.buttons.ButtonStyle import net.dv8tion.jda.api.components.separator.Separator import net.dv8tion.jda.api.entities.emoji.Emoji import net.dv8tion.jda.api.utils.TimeFormat @@ -22,9 +22,9 @@ import java.time.Instant class UrbanDictionaryView : View() { private val httpClient: OkHttpClient by context - private val query: UrbanDictionaryQuery? by state(context.get()) - private var pageIndex: Int by state(0) - private var definitions: List by state(emptyList()) + private val query: UrbanDictionaryQuery? by state { context.get() } + private var pageIndex: Int by state { 0 } + private var definitions: List by state { emptyList() } init { require(query != null) { "UrbanDictionaryQuery not present in context!" } diff --git a/example/src/main/resources/logback.xml b/example/src/main/resources/logback.xml index 8527d88..128e985 100644 --- a/example/src/main/resources/logback.xml +++ b/example/src/main/resources/logback.xml @@ -6,9 +6,9 @@ - + - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10a8dbd..216b00f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ publishing = "0.31.0" kotlin-coroutines = "1.10.1" kotlin-protobuf = "1.8.1" -jda = "54038f1cec" +jda = "4a9d724a21" slf4j-api = "2.0.13" kotlin-logging = "5.1.0" snowflake-id = "0.0.2"