Compare commits

...

43 Commits

Author SHA1 Message Date
renovate[bot]
ab45b92771 Update dependency dev.inmo:micro_utils.ktor.server to v0.26.2 2025-08-10 14:10:11 +00:00
ab85d3acb2 Merge pull request #325 from InsanusMokrassar/renovate/ktor-monorepo
Update ktor monorepo to v3.2.3
2025-08-09 21:36:53 +06:00
3c3262b9cc Merge pull request #342 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v27.1.2
2025-08-09 21:36:24 +06:00
renovate[bot]
e378c6630c Update telegram_bot_api_version to v27.1.2 2025-08-07 10:58:59 +00:00
renovate[bot]
4eda93538e Update ktor monorepo to v3.2.3 2025-07-29 11:32:32 +00:00
707ad9a160 Merge pull request #331 from InsanusMokrassar/renovate/compose_version
Update plugin org.jetbrains.compose to v1.8.2
2025-07-29 17:31:19 +06:00
68e9830a8f Merge pull request #341 from InsanusMokrassar/27.1.0
27.1.0
2025-07-29 17:28:01 +06:00
55ebdeadbc migration onto 27.1.0 2025-07-27 14:32:39 +06:00
renovate[bot]
d4f3d4bc68 Update plugin org.jetbrains.compose to v1.8.2 2025-07-25 17:23:59 +00:00
b3d06c9773 Merge pull request #340 from InsanusMokrassar/27.0.0
27.0.0
2025-07-25 23:22:44 +06:00
e6e3eabf97 update dependencies 2025-07-22 19:52:24 +06:00
47efedf311 start migration onto 27.0.0 2025-07-15 13:35:35 +06:00
8423b1377b Merge pull request #336 from InsanusMokrassar/26.1.0
26.1.0
2025-07-10 17:21:41 +06:00
d0029603ce improvements according to latest changes 2025-07-08 18:41:18 +06:00
8d8fa74779 start migration onto 26.1.0 2025-07-08 11:58:35 +06:00
459a70c47b Merge pull request #333 from InsanusMokrassar/26.0.0
26.0.0
2025-06-16 12:15:06 +06:00
88102f3afa update telegram bot api version up to last release one 2025-06-15 10:33:03 +06:00
a621058fdd update dependencies 2025-06-14 23:48:32 +06:00
56e072aabe Merge pull request #332 from InsanusMokrassar/25.0.0
25.0.0
2025-06-14 23:35:44 +06:00
73f05bbcd7 fix in repositories 2025-06-14 23:21:00 +06:00
f053013360 fix of build with adding google repository in webapp sample? 2025-06-14 22:28:14 +06:00
bc39279c6c update telegram bot api version 2025-06-01 22:18:08 +06:00
ad8fa92e87 protess on tests 2025-06-01 22:15:57 +06:00
b0554adb7f add delete_story test 2025-06-01 21:19:37 +06:00
ad90180def fixes 2025-06-01 20:23:19 +06:00
69eda92bc7 add set business account profile photo tests 2025-06-01 19:24:01 +06:00
aee070c6c6 add check of set business account bio 2025-05-25 19:42:47 +06:00
36163d5619 add sample for set business account username 2025-05-25 19:30:51 +06:00
92d1c7a402 add sections and update version of library 2025-05-25 13:49:02 +06:00
7ce784d0a2 add device storage sample and beckground/text color config in webapps 2025-05-25 13:01:22 +06:00
d203d48391 add support of setBusinessAccountName 2025-05-24 22:32:10 +06:00
9352bb0090 add tests for reading and removing business messages 2025-05-18 22:04:03 +06:00
b1bb11d826 update nexus.inmo.dev maven repo 2025-05-18 21:13:43 +06:00
349517462e start add tests for new business account features 2025-05-11 21:28:32 +06:00
1708cad654 start migration onto 25.0.0 2025-05-11 21:13:58 +06:00
f87a9c5c66 Merge pull request #316 from InsanusMokrassar/renovate/kotlin-monorepo
Update kotlin monorepo to v2.1.10
2025-02-15 16:46:54 +06:00
a7b54e4b63 Merge pull request #320 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.24.6
2025-02-15 16:45:39 +06:00
renovate[bot]
436213492d Update kotlin monorepo to v2.1.10 2025-02-15 10:45:34 +00:00
renovate[bot]
0c2110a71d Update dependency dev.inmo:micro_utils.ktor.server to v0.24.6 2025-02-15 10:45:29 +00:00
949fa1a429 Merge pull request #319 from InsanusMokrassar/renovate/ktor-monorepo
Update ktor monorepo to v3.1.0
2025-02-15 16:45:04 +06:00
97cdd5a95f Merge pull request #321 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v23.2.0
2025-02-15 16:44:49 +06:00
renovate[bot]
0cb116acef Update telegram_bot_api_version to v23.2.0 2025-02-15 10:32:03 +00:00
renovate[bot]
a0332c4efd Update ktor monorepo to v3.1.0 2025-02-11 19:14:32 +00:00
15 changed files with 717 additions and 136 deletions

