JetBrains Space Help

(Kotlin) How to Add Interactive UI to Messages

Prerequisites

We assume that:

What will we do

In this tutorial we will extend the functionality of the 'Remind me' bot that we created in the (Kotlin) How to Create a Chatbot tutorial. More specifically, we'll add UI elements to one of the bot messages: When the user sends remind without specifying the exact time, the bot will send the user a message containing three buttons with the predefined time.

If you don't want to pass this tutorial step by step and want just look at the resulting code, it's totally OK – here's the source code.

Step 1. Create a message containing interactive buttons

As you might remember from the (Kotlin) How to Create a Chatbot tutorial, Space SDK offers the separate DSL for creating chat messages. We call it Message Builder. Previously, we used it only to add fancy look to our messages. Now, let's use it to create a message with clickable buttons inside.

Currently, buttons are the only interactive elements in messages. A button must have an assigned action: When a user clicks the button, Space will send a special MessageActionPayload type of payload. Such a payload contains action ID and action arguments that we can parse in our chatbot.

MessageControlGroupBuilder, MessageFieldBuilder, MessageSectionBuilder: These are the classes that let us extend the message builder DSL. The button belongs to control group elements. So, we will extend the MessageControlGroupBuilder class with our own custom button function.

Let's create a new message type that will suggest the user three reminder time intervals with three buttons.

  1. Open the CommandRemind.kt file and append the following code:

    private fun MessageControlGroupBuilder.remindButton(delayMs: Long, reminderText: String) { val text = "${delayMs / 1000} seconds" val style = MessageButtonStyle.PRIMARY val action = PostMessageAction("remind", "$delayMs $reminderText") button(text, action, style) }

    Here we predefine a button with our custom style and text. The most important variable here is action: It returns an instance of PostMessageAction with the remind action ID, a timer delay value, and a reminder text.

    This is how such a button will look:

    Chatbot message button
  2. Now, let's create a message that contains the remindButton. For example, let's add three buttons to the message: each for a certain time delay.

    Append the following code to CommandRemind.kt:

    private fun suggestRemindMessage(reminderText: String): ChatMessage { return message { section { text("Remind me in ...") controls { // buttons for 5, 60, and 300 seconds remindButton(5 * 1000, reminderText) remindButton(60 * 1000, reminderText) remindButton(300 * 1000, reminderText) } } } }

    The final message will look like follows:

    Chatbot interactive message
  3. Now, let's decide when we will show the user our newly created suggestRemindMessage(). The most obvious decision is to show it when the user sent the remind command and a reminder text but didn't specify the time interval.

    In the CommandRemind.kt, find the runRemindCommand() function and update it as shown below:

    suspend fun runRemindCommand(payload: MessagePayload) { val remindMeArgs = getArgs(payload) when { remindMeArgs == null -> { sendMessage(payload.userId, helpMessage()) } remindMeArgs.delayMs == null && remindMeArgs.reminderText.isNotEmpty() -> { sendMessage(payload.userId, suggestRemindMessage(remindMeArgs.reminderText)) } remindMeArgs.delayMs == null -> { sendMessage(payload.userId, helpMessage()) } else -> { remindAfterDelay(payload.userId, remindMeArgs.delayMs, remindMeArgs.reminderText) } } }
  4. Done! Now we have a message with buttons that will be returned to a user when the user sends remind without a time interval. But what happens when the user actually clicks one of the buttons?

Step 2. Process the MessageActionPayload

When a user clicks the button, Space will send MessageActionPayload payload to the bot. The payload contains an ID of the action specified in the button and action arguments. Our task is to process the payload.

  1. First, let's create an overload for the runRemindCommand(payload: MessagePayload) function. The existing one accepts the MessagePayload while we need it to also accept MessageActionPayload.

    Open the CommandRemind.kt and append the code:

    suspend fun runRemindCommand(payload: MessageActionPayload) { val remindMeArgs = getArgs(payload) ?: return val delayMs = remindMeArgs.delayMs ?: return val reminderText = remindMeArgs.reminderText remindAfterDelay(payload.userId, delayMs, reminderText) } // overload of getArgs that accepts MessageActionPayload private fun getArgs(payload: MessageActionPayload): RemindMeArgs? { val args = payload.actionValue val delayMs = args.substringBefore(" ").toLongOrNull() ?: return null val reminderText = args.substringAfter(" ").trimStart().takeIf { it.isNotEmpty() } ?: return null return RemindMeArgs(delayMs, reminderText) }

    Here we take an action argument from the payload using payload.actionValue and run the timer with this argument.

  2. Now let's teach our chatbot's endpoint to process the MessageActionPayload.

    Open the Routes.kt file and update the Application.configureRouting() function:

    fun Application.configureRouting() { routing { post("api/space") { // read request body val body = call.receiveText() // verify if the request comes from a trusted Space instance val signature = call.request.header("X-Space-Public-Key-Signature") val timestamp = call.request.header("X-Space-Timestamp")?.toLongOrNull() // verifyWithPublicKey gets a key from Space, uses it to generate message hash // and compares the generated hash to the hash in a message if (signature.isNullOrBlank() || timestamp == null || !spaceClient.verifyWithPublicKey(body, timestamp, signature) ) { call.respond(HttpStatusCode.Unauthorized) return@post } when (val payload = readPayload(body)) { is ListCommandsPayload -> { // Space requests the list of supported commands call.respondText( ObjectMapper().writeValueAsString(getSupportedCommands()), ContentType.Application.Json ) } is MessagePayload -> { // user sent a message to the application val command = supportedCommands.find { it.name == payload.command() } if (command == null) { runHelpCommand(payload) } else { launch { command.run(payload) } } call.respond(HttpStatusCode.OK, "") } is MessageActionPayload -> { when (payload.actionId) { "remind" -> { launch { runRemindCommand(payload) } } else -> error("Unknown command ${payload.actionId}") } // After sending a command, Space will wait for HTTP OK confirmation call.respond(HttpStatusCode.OK, "") } } } } }

    Here we add the is MessageActionPayload condition that checks the actionId and if it's remind, runs our just-added overload of runRemindCommand.

  3. Nice! Let's check out how it works!

Step 3. Run the bot

  1. Start our application by clicking Run in the gutter next to the main function in Application.kt:

    Run Ktor server
  2. Open your Space instance and find the bot: press Ctrl+K and type its name.

    Find the bot
  3. Send the remind message.

  4. In the response, click the 5 seconds button.

    Run the chatbot

Great job! We have successfully added a message with buttons to our chatbot.

Last modified: 06 September 2022