BotContext allows to store data associated with user in a form of key-value pairs either on client, session or temp levels. Manipulating such a data in a raw key-value form may be quite complicated and uncomfortable. So Kotlin delegated properties comes to rescue.

JAICF provides helper functions that can be used in order to create delegated extension properties on BotContext encapsulating accesses to underlying key-value data storage. This functions are clientProperty, sessionProperty and tempProperty built on top of respective key-value maps. Source code can be found here.

var BotContext.userName by clientProperty<String>()

This code creates read-write extension property userName on BotContext of type String.

Accesses to this property will be reflected to accesses to property with key userName stored in BotContext.client.

action {
    context.userName = "John" // associates key "userName" with value "John" in BotContext.client
    val name = context.userName // reads the value associated with the key "userName" in BotContext.client
}

default

The property defined above will throw an exception in case it is read before its value was set. But often it’s useful to return some default value if the property value is not set yet or if the property is considered to be read-only. One can achieve that by providing default value inside a lambda expression:

var BotContext.isUserBlocked by clientProperty { false } // explicit type declaration can be omitted in that case

Inside a lambda expression BotContext is available as a lambda parameter. In case the property is considred to be computable but not assignable, it can be declared as a val

val BotContext.isUserBlocked by clientProperty { blackList.contains(it.clientId) }

saveDefault

By default such properties will recompute default value on each access while some explicit value is not set. Sometimes it’s desired behavior (as in the above example), but sometimes it’s not. For example getting default value may consist of costly computations, or it must be computed only once by design. In this case parameter saveDefault should be set to true:

val BotContext.sessionStartedAt by sessionProperty(saveDefault = true) { Instant.now() } // will be computed only once and immediately saved to `BotContext.session`
val BotContext.userInfo by clientProperty(saveDeafault = true) { httpClient.getUserInfo(it.clientId) } // requires long computation but will be computed only once

state("start") {
    action {
        val startTime = context.sessionStartedAt
        val userInfo = context.userInfo
        // ...
    }
}

state("hangup") {
    action {
        val duration = Instant.now() - context.sessionStartedAt
        // ...
    }
}

removeOnNull

Kotlin properties must either be able to provide some value or throw an exception in other case, it cannot be “cleared” in any sense. But sometimes an entry should be fully removed from underlying storage. In this case parameter removeOnNull should be set to true. Unfortunately, in that case the property must have a nullable type even if default value is not null:

var BotContext.order by sessionProperty<Order?>(removeOnNull = true) { createEmptyOrder() }

state("addItem") {
    action {
        context.order!!.add(item)
    }
}

state("clearOrder") {
    action {
        context.order = null
    }
}

key

In all of the above cases key of the property was not explicitely specified. In that case name of the property is used as a key in an underlying storage. In case explicit key is required, it can be passed in a function via the key parameter:

var BotContext.isUserBlocked by clientProperty { false } // key: "isUserBlocked"
var BotContext.isUserBlocked by clientProperty("blockStatus") { false } // key: "blockStatus"

Binding BotContext properties to other classes

It also possible to bind BotContext property to any class BotContext is accessible from, for example to ActionContext.

withContext

Function withContext allows to bind BotContext delegated property to a receiver of any other type by defining a way to get a BotContext from the new receiver:

val DefaultActionContext.order by sessionProperty<Order?>(removeOnNull = true) { createEmptyOrder() } withContext { context }

Here context inside a lambda is obtained from ActionContext.context.

Now property order can be accessed on ActionContext:

action {
    order.add(item)
}

with

Function with is similar to withContext, it also required defining a way to obtain BotContext, but it can be used only for properties, created by clientProperty, sessionProperty or tempProperty functions, contrary to withContext that can be used for any delegated property. With this limitation function with allows to redefine default value of the property with new one that will be computed with new receiver instead of BotContext:

val DefaultActionContext.phoneNumber by clientProperty<String>(saveDefault = true).with({ context }) {
    request.telegram?.contact?.phoneNumber ?: error("No phone number found") // `request` from `DefaultActionContext`
}

state("getContact") {
    activators {
        event(TelegramEvent.CONTACT)
    }
    
    action {
        scheduleCall(phoneNumber) // here `phoneNumber` is delegated property on ActionContext
    }
}