Skip to content

Commit 9aaa729

Browse files
committed
Almost map
1 parent 94ffbe2 commit 9aaa729

File tree

4 files changed

+94
-80
lines changed

4 files changed

+94
-80
lines changed

README.md

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Telegram Storage
22

3-
This library is your quick `Map<K, V>` in the Telegram channel. Your bot needs a channel (name or ID) with full admin
4-
rights. Don't change the description—the bot stores important meta there.
3+
This library is your quick `Map<K, V>` in the Telegram channel. To try it, your bot needs a channel (name or ID) with
4+
full admin rights.
5+
6+
## Warnings
7+
8+
* Don't change the description—the bot stores the keystore file ID there
9+
* After the first setup, you can't change the dictionary's key/value types
510

611
## Download
712

@@ -22,19 +27,18 @@ plugins {
2227

2328
```kotlin
2429
import com.github.demidko.telegram.*
25-
import kotlinx.serialization.Serializable
26-
import com.github.demidko.telegram.TelegramStorage.Constructors.newTelegramStorage
30+
import com.github.demidko.telegram.TelegramStorage.Constructors.TelegramStorage
2731

2832
@Serializable
2933
data class Person(val name: String, val address: String)
3034

3135
fun main() {
3236
val token = "Bot API token here"
3337
val channel = "Telegram channel name here"
34-
val storage = newTelegramStorage<Int, Person>(token, channel)
35-
38+
val storage = TelegramStorage<Int, Person>(token, channel)
39+
3640
storage[2] = Person("Elon Musk", "Texas") // saved to Telegram channel
3741

38-
val p: Person = storage[2] // restored Person("Elon Musk", "Texas") from channel
42+
val p = storage[2] // restored Person("Elon Musk", "Texas") from channel
3943
}
4044
```

build.gradle.kts

-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
@file:Suppress("VulnerableLibrariesLocal", "SpellCheckingInspection")
22

3-
import org.gradle.api.JavaVersion.VERSION_21
4-
import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
5-
6-
73
repositories {
84
mavenCentral()
95
maven("https://jitpack.io")

src/main/kotlin/com/github/demidko/telegram/TelegramStorage.kt

+64-58
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,62 @@ import com.github.kotlintelegrambot.entities.ChatId
66
import com.github.kotlintelegrambot.entities.ChatId.Companion.fromChannelUsername
77
import com.github.kotlintelegrambot.entities.ChatId.Companion.fromId
88
import com.github.kotlintelegrambot.entities.TelegramFile.ByByteArray
9+
import kotlinx.serialization.KSerializer
10+
import kotlinx.serialization.builtins.MapSerializer
911
import kotlinx.serialization.cbor.Cbor
10-
import kotlinx.serialization.decodeFromByteArray
11-
import kotlinx.serialization.encodeToByteArray
12+
import kotlinx.serialization.serializer
1213
import java.io.Closeable
14+
import java.io.Serializable
1315
import java.lang.Runtime.getRuntime
1416
import java.util.concurrent.ConcurrentHashMap
1517
import java.util.concurrent.Executors.newSingleThreadExecutor
16-
import java.util.concurrent.Future
1718

1819
/**
19-
* Immutable nosql database in your Telegram channel.
20+
* Immutable NoSQL database in your Telegram channel.
2021
* @param K key value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
2122
* @param V storable value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
2223
* Also see [Telegram Bot API limits](https://core.telegram.org/bots/faq#handling-media)
2324
*/
24-
class TelegramStorage<K, V> @Deprecated(
25-
"This method had to be made public because of weak JVM generics. Don't use it",
26-
ReplaceWith(
27-
"newTelegramStorage<K, V>(bot, channel)",
28-
"com.github.demidko.telegram.TelegramStorage.Constructors.newTelegramStorage"
29-
)
30-
) constructor(
25+
class TelegramStorage<K, V>(
3126
private val bot: Bot,
3227
private val channel: ChatId,
33-
private val keyToTelegramFileId: MutableMap<K, String>
28+
keySerializer: KSerializer<K>,
29+
private val valueSerializer: KSerializer<V>,
3430
) : Closeable {
3531

36-
@Suppress("Deprecation")
3732
companion object Constructors {
3833
/**
39-
* Immutable nosql database in your Telegram channel.
34+
* Immutable NoSQL database in your Telegram channel.
4035
* @param K key value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
4136
* @param V storable value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
4237
* Also see [Telegram Bot API limits](https://core.telegram.org/bots/faq#handling-media)
4338
* @param botToken Telegram bot token. Must be admin of the [channelName]
4439
* @param channelName Telegram channel name. Do not change the channel description or files!
4540
*/
46-
inline fun <reified K, V> newTelegramStorage(botToken: String, channelName: String): TelegramStorage<K, V> {
47-
return newTelegramStorage(bot { token = botToken }, fromChannelUsername(channelName))
41+
inline fun <reified K, reified V> TelegramStorage(
42+
botToken: String,
43+
channelName: String
44+
): TelegramStorage<K, V> {
45+
return TelegramStorage(bot { token = botToken }, fromChannelUsername(channelName))
4846
}
4947

5048
/**
51-
* Immutable nosql database in your Telegram channel.
49+
* Immutable NoSQL database in your Telegram channel..
5250
* @param K key value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
5351
* @param V storable value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
5452
* Also see [Telegram Bot API limits](https://core.telegram.org/bots/faq#handling-media)
5553
* @param botToken Telegram bot token. Must be admin of the [channelId]
5654
* @param channelId Telegram channel ID. Do not change the channel description or files!
5755
*/
58-
inline fun <reified K, V> newTelegramStorage(botToken: String, channelId: Long): TelegramStorage<K, V> {
59-
return newTelegramStorage(bot { token = botToken }, fromId(channelId))
56+
inline fun <reified K, reified V> TelegramStorage(
57+
botToken: String,
58+
channelId: Long
59+
): TelegramStorage<K, V> {
60+
return TelegramStorage(bot { token = botToken }, fromId(channelId))
6061
}
6162

6263
/**
63-
* Immutable nosql database in your Telegram channel.
64+
* Immutable NoSQL database in your Telegram channel.
6465
* @param K key value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
6566
* @param V storable value type. Should be [basic](https://kotlinlang.org/docs/basic-types.html) or annotated with [Serializable].
6667
* Also see [Telegram Bot API limits](https://core.telegram.org/bots/faq#handling-media)
@@ -69,14 +70,13 @@ class TelegramStorage<K, V> @Deprecated(
6970
* @param channel Telegram channel. Use [fromId] or [fromChannelUsername]. The [bot] must be admin of this channel.
7071
* Do not change the channel description or files!
7172
*/
72-
inline fun <reified K, V> newTelegramStorage(bot: Bot, channel: ChatId): TelegramStorage<K, V> {
73-
val fileId = bot.getChat(channel).get().description ?: return TelegramStorage(bot, channel, ConcurrentHashMap())
74-
val bytes = bot.downloadFileBytes(fileId) ?: return TelegramStorage(bot, channel, ConcurrentHashMap())
75-
val map = Cbor.decodeFromByteArray<Map<K, String>>(bytes)
76-
return TelegramStorage(bot, channel, ConcurrentHashMap(map))
73+
inline fun <reified K, reified V> TelegramStorage(bot: Bot, channel: ChatId): TelegramStorage<K, V> {
74+
return TelegramStorage(bot, channel, serializer<K>(), serializer<V>())
7775
}
7876
}
7977

78+
private val keystoreSerializer = MapSerializer(keySerializer, serializer<String>())
79+
8080
/**
8181
* Single thread to safe execution order
8282
*/
@@ -85,57 +85,63 @@ class TelegramStorage<K, V> @Deprecated(
8585
/**
8686
* Shutdown handler to save [keyToTelegramFileId] to [channel]
8787
*/
88-
private val onShutdown = Thread {
89-
atomicExecutor.submit {
90-
val map = keyToTelegramFileId.toMap()
91-
val telegramFile = Cbor.encodeToByteArray(map).let(::ByByteArray)
92-
val fileId = bot.sendDocument(channel, telegramFile).first?.body()?.result?.document?.fileId!!
93-
val isSuccessfully = bot.setChatDescription(channel, fileId).first?.isSuccessful!!
94-
check(isSuccessfully) { "Can't save file id $fileId" }
95-
}.get()
96-
}.apply(getRuntime()::addShutdownHook)
97-
98-
val size get() = keyToTelegramFileId.size
88+
private val shutdownHook = Thread(::close).apply(getRuntime()::addShutdownHook)
9989

100-
fun remove(k: K): Future<*>? {
101-
return atomicExecutor.submit {
102-
keyToTelegramFileId.remove(k)
103-
}
90+
private val keyToTelegramFileId: ConcurrentHashMap<K, String> = run {
91+
val fileId = bot.getChat(channel).get().description ?: return@run ConcurrentHashMap()
92+
val bytes = bot.downloadFileBytes(fileId) ?: return@run ConcurrentHashMap()
93+
val keys = Cbor.decodeFromByteArray(keystoreSerializer, bytes)
94+
ConcurrentHashMap(keys)
10495
}
10596

97+
val size get() = keyToTelegramFileId.size
98+
10699
val keys get() = keyToTelegramFileId.keys
107100

108-
fun setBinaryFuture(k: K, v: ByteArray): Future<*>? {
109-
return atomicExecutor.submit {
110-
val telegramFile = ByByteArray(v)
111-
keyToTelegramFileId[k] = bot.sendDocument(channel, telegramFile).first?.body()?.result?.document?.fileId!!
101+
fun remove(k: K) {
102+
val removeFuture = atomicExecutor.submit {
103+
keyToTelegramFileId.remove(k)
112104
}
105+
checkNotNull(removeFuture).get()
113106
}
114107

115-
fun setBinary(k: K, v: ByteArray) {
116-
checkNotNull(setBinaryFuture(k, v)).get()
108+
fun clear() {
109+
val destructionFuture = atomicExecutor.submit {
110+
keyToTelegramFileId.clear()
111+
bot.setChatDescription(channel, "")
112+
}
113+
checkNotNull(destructionFuture).get()
117114
}
118115

119-
inline operator fun <reified V1 : V> get(k: K): V1? {
120-
val bytes = getBinary(k) ?: return null
121-
return Cbor.decodeFromByteArray<V1>(bytes)
116+
fun isEmpty(): Boolean {
117+
return keyToTelegramFileId.isEmpty()
122118
}
123119

124-
inline fun <reified V1 : V> setFuture(k: K, v: V1): Future<*>? {
125-
return setBinaryFuture(k, Cbor.encodeToByteArray(v))
120+
operator fun get(k: K): V? {
121+
val bytes = keyToTelegramFileId[k]?.let(bot::downloadFileBytes) ?: return null
122+
return Cbor.decodeFromByteArray(valueSerializer, bytes)
126123
}
127124

128-
inline operator fun <reified V1 : V> set(k: K, v: V1) {
129-
checkNotNull(setFuture(k, v)).get()
125+
operator fun set(k: K, v: V) {
126+
val setValueFuture = atomicExecutor.submit {
127+
val telegramFile = ByByteArray(Cbor.encodeToByteArray(valueSerializer, v))
128+
keyToTelegramFileId[k] = bot.sendDocument(channel, telegramFile).first?.body()?.result?.document?.fileId!!
129+
}
130+
checkNotNull(setValueFuture).get()
130131
}
131132

132-
fun getBinary(k: K): ByteArray? {
133-
return keyToTelegramFileId[k]?.let(bot::downloadFileBytes)
133+
fun containsKey(k: K): Boolean {
134+
return keyToTelegramFileId.containsKey(k)
134135
}
135136

136137
override fun close() {
137-
onShutdown.start()
138-
onShutdown.join()
139-
getRuntime().removeShutdownHook(onShutdown)
138+
val shutdownFuture = atomicExecutor.submit {
139+
val telegramFile = Cbor.encodeToByteArray(keystoreSerializer, keyToTelegramFileId).let(::ByByteArray)
140+
val fileId = bot.sendDocument(channel, telegramFile).first?.body()?.result?.document?.fileId!!
141+
val isSuccessfully = bot.setChatDescription(channel, fileId).first?.isSuccessful!!
142+
check(isSuccessfully) { "Can't save file id $fileId" }
143+
}
144+
checkNotNull(shutdownFuture).get()
145+
getRuntime().removeShutdownHook(shutdownHook)
140146
}
141147
}
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,54 @@
11
package com.github.demidko.telegram
22

3-
import com.github.demidko.telegram.TelegramStorage.Constructors.newTelegramStorage
3+
import com.github.demidko.telegram.TelegramStorage.Constructors.TelegramStorage
44
import com.google.common.truth.Truth.assertThat
55
import kotlinx.serialization.Serializable
6+
import org.junit.jupiter.api.AfterAll
67
import org.junit.jupiter.api.AfterEach
78
import org.junit.jupiter.api.BeforeEach
89
import org.junit.jupiter.api.Test
910
import java.lang.System.getenv
1011

1112
/**
12-
* You need provide BOT_TOKEN and CHANNEL_NAME environment variables for IT test
13+
* **You need provide BOT_TOKEN and CHANNEL_NAME environment variables for IT test**
1314
*/
14-
class TelegramStorageIT {
15+
object TelegramStorageIT {
16+
17+
private val botToken = getenv("BOT_TOKEN")
18+
19+
private val channelName = getenv("CHANNEL_NAME")
1520

1621
@Serializable
1722
data class Person(
1823
val name: String,
1924
val address: String,
2025
)
2126

22-
private lateinit var storage: TelegramStorage<Int, Person>
27+
private lateinit var employees: TelegramStorage<String, Person>
2328

2429
@BeforeEach
2530
fun openChannelStorage() {
26-
val botToken = getenv("BOT_TOKEN")
27-
val channelName = getenv("CHANNEL_NAME")
28-
storage = newTelegramStorage(botToken, channelName)
31+
employees = TelegramStorage(botToken, channelName)
2932
}
3033

3134
@AfterEach
3235
fun closeChannelStorage() {
33-
storage.close()
36+
employees.close()
3437
}
3538

3639
@Test
3740
fun testSave() {
38-
storage[2] = Person("Elon Musk", "Texas")
41+
employees["Special Government Employee"] = Person("Elon Musk", "Texas")
3942
}
4043

4144
@Test
4245
fun testDownload() {
43-
val person: Person = storage[2]!!
44-
assertThat(person).isEqualTo(Person("Elon Musk", "Texas"))
46+
assertThat(employees["Special Government Employee"]!!).isEqualTo(Person("Elon Musk", "Texas"))
47+
}
48+
49+
@AfterAll
50+
@JvmStatic
51+
fun clearChannelStorage() {
52+
employees.clear()
4553
}
4654
}

0 commit comments

Comments
 (0)