Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions core/src/main/kotlin/at/xirado/jdui/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
11 changes: 4 additions & 7 deletions core/src/main/kotlin/at/xirado/jdui/JDUIListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
11 changes: 7 additions & 4 deletions core/src/main/kotlin/at/xirado/jdui/component/Component.kt
Original file line number Diff line number Diff line change
@@ -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<E> = suspend ViewComponentInteraction<E>.() -> Unit

@ViewDSL
abstract class Component<T> {
internal abstract val type: KType
Expand All @@ -19,11 +22,11 @@ abstract class StatelessComponent<T: JDAComponent> : Component<T>() {
internal abstract fun buildComponent(uniqueId: Int): T
}

abstract class StatefulActionComponent<T: JDAComponent, E: Any> : StatefulComponent<T>() {
internal abstract val callback: suspend E.() -> Unit
internal abstract val callbackClazz: KClass<E>
abstract class StatefulActionComponent<T: JDAComponent, E: GenericComponentInteractionCreateEvent> : StatefulComponent<T>() {
internal abstract val callback: suspend ViewComponentInteraction<E>.() -> Unit
internal abstract val eventClazz: KClass<E>

internal abstract suspend fun processInteraction(event: GenericComponentInteractionCreateEvent)
internal abstract suspend fun processInteraction(interaction: ViewComponentInteraction<E>)
}

abstract class ParentComponent<T, C> : Component<T>() {
Expand Down
17 changes: 9 additions & 8 deletions core/src/main/kotlin/at/xirado/jdui/component/message/Button.kt
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
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<ButtonInteractionEvent>,
) : StatefulActionComponent<JDAButton, ButtonInteractionEvent>() {
override fun buildComponent(id: String, uniqueId: Int): JDAButton {
return JDAButton.of(style, id, label, emoji)
.withDisabled(disabled)
.withUniqueId(uniqueId)
}

override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) {
callback(event as ButtonInteractionEvent)
override suspend fun processInteraction(interaction: ViewComponentInteraction<ButtonInteractionEvent>) {
callback(interaction)
}

override val type = typeOf<JDAButton>()
override val callbackClazz = ButtonInteractionEvent::class
override val eventClazz = ButtonInteractionEvent::class
}

class LinkButton(
Expand All @@ -50,7 +51,7 @@ fun button(
label: String? = null,
emoji: Emoji? = null,
disabled: Boolean = false,
callback: suspend ButtonInteractionEvent.() -> Unit,
callback: ComponentCallback<ButtonInteractionEvent>,
): ActionButton {
require(style != ButtonStyle.LINK) { "Cannot use ButtonStyle.LINK here. Use link() instead" }
return ActionButton(style, label, emoji, disabled, callback)
Expand Down
29 changes: 15 additions & 14 deletions core/src/main/kotlin/at/xirado/jdui/component/message/SelectMenu.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,7 +19,7 @@ class StringSelectMenu(
var range: IntRange,
var placeholder: String?,
var disabled: Boolean,
override val callback: StringCallback
override val callback: ComponentCallback<StringSelectInteractionEvent>
) : StatefulActionComponent<JDAStringSelectMenu, StringSelectInteractionEvent>() {
override fun buildComponent(id: String, uniqueId: Int): JDAStringSelectMenu {
return JDAStringSelectMenu.create(id)
Expand All @@ -30,12 +31,12 @@ class StringSelectMenu(
.build()
}

override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) {
callback(event as StringSelectInteractionEvent)
override suspend fun processInteraction(interaction: ViewComponentInteraction<StringSelectInteractionEvent>) {
callback(interaction)
}

override val type = typeOf<JDAStringSelectMenu>()
override val callbackClazz = StringSelectInteractionEvent::class
override val eventClazz = StringSelectInteractionEvent::class
}

class EntitySelectMenu(
Expand All @@ -44,7 +45,7 @@ class EntitySelectMenu(
var range: IntRange,
var placeholder: String?,
var disabled: Boolean,
override val callback: EntityCallback,
override val callback: ComponentCallback<EntitySelectInteractionEvent>,
) : StatefulActionComponent<JDAEntitySelectMenu, EntitySelectInteractionEvent>() {
override fun buildComponent(id: String, uniqueId: Int): JDAEntitySelectMenu {
return JDAEntitySelectMenu.create(id, targets)
Expand All @@ -56,20 +57,20 @@ class EntitySelectMenu(
.build()
}

override suspend fun processInteraction(event: GenericComponentInteractionCreateEvent) {
callback(event as EntitySelectInteractionEvent)
override suspend fun processInteraction(interaction: ViewComponentInteraction<EntitySelectInteractionEvent>) {
callback(interaction)
}

override val type = typeOf<JDAEntitySelectMenu>()
override val callbackClazz = EntitySelectInteractionEvent::class
override val eventClazz = EntitySelectInteractionEvent::class
}

fun stringSelect(
options: Collection<SelectOption>,
range: IntRange = 1..1,
placeholder: String? = null,
disabled: Boolean = false,
callback: StringCallback
callback: ComponentCallback<StringSelectInteractionEvent>
): StringSelectMenu {
return StringSelectMenu(options, range, placeholder, disabled, callback)
}
Expand All @@ -80,7 +81,7 @@ fun entitySelect(
range: IntRange = 1..1,
placeholder: String? = null,
disabled: Boolean = false,
callback: EntityCallback
callback: ComponentCallback<EntitySelectInteractionEvent>
) : EntitySelectMenu {
return EntitySelectMenu(targets, channelTypes, range, placeholder, disabled, callback)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TextDisplay(
var content: String
) : StatelessComponent<JDATextDisplay>() {
override fun buildComponent(uniqueId: Int): JDATextDisplay {
return JDATextDisplay.create(content).withUniqueId(uniqueId)
return JDATextDisplay.of(content).withUniqueId(uniqueId)
}

override val type = typeOf<JDATextDisplay>()
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal class UserStateCollection(
return

if (index > unpackedUserState.lastIndex) {
userState[property] = property.default
userState[property] = property.default()
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import kotlin.reflect.KProperty
class UserStateProperty<T> internal constructor(
internal val index: Int,
internal val property: KProperty<T>,
internal val default: T,
internal val default: () -> T,
internal val state: ViewState,
): ReadWriteProperty<Any?, T> {
internal val serializer = serializer(property.returnType)
Expand Down
Loading
Loading