Skip to content

Commit

Permalink
Improve slash command API and add support for components (#310)
Browse files Browse the repository at this point in the history
* Make slash command creation eager

createGuildApplicationCommands and createGlobalApplicationCommands should eagerly create new commands to be more in line with the rest of the API

* Fix typo in InteractionBehavior

ackowledgePublic -> acknowledgePublic

* Don't compute supplier in InteractionBehavior

It's a waste of resources, and might result in unexpected behavior for non-Kord suppliers.

* Specify withStrategy for Interactions

Return types are important!

* Introduce type to command options

Also add Mentionable.

Seems like Discord consistently tells us what kind of option an object is via the type field, this allows us to more clearly represent the json data in the lower level APIs.

I ended up deleting DiscordOptionValue and replacing it with a more fleshed out CommandArgument.

* Add KordDsl to builders

* Add allowedMentions builder functions

* Add permission edits to guild commands

* Make full member available for guild contexts

* Support buttons/components (#303)

* Add new API models and properties

* Add new InteractionCallbackType

* Add ability to send components

* Update core entities

* Update tests

* Add @KordPreview annotations

* Convert don't trust doc comments to normal comments

* Fix compilation issue

* Optimize interaction serializing

* Add builder

* Fix tests

* Apply some requested changes

* Hopefully fix formatting

* Make builders enforce Discord restrictions

* Improve builders again

* Remove LinkButtonBuilder.style

* Improve builders
- Add components to more supported builders
- Make required fields parameters
- Make builders appending

* Update deprecated message (#280)

* Expose the creation of application commands behavior (#281)

* Fix GuildUpdate core handling (#284)

* Expose the creation of application commands behavior

* Fix type of emitted event

* Sealed message types (#282)

* Expose the creation of application commands behavior

* Make message types sealed

* make Unknown a class

* Add missing message types

* make MessageTypeSerializer internal

* Add buttons to Activity (#287)

* Add buttons to Activity

* Also pass buttons in constructor

* Add missing fields to Guild (#288)

* Add missing fields to Guild
- Add welcome_screen
- Add nsfw

* Fix failing tests

* Fix another failing tests

* Add Message.applicationId (#289)

* Message interaction (#283)

* Expose the creation of application commands behavior

* Add interaction message

* Apply suggestions

* reference the MessageInteraction in docs

* Implement Strategizable for MessageInteraction

* cache user from interaction message

* Fix compilation errors

* Fix withStrategy return type

Co-authored-by: Bart Arys <[email protected]>

Co-authored-by: Bart Arys <[email protected]>

* Implement Stage instances (#291)

* Add low-level implementation of stage instances

* Add helper functions

* Add core entities and api representations

* Expose creation of StageInstanceBehavior to unsafe
- Revert outdated change

* Final additions
- Add StageInstanceBehavior.asStageInstance
- Fix compiler issue
- Add StageChannelBehavior.getStageInstance()

* Add StageInstances to EntitySupplier.kt

* Add StageInstances to EntitySupplier.kt

* Fix typo

* Apply requested changes

* Fix return type of channel in VoiceState (#295)

* Fix return type of channel in VoiceState

* Fix GuildService#modifyVoiceState endpoint

* Fix MessageInteraction#user id

* Add missing session start limit field (#306)

- Adds the max_concurrency field to the session start limit data class

* Update to Kotlin 1.5 (#299)

* Update to 1.5.0-RC

* trigger on branch pushes

* Port to Kotlin 1.5 (#268)

* Port dependencies to Kotlin 1.5
- Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug
- Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug
- Bump other Kotlin dependencies to latest

fixup! Port dependencies to Kotlin 1.5 - Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug - Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug - Bump other Kotlin dependencies to latest

* Replace deprecated kotlin.time APIs

* Replace more deprecated APIs & inline classes

* Replace deprecated usage of time API in tests

* Possibly fix test

Co-authored-by: Hope <[email protected]>

* Port to Kotlin 1.5
- Bump dependencies to 1.5 recommended versions
- Remove inline classes in favor of value classes
- Add required opt-ins
- Migrate some more deprecated apis

* Fix some gradle issues

* Fix Gradle compilation issue

* Remove documentationFileName as the new dokka version doesn't support it anymore and there is no replacement yet

* Port kotlinx.serialization to 1.2.0 (#279)

* Port kotlinx.serialization to 1.2.0
- Convert local classes to top level classes (See Kotlin/kotlinx.serialization#1472)
- Improve handling of empty JSON bodies (See Kotlin/kotlinx.serialization#678)
- Fix Failing Command test

* Fix failing test

* Properly decode null in gateway events (#286)

* Properly decode null in gateway events

* Update gateway/src/main/kotlin/Event.kt

Co-authored-by: Bart Arys <[email protected]>

Co-authored-by: Bart Arys <[email protected]>

* Update Kotlinx.serialization (#290)

* Update Kotlin 1.5 branch to upstream (#292)

* Implement voice stage channel (#239)

* compute All

* implement rest endpoints

* JSON representation

* implement core representation

* handle stage channels

* Apply suggestions

Co-authored-by: BartArys <[email protected]>

* Remove duplicated factory function
Co-authored-by: BartArys <[email protected]>

* add documentation

* Document the requestToSpeak variable

Co-authored-by: BartArys <[email protected]>

* Fix CI triggers

* Add "Competing" activity type (Fix #270) (#272)

* Make Updatestatus activities not-null (#274)

As per Discord's documentation: discord/discord-api-docs#2789

* Fix memory issues related to Permission combining (#277)

* Do not octuple bitset size on copy

the pure plus and minus function create a new array to work with, this incorrectly created an array of a size equal to the amount of bits that were allocated, instead the amount of longs. Thus, octupling the internal size.

* Optimize Permission All

The All Permission folded each DiscordBitSet of each value into eachother, resulting in n + 1 bitsets being created. This commit changes that to use the internal `add` which instead, which only mutates the single bitset created.

* Add Stream permission

It was missing

* Add Permission All regression tests

* Update deprecated message (#280)

* Expose the creation of application commands behavior (#281)

* Fix GuildUpdate core handling (#284)

* Expose the creation of application commands behavior

* Fix type of emitted event

* Sealed message types (#282)

* Expose the creation of application commands behavior

* Make message types sealed

* make Unknown a class

* Add missing message types

* make MessageTypeSerializer internal

* Add buttons to Activity (#287)

* Add buttons to Activity

* Also pass buttons in constructor

* Add missing fields to Guild (#288)

* Add missing fields to Guild
- Add welcome_screen
- Add nsfw

* Fix failing tests

* Fix another failing tests

* Add Message.applicationId (#289)

Co-authored-by: Hope <[email protected]>
Co-authored-by: BartArys <[email protected]>
Co-authored-by: HopeBaron <[email protected]>
Co-authored-by: Bart Arys <[email protected]>
Co-authored-by: Noah Hendrickson <[email protected]>

* Fix broken CI (#293)

* Migrate to kotlinx-datetime (#297)

* Migrate API code to kotlinx-datetime

* Update tests

* Remove dead code

* Replace iso serializing with kx.dt

* Bump dependencies to Kotlin 1.5.10 (#305)

* Bump dependencies to Kotlin 1.5.10
- Fix sample code

* Update remaining dependencies

* Update ktor to 1.6.0

Co-authored-by: HopeBaron <[email protected]>
Co-authored-by: Michael Rittmeister <[email protected]>
Co-authored-by: Hope <[email protected]>
Co-authored-by: Noah Hendrickson <[email protected]>

* Filter out non-guilds when fetching rest guilds (#301)

This is a side-effect from the system we currently employ when it comes to creating channels. Unknown channels are instantiated as an anonymous channel without hierarchy. While we could assume it is a guild channel, it's not in actuality.

So the solution, as we did with cache, is to filter out channels that don't inherit the hierarchy.

* Improve builders again
- Merge in upstream

* Fix builder requirements
- Fix error when sending a emoji button

* Fix merge related syntax errors

* Restore default sample

* Fix formatting isssue in documentation

Co-authored-by: Bart Arys <[email protected]>

* Max component list a val

Co-authored-by: Bart Arys <[email protected]>

* Apply suggestions from code review

Co-authored-by: Bart Arys <[email protected]>

* Apply requested changes

* No longer make DiscordInteraction a sealed class

* Fix DiscordInteraction serialization

* Add some documentation for components

* Remove unused imports from builder

Co-authored-by: Noah Hendrickson <[email protected]>
Co-authored-by: Hope <[email protected]>
Co-authored-by: Bart Arys <[email protected]>
Co-authored-by: 2D <[email protected]>
Co-authored-by: HopeBaron <[email protected]>

* Add core versions of components

* Restructure and document ButtonBuilder

* Remove ActionRowContainerBuilder

* Make ComponentInteraction message nullable

ephemeral messages in the interaction contain a id, which is not a message id, and the flags.

We can't construct a behaviour from this (since the id isn't real), so I decided to drop the data if the message is ephemeral.

* Add missing components to interaction builders

* Add missing ComponentInteraction behavior

* Fix withStrategy for ComponentInteractionBehavior

* Implement ComponentInteractionBehavior

* Move component builders directory

We use singular for package names

* Fix interaction embeds optionality

* Make CommandInteraction#guildId optional

* Make MessageModifyBuilder components vals

Co-authored-by: Michael Rittmeister <[email protected]>
Co-authored-by: Noah Hendrickson <[email protected]>
Co-authored-by: Hope <[email protected]>
Co-authored-by: 2D <[email protected]>
Co-authored-by: HopeBaron <[email protected]>
  • Loading branch information
6 people committed Jun 24, 2021
1 parent 0e795d3 commit dab0d77
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 169 deletions.
6 changes: 6 additions & 0 deletions common/src/main/kotlin/entity/DiscordMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import kotlin.contracts.contract
* @param stickers The stickers sent with the message (bots currently can only receive messages with stickers, not send).
* @param referencedMessage the message associated with [messageReference].
* @param applicationId if the message is a response to an [Interaction][DiscordInteraction], this is the id of the interaction's application
* @param components a list of [components][DiscordComponent] which have been added to this message
*/
@Serializable
data class DiscordMessage(
Expand Down Expand Up @@ -103,6 +104,11 @@ data class DiscordMessage(
val stickers: Optional<List<DiscordMessageSticker>> = Optional.Missing(),
@SerialName("referenced_message")
val referencedMessage: Optional<DiscordMessage?> = Optional.Missing(),
/*
* don't trust the docs:
* This is a list even though the docs say it's a component
*/
val components: Optional<List<DiscordComponent>> = Optional.Missing(),
val interaction: Optional<DiscordMessageInteraction> = Optional.Missing()
)

Expand Down
17 changes: 15 additions & 2 deletions core/src/main/kotlin/Unsafe.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package dev.kord.core

import dev.kord.common.annotation.KordExperimental
import dev.kord.common.annotation.KordUnsafe

import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.*
import dev.kord.core.behavior.channel.*
import dev.kord.core.behavior.interaction.ComponentInteractionBehavior
import dev.kord.rest.service.InteractionService

/**
Expand Down Expand Up @@ -91,4 +91,17 @@ class Unsafe(private val kord: Kord) {
): GlobalApplicationCommandBehavior =
GlobalApplicationCommandBehavior(applicationId, commandId, service)

}
/**
* Creates a ComponentInteractionBehavior with the given [id], [channelId],
* [token] and [applicationId].
*/
fun componentInteraction(
id: Snowflake,
channelId: Snowflake,
token: String,
applicationId: Snowflake = kord.selfId,
): ComponentInteractionBehavior = ComponentInteractionBehavior(
id, channelId, token, applicationId, kord
)

}
6 changes: 2 additions & 4 deletions core/src/main/kotlin/cache/data/ComponentData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import dev.kord.common.entity.DiscordComponent
import dev.kord.common.entity.DiscordPartialEmoji
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.mapList
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -24,16 +23,15 @@ data class ComponentData(

companion object {

fun from(entity: DiscordComponent): ComponentData = with(entity) {
fun from(entity: DiscordComponent) = with(entity) {
ComponentData(
type,
style,
label,
emoji,
customId,
url,
disabled,
components.mapList { from(it) }
disabled
)
}

Expand Down
11 changes: 7 additions & 4 deletions core/src/main/kotlin/cache/data/MessageData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ data class MessageData(
val flags: Optional<MessageFlags> = Optional.Missing(),
val stickers: Optional<List<MessageStickerData>> = Optional.Missing(),
val referencedMessage: Optional<MessageData?> = Optional.Missing(),
val interaction: Optional<MessageInteractionData> = Optional.Missing()
val interaction: Optional<MessageInteractionData> = Optional.Missing(),
val components: Optional<List<ComponentData>> = Optional.Missing()
) {

fun plus(selfId: Snowflake, reaction: MessageReactionAddData): MessageData {
Expand Down Expand Up @@ -103,7 +104,8 @@ data class MessageData(
flags,
stickers = stickers,
referencedMessage = referencedMessage,
interaction = interaction
interaction = interaction,
components = components
)
}

Expand Down Expand Up @@ -138,10 +140,11 @@ data class MessageData(
flags,
stickers.mapList { MessageStickerData.from(it) },
referencedMessage.mapNotNull { from(it) },
interaction.map { MessageInteractionData.from(it) }
interaction.map { MessageInteractionData.from(it) },
components = components.mapList { ComponentData.from(it) }
)
}
}
}

fun DiscordMessage.toData() = MessageData.from(this)
fun DiscordMessage.toData() = MessageData.from(this)
11 changes: 8 additions & 3 deletions core/src/main/kotlin/entity/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package dev.kord.core.entity
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.MessageType
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.map
import dev.kord.common.entity.optional.mapNullable
import dev.kord.common.entity.optional.orEmpty
import dev.kord.common.entity.optional.unwrap
import dev.kord.common.exception.RequestException
import dev.kord.core.Kord
import dev.kord.core.behavior.MessageBehavior
Expand All @@ -16,8 +16,9 @@ import dev.kord.core.entity.channel.Channel
import dev.kord.core.entity.channel.GuildChannel
import dev.kord.core.entity.channel.GuildMessageChannel
import dev.kord.core.entity.channel.MessageChannel
import dev.kord.core.entity.interaction.MessageInteraction
import dev.kord.core.entity.component.Component
import dev.kord.core.entity.interaction.Interaction
import dev.kord.core.entity.interaction.MessageInteraction
import dev.kord.core.exception.EntityNotFoundException
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
Expand Down Expand Up @@ -108,7 +109,7 @@ class Message(
/**
* If the message is a response to an [Interaction], this is the id of the interaction's application
*/
val applicationId: Snowflake? get() = data.applicationId.value
val applicationId: Snowflake? get() = data.application.unwrap { it.id }

/**
* The message being replied to.
Expand Down Expand Up @@ -232,6 +233,10 @@ class Message(
*/
val webhookId: Snowflake? get() = data.webhookId.value

@KordPreview
val components: List<Component>
get() = data.components.orEmpty().map { Component(it) }

/**
* Returns itself.
*/
Expand Down
31 changes: 14 additions & 17 deletions core/src/main/kotlin/entity/interaction/Interaction.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package dev.kord.core.entity.interaction

import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.CommandArgument
import dev.kord.common.entity.InteractionType
import dev.kord.common.entity.Permissions
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.*
import dev.kord.common.entity.optional.*
import dev.kord.core.Kord
import dev.kord.core.KordObject
Expand All @@ -14,18 +11,25 @@ import dev.kord.core.behavior.MemberBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.channel.GuildMessageChannelBehavior
import dev.kord.core.behavior.interaction.ComponentInteractionBehavior
import dev.kord.core.behavior.interaction.EphemeralInteractionResponseBehavior
import dev.kord.core.behavior.interaction.InteractionBehavior
import dev.kord.core.behavior.interaction.PublicInteractionResponseBehavior
import dev.kord.core.cache.data.ApplicationInteractionData
import dev.kord.core.cache.data.InteractionData
import dev.kord.core.cache.data.ResolvedObjectsData
import dev.kord.core.entity.*
import dev.kord.core.entity.channel.DmChannel
import dev.kord.core.entity.channel.ResolvedChannel
import dev.kord.core.entity.component.ActionRowComponent
import dev.kord.core.entity.component.ButtonComponent
import dev.kord.core.entity.component.Component
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.rest.builder.interaction.UpdateMessageInteractionResponseCreateBuilder
import dev.kord.rest.json.request.InteractionApplicationCommandCallbackData
import dev.kord.rest.json.request.InteractionResponseCreateRequest
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
* An instance of [Interaction] (https://discord.com/developers/docs/interactions/slash-commands#interaction)
Expand Down Expand Up @@ -303,11 +307,11 @@ fun OptionValue(value: CommandArgument<*>, resolvedObjects: ResolvedObjects?): O

is CommandArgument.MentionableArgument -> {
val channel = resolvedObjects?.channels.orEmpty()[value.value]
val user = resolvedObjects?.users.orEmpty()[value.value]
val user = resolvedObjects?.channels.orEmpty()[value.value]
val member = resolvedObjects?.members.orEmpty()[value.value]
val role = resolvedObjects?.roles.orEmpty()[value.value]
val role = resolvedObjects?.members.orEmpty()[value.value]

OptionValue.MentionableOptionValue((channel ?: member ?: user ?: role)!!)
OptionValue.MentionableOptionValue((channel ?: user ?: member ?: role)!!)
}

is CommandArgument.RoleArgument -> {
Expand Down Expand Up @@ -381,11 +385,9 @@ class ComponentInteraction(
*
* @see Component
*/
val component: ButtonComponent?
val component: ButtonComponent
get() = message?.components.orEmpty()
.filterIsInstance<ActionRowComponent>()
.flatMap { it.buttons }
.firstOrNull { it.customId == componentId }
.filterIsInstance<ButtonComponent>().first { it.customId == componentId }


override fun withStrategy(strategy: EntitySupplyStrategy<*>): ComponentInteraction = ComponentInteraction(
Expand Down Expand Up @@ -459,8 +461,3 @@ fun OptionValue<*>.boolean() = value as Boolean

@KordPreview
fun OptionValue<*>.int() = value as Int

@KordPreview
fun OptionValue<*>.mentionable(): Entity {
return value as Entity
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class MessageInteraction(
override val id: Snowflake get() = data.id

/**
* the [name][ApplicationCommand.name] of the [ApplicationCommand] that triggered this message.
* the [name][ApplicationCommand.name] of the [ApplicationCommand] that triggered this message.
*/
val name: String get() = data.name

Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
package dev.kord.rest.builder.interaction

import dev.kord.common.annotation.KordDsl
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.InteractionResponseType
import dev.kord.common.entity.MessageFlag
import dev.kord.common.entity.MessageFlags
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.delegate.delegate
import dev.kord.common.entity.optional.map
import dev.kord.common.entity.optional.mapList
import dev.kord.common.entity.optional.optional
import dev.kord.rest.builder.component.ActionRowBuilder
import dev.kord.rest.builder.component.MessageComponentBuilder
import dev.kord.rest.builder.message.AllowedMentionsBuilder
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.json.request.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@KordDsl
@KordPreview
class EphemeralInteractionResponseModifyBuilder : BaseInteractionResponseModifyBuilder {
private var _content: Optional<String> = Optional.Missing()
override var content: String? by ::_content.delegate()

override val embeds: MutableList<EmbedBuilder> = mutableListOf()


private var _allowedMentions: Optional<AllowedMentionsBuilder> = Optional.Missing()
override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate()

val components: MutableList<MessageComponentBuilder> = mutableListOf()

@OptIn(ExperimentalContracts::class)
inline fun allowedMentions(builder: AllowedMentionsBuilder.() -> Unit) {
contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) }
allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(builder)
}

@OptIn(ExperimentalContracts::class)
@KordPreview
inline fun actionRow(builder: ActionRowBuilder.() -> Unit) {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}

components.add(ActionRowBuilder().apply(builder))
}

override fun toRequest(): MultipartInteractionResponseModifyRequest {
return MultipartInteractionResponseModifyRequest(
InteractionResponseModifyRequest(
content = _content,
allowedMentions = _allowedMentions.map { it.build() },
components = Optional.missingOnEmpty(components.map { it.build() }),
embeds = embeds.map { it.toRequest() }
)
)

}
}


@KordDsl
@KordPreview
class EphemeralInteractionResponseCreateBuilder : BaseInteractionResponseCreateBuilder {
private var _content: Optional<String> = Optional.Missing()
Expand All @@ -61,11 +81,11 @@ class EphemeralInteractionResponseCreateBuilder : BaseInteractionResponseCreateB
val data = InteractionApplicationCommandCallbackData(
content = _content,
flags = flags,
embeds = embeds.map { it.toRequest() }
embeds = Optional.missingOnEmpty(embeds.map { it.toRequest() })
)
return MultipartInteractionResponseCreateRequest(
InteractionResponseCreateRequest(type, data.optional())
)

}
}
}
Loading

0 comments on commit dab0d77

Please sign in to comment.