Skip to content

Commit

Permalink
7.2.2 (#224)
Browse files Browse the repository at this point in the history
* Added option to define custom keys for stateful links.
  • Loading branch information
vendelieu authored Aug 21, 2024
1 parent e67ac2e commit 3662cab
Show file tree
Hide file tree
Showing 19 changed files with 154 additions and 89 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Telegram-bot (KtGram) changelog

### 7.2.2

* Added option to define custom keys for stateful links.

### 7.2.1

* Fix ksp applying flow in plugin.
Expand Down
65 changes: 40 additions & 25 deletions ksp/src/jvmMain/kotlin/eu/vendeli/ksp/ChainStateBindingsBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import eu.vendeli.ksp.utils.FileBuilder
import eu.vendeli.ksp.utils.userClass
import eu.vendeli.ksp.utils.idLongClass

fun buildChainStateBindings(
botCtxBuilder: FileBuilder,
Expand All @@ -22,23 +23,11 @@ fun buildChainStateBindings(
) = botCtxBuilder.run {
val providerName = "${chainDeclaration.simpleName.getShortName()}AllState"
val chainType = chainDeclaration.toClassName()
val keyClasses = mutableSetOf<TypeName>()
addImport("kotlinx.coroutines", "runBlocking")

val user = FunSpec
.constructorBuilder()
.addParameter("user", userClass)
.build()

val stateBindingClass = TypeSpec
.classBuilder(providerName)
.primaryConstructor(user)
.addProperty(
PropertySpec
.builder("user", userClass)
.initializer("user")
.addModifiers(KModifier.PRIVATE)
.build(),
)

statefulLinks.forEach { l ->
val linkName = l.simpleName.getShortName()
Expand All @@ -57,27 +46,53 @@ fun buildChainStateBindings(
.toTypeName()
.copy(true)

val stateKeyType = l.getAllProperties()
.first {
it.simpleName.getShortName() == "state"
}.type.resolve().arguments.first().toTypeName()

keyClasses.add(stateKeyType)

stateBindingClass.addProperty(
PropertySpec
.builder(linkName, linkType)
.getter(
FunSpec
.getterBuilder()
.addStatement("return runBlocking {\n\t $linkQName.state.get(user) \n\t}")
.addStatement("return runBlocking {\n\t $linkQName.state.get(key) \n\t}")
.build(),
).build(),
)
}

addFunction(
FunSpec
.builder("getAllState")
.receiver(userClass)
.returns(ClassName(packageName, providerName))
.addParameter("chain", chainType)
.addCode("return $providerName(this)")
.build(),
)
keyClasses.singleOrNull()?.let { keyClass ->
val constructorKeyFun = FunSpec
.constructorBuilder()
.addParameter("key", keyClass)
.build()

stateBindingClass
.primaryConstructor(constructorKeyFun)
.addProperty(
PropertySpec
.builder("key", keyClass)
.initializer("key")
.addModifiers(KModifier.PRIVATE)
.build(),
)

addFunction(
FunSpec
.builder("getAllState").also {
if (keyClass == idLongClass) it.receiver(idLongClass)
else it.addParameter("key", keyClass)
}
.returns(ClassName(packageName, providerName))
.addParameter("chain", chainType)
.addCode("return $providerName(${if (keyClass == idLongClass) "this" else "key"})")
.build(),
)

addType(stateBindingClass.build())
addType(stateBindingClass.build())
}
}
3 changes: 2 additions & 1 deletion ksp/src/jvmMain/kotlin/eu/vendeli/ksp/InputChainCollector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ internal fun collectInputChains(
)
if (isStatefulLink) {
add("val linkState = inst.action(user, update, bot)\n")
add("inst.state.set(user, linkState)\n")
add("val stateKey = inst.state.stateKey.select(update)\n")
add("if(stateKey != null) inst.state.set(stateKey, linkState)\n")
} else {
add("inst.action(user, update, bot)\n")
}
Expand Down
2 changes: 2 additions & 0 deletions ksp/src/jvmMain/kotlin/eu/vendeli/ksp/utils/HelperUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import eu.vendeli.tgbot.types.internal.DeletedBusinessMessagesUpdate
import eu.vendeli.tgbot.types.internal.EditedBusinessMessageUpdate
import eu.vendeli.tgbot.types.internal.EditedChannelPostUpdate
import eu.vendeli.tgbot.types.internal.EditedMessageUpdate
import eu.vendeli.tgbot.types.internal.IdLong
import eu.vendeli.tgbot.types.internal.InlineQueryUpdate
import eu.vendeli.tgbot.types.internal.MessageReactionCountUpdate
import eu.vendeli.tgbot.types.internal.MessageReactionUpdate
Expand Down Expand Up @@ -72,6 +73,7 @@ internal val doublePrimitiveType = TypeVariableName("double")

internal val userClass = User::class.asTypeName()
internal val botClass = TelegramBot::class.asTypeName()
internal val idLongClass = IdLong::class.asTypeName()

internal val updateClass = ProcessedUpdate::class.asTypeName()
internal val messageUpdClass = MessageUpdate::class.asTypeName()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package eu.vendeli.ktgram.botctx.redis.chain

import eu.vendeli.tgbot.types.internal.IdLong
import eu.vendeli.tgbot.types.internal.chain.KeySelector
import eu.vendeli.tgbot.types.internal.chain.StatefulLink
import kotlinx.serialization.json.Json
import kotlin.reflect.KClass

open class BaseRedisLinkStateManager<L : StatefulLink<T>, T : Any>(
open class BaseRedisLinkStateManager<L : StatefulLink<IdLong, T>, T : Any>(
url: String = "redis://localhost",
storageType: KClass<T>,
linkRef: KClass<L>,
serializer: Json = Json,
) : RedisLinkStateManager<L, T>(url, storageType, linkRef, serializer)
stateKeySelector: KeySelector<IdLong>,
) : RedisLinkStateManager<L, T>(url, storageType, linkRef, serializer, stateKeySelector)
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package eu.vendeli.ktgram.botctx.redis.chain

import eu.vendeli.tgbot.types.internal.IdLong
import eu.vendeli.tgbot.types.internal.chain.LinkStateManager
import eu.vendeli.tgbot.types.internal.chain.StatefulLink
import eu.vendeli.tgbot.types.internal.userOrNull
import kotlinx.serialization.json.Json
import kotlin.reflect.KClass

abstract class RedisBaseStatefulLink<L : StatefulLink<String>>(
abstract class RedisBaseStatefulLink<L : StatefulLink<IdLong, String>>(
url: String = "redis://localhost",
linkRef: KClass<L>,
serializer: Json,
) : StatefulLink<String>() {
override val state: LinkStateManager<String> = BaseRedisLinkStateManager(url, String::class, linkRef, serializer)
) : StatefulLink<IdLong, String>() {
override val state: LinkStateManager<IdLong, String> = BaseRedisLinkStateManager(url, String::class, linkRef, serializer) { it.userOrNull }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.vendeli.ktgram.botctx.redis.chain

import eu.vendeli.tgbot.types.User
import eu.vendeli.tgbot.types.internal.IdLong
import eu.vendeli.tgbot.types.internal.chain.KeySelector
import eu.vendeli.tgbot.types.internal.chain.LinkStateManager
import eu.vendeli.tgbot.types.internal.chain.StatefulLink
import eu.vendeli.tgbot.utils.fullName
Expand All @@ -11,29 +12,32 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.serializerOrNull
import kotlin.reflect.KClass

abstract class RedisLinkStateManager<L : StatefulLink<T>, T : Any>(
abstract class RedisLinkStateManager<L, T>(
url: String = "redis://localhost",
storageType: KClass<T>,
private val linkRef: KClass<L>,
private val serializer: Json = Json,
) : LinkStateManager<T> {
stateSelector: KeySelector<IdLong>,
) : LinkStateManager<IdLong, T>
where L : StatefulLink<IdLong, T>, T : Any {
open val redis: RedisReactiveCommands<String, String> = RedisClient.create(url).connect().reactive()
override val stateKey: KeySelector<IdLong> = stateSelector

@OptIn(InternalSerializationApi::class)
private val storageTypeSerializer =
storageType.serializerOrNull() ?: error("Serializer for $storageType is not found")

override suspend fun get(user: User): T? =
redis.get("linkState-${linkRef::class.fullName}-${user.id}").block()?.let {
override suspend fun get(entity: IdLong): T? =
redis.get("linkState-${linkRef::class.fullName}-${entity.id}").block()?.let {
serializer.decodeFromString(storageTypeSerializer, it)
}

override suspend fun del(user: User) {
redis.del("linkState-${linkRef::class.fullName}-${user.id}").block()
override suspend fun del(entity: IdLong) {
redis.del("linkState-${linkRef::class.fullName}-${entity.id}").block()
}

override suspend fun set(user: User, value: T) {
override suspend fun set(entity: IdLong, value: T) {
val stringValue = serializer.encodeToString(storageTypeSerializer, value)
redis.set("linkState-${linkRef::class.fullName}-${user.id}", stringValue).block()
redis.set("linkState-${linkRef::class.fullName}-${entity.id}", stringValue).block()
}
}
53 changes: 32 additions & 21 deletions telegram-bot/api/telegram-bot.api
Original file line number Diff line number Diff line change
Expand Up @@ -2266,18 +2266,13 @@ public abstract class eu/vendeli/tgbot/implementations/BotContextMapImpl : eu/ve
public fun <init> ()V
public fun del (JLjava/lang/String;)V
public fun delAsync (JLjava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun get (JLjava/lang/String;)Ljava/lang/Object;
public fun get (JLjava/lang/String;)Ljava/lang/String;
public synthetic fun get (Leu/vendeli/tgbot/types/User;Ljava/lang/String;)Ljava/lang/Object;
public fun get (Leu/vendeli/tgbot/types/User;Ljava/lang/String;)Ljava/lang/String;
public fun get (JLjava/lang/String;)Ljava/lang/Object;
public fun get (Leu/vendeli/tgbot/types/User;Ljava/lang/String;)Ljava/lang/Object;
public fun getAsync (JLjava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
protected final fun getStorage ()Lio/ktor/util/collections/ConcurrentMap;
public synthetic fun set (JLjava/lang/String;Ljava/lang/Object;)V
public fun set (JLjava/lang/String;Ljava/lang/String;)V
public synthetic fun set (Leu/vendeli/tgbot/types/User;Ljava/lang/String;Ljava/lang/Object;)V
public fun set (Leu/vendeli/tgbot/types/User;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun setAsync (JLjava/lang/String;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun setAsync (JLjava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun set (JLjava/lang/String;Ljava/lang/Object;)V
public fun set (Leu/vendeli/tgbot/types/User;Ljava/lang/String;Ljava/lang/Object;)V
public fun setAsync (JLjava/lang/String;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class eu/vendeli/tgbot/implementations/ClassDataImpl : eu/vendeli/tgbot/implementations/BotContextMapImpl, eu/vendeli/tgbot/interfaces/ctx/ClassData {
Expand Down Expand Up @@ -3834,7 +3829,7 @@ public final class eu/vendeli/tgbot/types/Update$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class eu/vendeli/tgbot/types/User {
public final class eu/vendeli/tgbot/types/User : eu/vendeli/tgbot/types/internal/IdLong {
public static final field Companion Leu/vendeli/tgbot/types/User$Companion;
public fun <init> (JZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;)V
public synthetic fun <init> (JZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand All @@ -3860,7 +3855,7 @@ public final class eu/vendeli/tgbot/types/User {
public final fun getCanReadAllGroupMessages ()Ljava/lang/Boolean;
public final fun getFirstName ()Ljava/lang/String;
public final fun getHasMainWebApp ()Ljava/lang/Boolean;
public final fun getId ()J
public fun getId ()J
public final fun getLanguageCode ()Ljava/lang/String;
public final fun getLastName ()Ljava/lang/String;
public final fun getSupportsInlineQueries ()Ljava/lang/Boolean;
Expand Down Expand Up @@ -4783,7 +4778,7 @@ public final class eu/vendeli/tgbot/types/business/BusinessOpeningHoursInterval$
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class eu/vendeli/tgbot/types/chat/Chat {
public final class eu/vendeli/tgbot/types/chat/Chat : eu/vendeli/tgbot/types/internal/IdLong {
public static final field Companion Leu/vendeli/tgbot/types/chat/Chat$Companion;
public fun <init> (JLeu/vendeli/tgbot/types/chat/ChatType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V
public synthetic fun <init> (JLeu/vendeli/tgbot/types/chat/ChatType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand All @@ -4799,7 +4794,7 @@ public final class eu/vendeli/tgbot/types/chat/Chat {
public fun equals (Ljava/lang/Object;)Z
public final fun getFirstName ()Ljava/lang/String;
public final fun getFullName ()Ljava/lang/String;
public final fun getId ()J
public fun getId ()J
public final fun getLastName ()Ljava/lang/String;
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Leu/vendeli/tgbot/types/chat/ChatType;
Expand Down Expand Up @@ -7490,6 +7485,10 @@ public final class eu/vendeli/tgbot/types/internal/HttpLogLevel : java/lang/Enum
public static fun values ()[Leu/vendeli/tgbot/types/internal/HttpLogLevel;
}

public abstract interface class eu/vendeli/tgbot/types/internal/IdLong {
public abstract fun getId ()J
}

public abstract class eu/vendeli/tgbot/types/internal/Identifier {
public static final field Companion Leu/vendeli/tgbot/types/internal/Identifier$Companion;
public abstract fun getGet ()Ljava/lang/Object;
Expand Down Expand Up @@ -7968,14 +7967,25 @@ public abstract interface class eu/vendeli/tgbot/types/internal/UserReference {

public class eu/vendeli/tgbot/types/internal/chain/BaseLinkStateManager : eu/vendeli/tgbot/types/internal/chain/LinkStateManager {
public fun <init> ()V
public fun del (Leu/vendeli/tgbot/types/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun get (Leu/vendeli/tgbot/types/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun <init> (Leu/vendeli/tgbot/types/internal/chain/KeySelector;)V
public synthetic fun <init> (Leu/vendeli/tgbot/types/internal/chain/KeySelector;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun del (Leu/vendeli/tgbot/types/internal/IdLong;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun del (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun get (Leu/vendeli/tgbot/types/internal/IdLong;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun get (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
protected final fun getData ()Lco/touchlab/stately/collections/ConcurrentMutableMap;
public fun set (Leu/vendeli/tgbot/types/User;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getStateKey ()Leu/vendeli/tgbot/types/internal/chain/KeySelector;
public fun set (Leu/vendeli/tgbot/types/internal/IdLong;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public synthetic fun set (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract class eu/vendeli/tgbot/types/internal/chain/BaseStatefulLink : eu/vendeli/tgbot/types/internal/chain/StatefulLink {
public fun <init> ()V
public fun getState ()Leu/vendeli/tgbot/types/internal/chain/LinkStateManager;
}

public abstract interface class eu/vendeli/tgbot/types/internal/chain/KeySelector {
public abstract fun select (Leu/vendeli/tgbot/types/internal/ProcessedUpdate;)Ljava/lang/Object;
}

public abstract interface class eu/vendeli/tgbot/types/internal/chain/Link {
Expand All @@ -7992,9 +8002,10 @@ public final class eu/vendeli/tgbot/types/internal/chain/Link$DefaultImpls {
}

public abstract interface class eu/vendeli/tgbot/types/internal/chain/LinkStateManager {
public abstract fun del (Leu/vendeli/tgbot/types/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun get (Leu/vendeli/tgbot/types/User;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun set (Leu/vendeli/tgbot/types/User;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun del (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun get (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getStateKey ()Leu/vendeli/tgbot/types/internal/chain/KeySelector;
public abstract fun set (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract class eu/vendeli/tgbot/types/internal/chain/StatefulLink : eu/vendeli/tgbot/types/internal/chain/Link {
Expand All @@ -8004,7 +8015,7 @@ public abstract class eu/vendeli/tgbot/types/internal/chain/StatefulLink : eu/ve
public fun getBeforeAction ()Leu/vendeli/tgbot/types/internal/Action;
public fun getBreakCondition ()Leu/vendeli/tgbot/types/internal/BreakCondition;
public fun getRetryAfterBreak ()Z
public fun getState ()Leu/vendeli/tgbot/types/internal/chain/LinkStateManager;
public abstract fun getState ()Leu/vendeli/tgbot/types/internal/chain/LinkStateManager;
}

public final class eu/vendeli/tgbot/types/internal/configuration/BotConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import eu.vendeli.tgbot.utils.asyncAction
import io.ktor.util.collections.ConcurrentMap
import kotlinx.coroutines.Deferred

abstract class BotContextMapImpl : BotContext<String> {
protected val storage by lazy { ConcurrentMap<String, String?>() }
override fun set(telegramId: Long, key: String, value: String?) {
abstract class BotContextMapImpl<T> : BotContext<T> {
protected val storage by lazy { ConcurrentMap<String, T?>() }
override fun set(telegramId: Long, key: String, value: T?) {
storage["$telegramId-$key"] = value
}

override suspend fun setAsync(telegramId: Long, key: String, value: String?): Deferred<Boolean> = asyncAction {
override suspend fun setAsync(telegramId: Long, key: String, value: T?): Deferred<Boolean> = asyncAction {
storage["$telegramId-$key"] = value
true
}

override fun get(telegramId: Long, key: String): String? =
override fun get(telegramId: Long, key: String): T? =
storage["$telegramId-$key"]

override suspend fun getAsync(telegramId: Long, key: String): Deferred<String?> = asyncAction {
override suspend fun getAsync(telegramId: Long, key: String): Deferred<T?> = asyncAction {
get(telegramId, key)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package eu.vendeli.tgbot.implementations
import eu.vendeli.tgbot.interfaces.ctx.ClassData

class ClassDataImpl :
BotContextMapImpl(),
BotContextMapImpl<String>(),
ClassData<String> {
override suspend fun clearAll(telegramId: Long) {
storage.entries.removeAll { it.key.startsWith("$telegramId-") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package eu.vendeli.tgbot.implementations
import eu.vendeli.tgbot.interfaces.ctx.UserData

class UserDataMapImpl :
BotContextMapImpl(),
BotContextMapImpl<String>(),
UserData<String>
Loading

0 comments on commit 3662cab

Please sign in to comment.