View File

@@ -2,24 +2,69 @@ import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.common.Percentage
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
import dev.inmo.tgbotapi.extensions.api.business.deleteBusinessMessages
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGifts
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
import dev.inmo.tgbotapi.extensions.api.business.readBusinessMessage
import dev.inmo.tgbotapi.extensions.api.business.removeBusinessAccountProfilePhoto
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountBio
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountName
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountProfilePhoto
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountUsername
import dev.inmo.tgbotapi.extensions.api.business.transferBusinessAccountStars
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage
import dev.inmo.tgbotapi.extensions.api.chat.modify.unpinChatMessage
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
import dev.inmo.tgbotapi.extensions.api.get.getBusinessConnection
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.stories.deleteStory
import dev.inmo.tgbotapi.extensions.api.stories.postStory
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.accessibleMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.commonMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.extendedPrivateChatOrThrow
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.flushAccumulatedUpdates
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.requests.abstracts.multipartFile
import dev.inmo.tgbotapi.requests.business_connection.InputProfilePhoto
import dev.inmo.tgbotapi.requests.stories.PostStory
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageId
import dev.inmo.tgbotapi.types.RawChatId
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
import dev.inmo.tgbotapi.types.message.content.PhotoContent
import dev.inmo.tgbotapi.types.message.content.StoryContent
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.content.VideoContent
import dev.inmo.tgbotapi.types.message.content.VisualMediaGroupPartContent
import dev.inmo.tgbotapi.types.stories.InputStoryContent
import dev.inmo.tgbotapi.types.stories.StoryArea
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
import dev.inmo.tgbotapi.types.stories.StoryAreaType
import dev.inmo.tgbotapi.utils.botCommand
import dev.inmo.tgbotapi.utils.code
import dev.inmo.tgbotapi.utils.extensions.splitForText
import dev.inmo.tgbotapi.utils.row
import korlibs.time.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
suspend fun main(args: Array<String>) {
val botToken = args.first()
@@ -34,21 +79,25 @@ suspend fun main(args: Array<String>) {
}
val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>()
val chatsBusinessConnections = mutableMapOf<ChatId, BusinessConnectionId>()
val businessConnectionsChatsMutex = Mutex()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
val me = getMe()
println(me)
flushAccumulatedUpdates()
onBusinessConnectionEnabled {
businessConnectionsChatsMutex.withLock {
businessConnectionsChats[it.id] = it.userChatId
chatsBusinessConnections[it.userChatId] = it.id
}
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been enabled")
}
onBusinessConnectionDisabled {
businessConnectionsChatsMutex.withLock {
businessConnectionsChats.remove(it.id)
chatsBusinessConnections.remove(it.userChatId)
}
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been disabled")
}
@@ -71,7 +120,20 @@ suspend fun main(args: Array<String>) {
if (businessContentMessage.sentByBusinessConnectionOwner) {
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
} else {
reply(sent, "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat")
reply(
to = sent,
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
)
send(
chatId = businessConnectionsChats[it.businessConnectionId] ?: return@ifBusinessContentMessage,
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
replyMarkup = inlineKeyboard {
row {
dataButton("Read message", "read ${it.chat.id.chatId.long} ${it.messageId.long}")
dataButton("Delete message", "delete ${it.chat.id.chatId.long} ${it.messageId.long}")
}
}
)
}
}
}
@@ -98,5 +160,328 @@ suspend fun main(args: Array<String>) {
}
send(businessConnectionOwnerChat, "There are several removed messages in chat ${it.chat.id}: ${it.messageIds}")
}
onCommand("get_business_account_info", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id]
val businessConnectionInfo = businessConnectionId ?.let { getBusinessConnection(it) }
reply(it) {
if (businessConnectionInfo == null) {
+"There is no business connection for current chat"
} else {
+(Json { prettyPrint = true; encodeDefaults = true }.encodeToString(businessConnectionInfo))
}
}
}
onMessageDataCallbackQuery(Regex("read \\d+ \\d+")) {
val (_, chatIdString, messageIdString) = it.data.split(" ")
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
val readResponse = businessConnectionId ?.let { readBusinessMessage(it, chatId, messageId) }
answer(
it,
if (readResponse == null) {
"There is no business connection for current chat"
} else {
"Message has been read"
}
)
}
onMessageDataCallbackQuery(Regex("delete \\d+ \\d+")) {
val (_, chatIdString, messageIdString) = it.data.split(" ")
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
val readResponse = businessConnectionId ?.let { deleteBusinessMessages(it, listOf(messageId)) }
answer(
it,
if (readResponse == null) {
"There is no business connection for current chat"
} else {
"Message has been deleted"
}
)
}
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
val firstName = args[0]
val secondName = args.getOrNull(1)
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val set = runCatching {
setBusinessAccountName(
businessConnectionId,
firstName,
secondName
)
}.getOrElse { false }
reply(it) {
if (set) {
+"Account name has been set"
} else {
+"Account name has not been set"
}
}
}
onCommandWithArgs("set_business_account_username", initialFilter = { it.chat is PrivateChat }) { it, args ->
val username = args[0]
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val set = runCatching {
setBusinessAccountUsername(
businessConnectionId,
username
)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account username has been set"
} else {
+"Account username has not been set"
}
}
}
onCommand("get_business_account_star_balance", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val starAmount = runCatching {
getBusinessAccountStarBalance(businessConnectionId)
}.getOrElse {
it.printStackTrace()
null
}
reply(it) {
if (starAmount != null) {
+"Account stars amount: $starAmount"
} else {
+"Account stars amount has not been got"
}
}
}
onCommandWithArgs("transfer_business_account_stars", initialFilter = { it.chat is PrivateChat }) { it, args ->
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val count = args.firstOrNull() ?.toIntOrNull() ?: reply(it) {
"Pass amount of stars to transfer to bot with command"
}.let {
return@onCommandWithArgs
}
val transferred = runCatching {
transferBusinessAccountStars(businessConnectionId, count)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (transferred) {
+"Stars have been transferred"
} else {
+"Stars have not been transferred"
}
}
}
onCommand("get_business_account_gifts", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val giftsFlow = runCatching {
getBusinessAccountGiftsFlow(businessConnectionId)
}.getOrElse {
it.printStackTrace()
null
}
if (giftsFlow == null) {
reply(it) {
+"Error in receiving of gifts"
}
} else {
giftsFlow.collect { giftsPage ->
giftsPage.gifts.joinToString {
it.toString()
}.splitForText().forEach { message ->
reply(it, message)
}
}
}
}
onCommand("set_business_account_bio", requireOnlyCommandInMessage = false, initialFilter = { it.chat is PrivateChat }) {
val initialBio = getChat(it.chat).extendedPrivateChatOrThrow().bio
val bio = it.content.text.removePrefix("/set_business_account_bio").trim()
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val set = runCatching {
setBusinessAccountBio(
businessConnectionId,
bio
)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account bio has been set. It will be reset within 15 seconds.\n\nInitial bio: " + code(initialBio)
} else {
+"Account bio has not been set"
}
}
delay(15.seconds)
val reset = runCatching {
setBusinessAccountBio(
businessConnectionId,
initialBio
)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (reset) {
+"Account bio has been reset"
} else {
+"Account bio has not been set. Set it manually: " + code(initialBio)
}
}
}
suspend fun handleSetProfilePhoto(it: CommonMessage<TextContent>, isPublic: Boolean) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo for using of this command"
}
return@handleSetProfilePhoto
}
val set = runCatching {
val file = downloadFileToTemp(replyTo.content)
setBusinessAccountProfilePhoto(
businessConnectionId,
InputProfilePhoto.Static(
file.multipartFile()
),
isPublic = isPublic
)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account profile photo has been set. It will be reset within 15 seconds"
} else {
+"Account profile photo has not been set"
}
}
if (set == false) { return@handleSetProfilePhoto }
delay(15.seconds)
val reset = runCatching {
removeBusinessAccountProfilePhoto(
businessConnectionId,
isPublic = isPublic
)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (reset) {
+"Account profile photo has been reset"
} else {
+"Account profile photo has not been set. Set it manually"
}
}
}
onCommand("set_business_account_profile_photo", initialFilter = { it.chat is PrivateChat }) {
handleSetProfilePhoto(it, false)
}
onCommand("set_business_account_profile_photo_public", initialFilter = { it.chat is PrivateChat }) {
handleSetProfilePhoto(it, true)
}
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
}
return@onCommand
}
val posted = runCatching {
val file = downloadFileToTemp(replyTo.content)
postStory(
businessConnectionId,
when (replyTo.content) {
is PhotoContent -> InputStoryContent.Photo(
file.multipartFile()
)
is VideoContent -> InputStoryContent.Video(
file.multipartFile()
)
},
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
areas = listOf(
StoryArea(
StoryAreaPosition(
x = Percentage.of100(50.0),
y = Percentage.of100(50.0),
width = Percentage.of100(8.0),
height = Percentage.of100(8.0),
rotationAngle = 45.0,
cornerRadius = Percentage.of100(4.0),
),
StoryAreaType.Link(
"https://github.com/InsanusMokrassar/TelegramBotAPI-examples/blob/master/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt"
)
)
)
) {
+"It is test of postStory :)"
}
}.getOrElse {
it.printStackTrace()
null
}
reply(it) {
if (posted != null) {
+"Story has been posted. You may unpost it with " + botCommand("remove_story")
} else {
+"Story has not been posted"
}
}
}
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<StoryContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
}
return@onCommand
}
val deleted = runCatching {
deleteStory(businessConnectionId, replyTo.content.story.id)
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (deleted) {
+"Story has been deleted"
} else {
+"Story has not been deleted"
}
}
}
// Will work when some premium user sending to some other user checklist
onChecklistContent {
execute(
it.content.createResend(
it.chat.id,
businessConnectionId = it.chat.id.businessConnectionId ?: chatsBusinessConnections[it.chat.id] ?: return@onChecklistContent
)
)
}
}.second.join()
}

View File

@@ -4,9 +4,13 @@ import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChannelDirectMessagesConfigurationChanged
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
import dev.inmo.tgbotapi.types.update.abstracts.Update
@@ -65,6 +69,7 @@ suspend fun main(vararg args: String) {
onCommand("start") {
println(data.update)
println(data.commonMessage)
println(getChat(it.chat))
}
onCommand(
@@ -77,6 +82,17 @@ suspend fun main(vararg args: String) {
println(data.commonMessage)
}
onCommand("getMyStarBalance") {
reply(
to = it,
text = getMyStarBalance().toString()
)
}
onChannelDirectMessagesConfigurationChanged {
println(it.chatEvent)
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}

View File

@@ -12,14 +12,16 @@ plugins {
id "org.jetbrains.kotlin.multiplatform"
}
apply plugin: 'application'
mainClassName="InlineQueriesBotKt"
apply from: "$nativePartTemplate"
kotlin {
jvm()
jvm {
binaries {
executable {
mainClass.set("InlineQueriesBotKt")
}
}
}
sourceSets {
commonMain {
@@ -27,12 +29,9 @@ kotlin {
implementation kotlin('stdlib')
api "dev.inmo:tgbotapi:$telegram_bot_api_version"
api "io.ktor:ktor-client-logging:$ktor_version"
}
}
}
}
dependencies {
implementation 'io.ktor:ktor-client-logging-jvm:3.0.3'
}

View File

@@ -1,4 +1,4 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply
@@ -59,7 +59,7 @@ suspend fun doInlineQueriesBot(token: String) {
reply(message, deepLink)
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it)
}

View File

@@ -1,4 +1,4 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe
@@ -89,7 +89,7 @@ suspend fun activateKeyboardsBot(
onCommandWithArgs("inline") { message, args ->
val numberArgs = args.mapNotNull { it.toIntOrNull() }
val numberOfPages = numberArgs.getOrNull(1) ?: numberArgs.firstOrNull() ?: 10
val page = numberArgs.firstOrNull() ?.takeIf { numberArgs.size > 1 } ?.coerceAtLeast(1) ?: 1
val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1
reply(
message,
replyMarkup = inlineKeyboard {
@@ -138,7 +138,8 @@ suspend fun activateKeyboardsBot(
onBaseInlineQuery {
val page = it.query.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery
val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery
val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() }
.toIntOrNull() ?: return@onBaseInlineQuery
answer(
it,
@@ -170,7 +171,7 @@ suspend fun activateKeyboardsBot(
setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard"))
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it)
}
}.join()

View File

@@ -12,12 +12,14 @@ plugins {
id "org.jetbrains.kotlin.multiplatform"
}
apply plugin: 'application'
mainClassName="RandomFileSenderBotKt"
kotlin {
jvm()
jvm {
binaries {
executable {
mainClass.set("RandomFileSenderBotKt")
}
}
}
sourceSets {
commonMain {

View File

@@ -1,4 +1,7 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.filter.filtered
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat
@@ -10,7 +13,9 @@ import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.ReplyParameters
import dev.inmo.tgbotapi.types.message.abstracts.BusinessContentMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.utils.DefaultKTgBotAPIKSLog
import dev.inmo.tgbotapi.utils.extensions.threadIdOrNull
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.currentCoroutineContext
@@ -19,7 +24,10 @@ suspend fun activateResenderBot(
token: String,
print: (Any) -> Unit
) {
telegramBotWithBehaviourAndLongPolling(token, scope = CoroutineScope(currentCoroutineContext() + SupervisorJob())) {
telegramBotWithBehaviourAndLongPolling(
token,
scope = CoroutineScope(currentCoroutineContext() + SupervisorJob()),
) {
onContentMessage(
subcontextUpdatesFilter = MessageFilterByChat,
initialFilter = { it !is BusinessContentMessage<*> || !it.sentByBusinessConnectionOwner }
@@ -31,15 +39,15 @@ suspend fun activateResenderBot(
it.content.createResend(
chat.id,
messageThreadId = it.threadIdOrNull,
replyParameters = it.replyInfo ?.messageMeta ?.let { meta ->
val quote = it.withContentOrNull<TextContent>() ?.content ?.quote
replyParameters = it.replyInfo?.messageMeta?.let { meta ->
val quote = it.withContentOrNull<TextContent>()?.content?.quote
ReplyParameters(
meta,
entities = quote ?.textSources ?: emptyList(),
quotePosition = quote ?.position
entities = quote?.textSources ?: emptyList(),
quotePosition = quote?.position
)
},
effectId = it.possiblyWithEffectMessageOrNull() ?.effectId
effectId = it.possiblyWithEffectMessageOrNull()?.effectId
)
) {
it.forEach(print)
@@ -49,7 +57,7 @@ suspend fun activateResenderBot(
println("Answer info: $answer")
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it)
}
print(bot.getMe())

View File

@@ -18,5 +18,5 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
implementation 'io.ktor:ktor-client-logging-jvm:3.0.3'
implementation 'io.ktor:ktor-client-logging-jvm:3.2.3'
}

View File

@@ -1,5 +1,5 @@
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandler
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.get.getCustomEmojiStickerOrNull
@@ -55,7 +55,7 @@ suspend fun activateStickerInfoBot(
withTypingAction(it.chat) {
it.content.textSources.mapNotNull {
if (it is CustomEmojiTextSource) {
getCustomEmojiStickerOrNull(it.customEmojiId) ?.stickerSetName
getCustomEmojiStickerOrNull(it.customEmojiId)?.stickerSetName
} else {
null
}
@@ -76,7 +76,7 @@ suspend fun activateStickerInfoBot(
)
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it)
}
}.join()

View File

@@ -16,10 +16,14 @@ plugins {
id "org.jetbrains.compose" version "$compose_version"
}
apply plugin: 'application'
kotlin {
jvm()
jvm {
binaries {
executable {
mainClass.set("WebAppServerKt")
}
}
}
js(IR) {
browser()
binaries.executable()
@@ -53,10 +57,6 @@ kotlin {
}
}
application {
mainClassName = "WebAppServerKt"
}
tasks.getByName("compileKotlinJvm")
.dependsOn(jsBrowserDistribution)
tasks.getByName("compileKotlinJvm").configure {

View File

@@ -1,6 +1,5 @@
import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.tgbotapi.types.userIdField
import dev.inmo.tgbotapi.types.webAppQueryIdField
import dev.inmo.tgbotapi.webapps.*
@@ -23,10 +22,12 @@ import kotlinx.dom.appendElement
import kotlinx.dom.appendText
import kotlinx.serialization.json.Json
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.attributes.placeholder
import org.jetbrains.compose.web.css.Style
import org.jetbrains.compose.web.css.StyleSheet
import org.jetbrains.compose.web.css.Color as ComposeColor
import org.jetbrains.compose.web.css.backgroundColor
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.color
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposable
@@ -39,6 +40,13 @@ fun HTMLElement.log(text: String) {
appendElement("p", {})
}
private object RootStyleSheet : StyleSheet() {
val rootClass by style {
color(ComposeColor("var(--tg-theme-text-color)"))
backgroundColor(ComposeColor("var(--tg-theme-bg-color)"))
}
}
@OptIn(ExperimentalUnsignedTypes::class)
fun main() {
console.log("Web app started")
@@ -46,6 +54,14 @@ fun main() {
val baseUrl = window.location.origin.removeSuffix("/")
renderComposable("root") {
Style(RootStyleSheet)
DisposableEffect(null) {
scopeElement.classList.add(RootStyleSheet.rootClass)
onDispose {
scopeElement.classList.remove(RootStyleSheet.rootClass)
}
}
val scope = rememberCoroutineScope()
val isSafeState = remember { mutableStateOf<Boolean?>(null) }
val logsState = remember { mutableStateListOf<Any?>() }
@@ -87,6 +103,7 @@ fun main() {
P()
Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}")
H3 { Text("Emoji status management") }
val emojiStatusAccessState = remember { mutableStateOf(false) }
webApp.onEmojiStatusAccessRequested {
emojiStatusAccessState.value = it.isAllowed
@@ -110,7 +127,7 @@ fun main() {
userId ?.let { userId ->
Button({
onClick {
scope.launchSafelyWithoutExceptions {
scope.launchLoggingDropExceptions {
client.post("$baseUrl/setCustomEmoji") {
parameter(userIdField, userId.long)
setBody(
@@ -127,10 +144,12 @@ fun main() {
}
}
}
P()
H3 { Text("Call server method with webAppQueryIdField") }
Button({
onClick {
scope.launchSafelyWithoutExceptions {
scope.launchLoggingDropExceptions {
handleResult({ "Clicked" }) {
client.post("${window.location.origin.removeSuffix("/")}/inline") {
parameter(webAppQueryIdField, it)
@@ -144,11 +163,40 @@ fun main() {
Text("Answer in chat button")
}
H3 { Text("Hide keyboard") }
val hideCountdown = remember { mutableStateOf<Int?>(null) }
Button({
onClick {
hideCountdown.value = 5
}
}) {
if (hideCountdown.value == null) {
Text("Hide")
} else {
Text("Hide in ${hideCountdown.value} seconds")
}
}
LaunchedEffect(hideCountdown.value) {
val value = hideCountdown.value
when {
value == null -> return@LaunchedEffect
value > 0 -> {
delay(1000)
hideCountdown.value = hideCountdown.value ?.minus(1)
}
else -> {
webApp.hideKeyboard()
hideCountdown.value = null
}
}
}
P()
H3 { Text("User info") }
Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}")
P()
Text("Alerts:")
H3 { Text("Alerts") }
Button({
onClick {
webApp.showPopup(
@@ -186,8 +234,22 @@ fun main() {
}) {
Text("Alert")
}
Button({
onClick {
webApp.showConfirm(
"This is confirm message"
) {
logsState.add(
"You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm"
)
}
}
}) {
Text("Confirm")
}
P()
H3 { Text("Write access callbacks") }
Button({
onClick {
webApp.requestWriteAccess()
@@ -206,6 +268,7 @@ fun main() {
}
P()
H3 { Text("Request contact") }
Button({
onClick {
webApp.requestContact()
@@ -220,24 +283,9 @@ fun main() {
}) {
Text("Request contact with callback")
}
P()
Button({
onClick {
webApp.showConfirm(
"This is confirm message"
) {
logsState.add(
"You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm"
)
}
}
}) {
Text("Confirm")
}
P()
H3 { Text("Closing confirmation") }
val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) }
Button({
onClick {
@@ -255,7 +303,7 @@ fun main() {
}
P()
H3 { Text("Colors") }
val headerColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateHeaderColor() {
val (r, g, b) = Random.nextUBytes(3)
@@ -280,7 +328,6 @@ fun main() {
}
P()
val backgroundColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBackgroundColor() {
val (r, g, b) = Random.nextUBytes(3)
@@ -305,7 +352,6 @@ fun main() {
}
P()
val bottomBarColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBottomBarColor() {
val (r, g, b) = Random.nextUBytes(3)
@@ -329,60 +375,6 @@ fun main() {
}
}
P()
val storageTrigger = remember { mutableStateOf<List<Pair<CloudStorageKey, CloudStorageValue>>>(emptyList()) }
fun updateCloudStorage() {
webApp.cloudStorage.getAll {
it.onSuccess {
storageTrigger.value = it.toList().sortedBy { it.first.key }
}
}
}
key(storageTrigger.value) {
storageTrigger.value.forEach { (key, value) ->
val keyState = remember { mutableStateOf(key.key) }
val valueState = remember { mutableStateOf(value.value) }
Input(InputType.Text) {
value(key.key)
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
value(value.value)
onInput { valueState.value = it.value }
}
Button({
onClick {
if (key.key != keyState.value) {
webApp.cloudStorage.remove(key)
}
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
let { // new element adding
val keyState = remember { mutableStateOf("") }
val valueState = remember { mutableStateOf("") }
Input(InputType.Text) {
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
onInput { valueState.value = it.value }
}
Button({
onClick {
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
}
remember {
webApp.apply {
@@ -432,9 +424,10 @@ fun main() {
}
}
}
P()
let { // Accelerometer
P()
let {
H3 { Text("Accelerometer") }
val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) }
webApp.onAccelerometerStarted { enabledState.value = true }
webApp.onAccelerometerStopped { enabledState.value = false }
@@ -475,7 +468,8 @@ fun main() {
}
P()
let { // Gyroscope
let {
H3 { Text("Gyroscope") }
val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) }
webApp.onGyroscopeStarted { enabledState.value = true }
webApp.onGyroscopeStopped { enabledState.value = false }
@@ -514,9 +508,10 @@ fun main() {
Text("z: ${zState.value}")
}
}
P()
let { // DeviceOrientation
P()
let {
H3 { Text("Device Orientation") }
val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) }
webApp.onDeviceOrientationStarted { enabledState.value = true }
webApp.onDeviceOrientationStopped { enabledState.value = false }
@@ -555,8 +550,181 @@ fun main() {
Text("gamma: ${gammaState.value}")
}
}
P()
H3 { Text("Cloud storage") }
val storageTrigger = remember { mutableStateOf<List<Pair<CloudStorageKey, CloudStorageValue>>>(emptyList()) }
fun updateCloudStorage() {
webApp.cloudStorage.getAll {
it.onSuccess {
storageTrigger.value = it.toList().sortedBy { it.first.key }
}
}
}
key(storageTrigger.value) {
storageTrigger.value.forEach { (key, value) ->
val keyState = remember { mutableStateOf(key.key) }
val valueState = remember { mutableStateOf(value.value) }
Input(InputType.Text) {
value(key.key)
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
value(value.value)
onInput { valueState.value = it.value }
}
Button({
onClick {
if (key.key != keyState.value) {
webApp.cloudStorage.remove(key)
}
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
let { // new element adding
val keyState = remember { mutableStateOf("") }
val valueState = remember { mutableStateOf("") }
Input(InputType.Text) {
onInput { keyState.value = it.value }
}
Input(InputType.Text) {
onInput { valueState.value = it.value }
}
Button({
onClick {
webApp.cloudStorage.set(keyState.value, valueState.value)
updateCloudStorage()
}
}) {
Text("Save")
}
}
}
P()
let { // DeviceStorage
H3 { Text("Device storage") }
val fieldKey = remember { mutableStateOf("") }
val fieldValue = remember { mutableStateOf("") }
val message = remember { mutableStateOf("") }
Div {
Text("Start type title of key. If value will be found in device storage, it will be shown in value input")
}
Input(InputType.Text) {
placeholder("Key")
value(fieldKey.value)
onInput {
fieldKey.value = it.value
webApp.deviceStorage.getItem(it.value) { e, v ->
fieldValue.value = v ?: ""
if (v == null) {
message.value = "Value for key \"${it.value}\" has not been found"
} else {
message.value = "Value for key \"${it.value}\" has been found: \"$v\""
}
}
}
}
Div {
Text("If you want to change value if typed key - just put it here")
}
Input(InputType.Text) {
placeholder("Value")
value(fieldValue.value)
onInput {
fieldValue.value = it.value
webApp.deviceStorage.setItem(fieldKey.value, it.value) { e, v ->
if (v == true) {
fieldValue.value = it.value
message.value = "Value \"${it.value}\" has been saved"
}
}
}
}
if (message.value.isNotEmpty()) {
Div { Text(message.value) }
}
}
P()
let { // DeviceStorage
H3 { Text("Secure storage") }
val fieldKey = remember { mutableStateOf("") }
val fieldValue = remember { mutableStateOf("") }
val message = remember { mutableStateOf("") }
val restorableState = remember { mutableStateOf(false) }
Div {
Text("Start type title of key. If value will be found in device storage, it will be shown in value input")
}
Input(InputType.Text) {
placeholder("Key")
value(fieldKey.value)
onInput {
fieldKey.value = it.value
webApp.secureStorage.getItem(it.value) { e, v, restorable ->
fieldValue.value = v ?: ""
restorableState.value = restorable == true
if (v == null) {
if (restorable == true) {
message.value = "Value for key \"${it.value}\" has not been found, but can be restored"
} else {
message.value = "Value for key \"${it.value}\" has not been found. Error: $e"
}
} else {
message.value = "Value for key \"${it.value}\" has been found: \"$v\""
}
}
}
}
if (restorableState.value) {
Button({
onClick {
webApp.secureStorage.restoreItem(fieldKey.value) { e, v ->
fieldValue.value = v ?: ""
if (v == null) {
message.value = "Value for key \"${fieldKey.value}\" has not been restored. Error: $e"
} else {
message.value = "Value for key \"${fieldKey.value}\" has been restored: \"$v\""
}
}
}
}) {
Text("Restore")
}
}
Div {
Text("If you want to change value if typed key - just put it here")
}
Input(InputType.Text) {
placeholder("Value")
value(fieldValue.value)
onInput {
fieldValue.value = it.value
webApp.secureStorage.setItem(fieldKey.value, it.value) { e, v ->
if (v) {
fieldValue.value = it.value
message.value = "Value \"${it.value}\" has been saved"
} else {
message.value = "Value \"${it.value}\" has not been saved. Error: $e"
}
}
}
}
if (message.value.isNotEmpty()) {
Div { Text(message.value) }
}
}
P()
H3 { Text("Events") }
EventType.values().forEach { eventType ->
when (eventType) {
EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ }

View File

@@ -1,4 +1,5 @@
import dev.inmo.kslog.common.*
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.server.createKtorServer
import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery
@@ -195,7 +196,7 @@ suspend fun main(vararg args: String) {
BotCommand("reply_markup", "Use to get reply markup keyboard with web app trigger"),
BotCommand("inline", "Use to get inline keyboard with web app trigger"),
)
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
println(getMe())

View File

@@ -14,8 +14,8 @@ allprojects {
nativePartTemplate = "${rootProject.projectDir.absolutePath}/native_template.gradle"
}
repositories {
mavenLocal()
mavenCentral()
google()
if (project.hasProperty("GITHUB_USER") && project.hasProperty("GITHUB_TOKEN")) {
maven {
url "https://maven.pkg.github.com/InsanusMokrassar/TelegramBotAPI"
@@ -26,7 +26,8 @@ allprojects {
}
}
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
maven { url "https://proxy.nexus.inmo.dev/repository/maven-releases/" }
mavenLocal()
}
}

View File

@@ -5,9 +5,9 @@ org.gradle.jvmargs=-Xmx3148m
kotlin.daemon.jvmargs=-Xmx3g -Xms500m
kotlin_version=2.1.0
telegram_bot_api_version=23.1.2
micro_utils_version=0.24.5
serialization_version=1.8.0
ktor_version=3.0.3
compose_version=1.7.3
kotlin_version=2.2.0
telegram_bot_api_version=27.1.2
micro_utils_version=0.26.2
serialization_version=1.9.0
ktor_version=3.2.3
compose_version=1.8.2

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip