Merge pull request from InsanusMokrassar/25.0.0

25.0.0
This commit is contained in:
2025-06-14 23:35:44 +06:00
committed by GitHub
13 changed files with 650 additions and 131 deletions
BusinessConnectionsBot/src/main/kotlin
CustomBot/src/main/kotlin
InlineQueriesBot
build.gradle
src
commonMain
kotlin
KeyboardsBot/KeyboardsBotLib/src/commonMain/kotlin
RandomFileSenderBot
ResenderBot/ResenderBotLib/src/commonMain/kotlin
StickerInfoBot/StickerInfoBotLib/src/commonMain/kotlin
WebApp
build.gradle
src
jsMain
kotlin
jvmMain
build.gradlegradle.properties

@ -2,24 +2,68 @@ import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog 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.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.pinChatMessage
import dev.inmo.tgbotapi.extensions.api.chat.modify.unpinChatMessage 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.get.getBusinessConnection
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send 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.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* 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.ifAccessibleMessage
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull 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.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.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.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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json
suspend fun main(args: Array<String>) { suspend fun main(args: Array<String>) {
val botToken = args.first() val botToken = args.first()
@ -34,6 +78,7 @@ suspend fun main(args: Array<String>) {
} }
val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>() val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>()
val chatsBusinessConnections = mutableMapOf<ChatId, BusinessConnectionId>()
val businessConnectionsChatsMutex = Mutex() val businessConnectionsChatsMutex = Mutex()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) { telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
@ -43,12 +88,14 @@ suspend fun main(args: Array<String>) {
onBusinessConnectionEnabled { onBusinessConnectionEnabled {
businessConnectionsChatsMutex.withLock { businessConnectionsChatsMutex.withLock {
businessConnectionsChats[it.id] = it.userChatId businessConnectionsChats[it.id] = it.userChatId
chatsBusinessConnections[it.userChatId] = it.id
} }
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been enabled") send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been enabled")
} }
onBusinessConnectionDisabled { onBusinessConnectionDisabled {
businessConnectionsChatsMutex.withLock { businessConnectionsChatsMutex.withLock {
businessConnectionsChats.remove(it.id) businessConnectionsChats.remove(it.id)
chatsBusinessConnections.remove(it.userChatId)
} }
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been disabled") send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been disabled")
} }
@ -71,7 +118,20 @@ suspend fun main(args: Array<String>) {
if (businessContentMessage.sentByBusinessConnectionOwner) { if (businessContentMessage.sentByBusinessConnectionOwner) {
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat") reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
} else { } 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 +158,318 @@ suspend fun main(args: Array<String>) {
} }
send(businessConnectionOwnerChat, "There are several removed messages in chat ${it.chat.id}: ${it.messageIds}") 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"
}
}
}
}.second.join() }.second.join()
} }

@ -4,6 +4,7 @@ import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
@ -65,6 +66,7 @@ suspend fun main(vararg args: String) {
onCommand("start") { onCommand("start") {
println(data.update) println(data.update)
println(data.commonMessage) println(data.commonMessage)
println(getChat(it.chat))
} }
onCommand( onCommand(

@ -12,14 +12,16 @@ plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
} }
apply plugin: 'application'
mainClassName="InlineQueriesBotKt"
apply from: "$nativePartTemplate" apply from: "$nativePartTemplate"
kotlin { kotlin {
jvm() jvm {
binaries {
executable {
mainClass.set("InlineQueriesBotKt")
}
}
}
sourceSets { sourceSets {
commonMain { commonMain {
@ -27,12 +29,9 @@ kotlin {
implementation kotlin('stdlib') implementation kotlin('stdlib')
api "dev.inmo:tgbotapi:$telegram_bot_api_version" 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.1.0'
}

@ -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.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
@ -59,7 +59,7 @@ suspend fun doInlineQueriesBot(token: String) {
reply(message, deepLink) reply(message, deepLink)
} }
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it) println(it)
} }

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

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

@ -1,4 +1,4 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat
@ -31,15 +31,15 @@ suspend fun activateResenderBot(
it.content.createResend( it.content.createResend(
chat.id, chat.id,
messageThreadId = it.threadIdOrNull, messageThreadId = it.threadIdOrNull,
replyParameters = it.replyInfo ?.messageMeta ?.let { meta -> replyParameters = it.replyInfo?.messageMeta?.let { meta ->
val quote = it.withContentOrNull<TextContent>() ?.content ?.quote val quote = it.withContentOrNull<TextContent>()?.content?.quote
ReplyParameters( ReplyParameters(
meta, meta,
entities = quote ?.textSources ?: emptyList(), entities = quote?.textSources ?: emptyList(),
quotePosition = quote ?.position quotePosition = quote?.position
) )
}, },
effectId = it.possiblyWithEffectMessageOrNull() ?.effectId effectId = it.possiblyWithEffectMessageOrNull()?.effectId
) )
) { ) {
it.forEach(print) it.forEach(print)
@ -49,7 +49,7 @@ suspend fun activateResenderBot(
println("Answer info: $answer") println("Answer info: $answer")
} }
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it) println(it)
} }
print(bot.getMe()) print(bot.getMe())

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

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

@ -1,6 +1,5 @@
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.userIdField import dev.inmo.tgbotapi.types.userIdField
import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.types.webAppQueryIdField
import dev.inmo.tgbotapi.webapps.* import dev.inmo.tgbotapi.webapps.*
@ -23,10 +22,12 @@ import kotlinx.dom.appendElement
import kotlinx.dom.appendText import kotlinx.dom.appendText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.compose.web.attributes.InputType 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.Color as ComposeColor
import org.jetbrains.compose.web.css.backgroundColor 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.*
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposable import org.jetbrains.compose.web.renderComposable
@ -39,6 +40,13 @@ fun HTMLElement.log(text: String) {
appendElement("p", {}) 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) @OptIn(ExperimentalUnsignedTypes::class)
fun main() { fun main() {
console.log("Web app started") console.log("Web app started")
@ -46,6 +54,14 @@ fun main() {
val baseUrl = window.location.origin.removeSuffix("/") val baseUrl = window.location.origin.removeSuffix("/")
renderComposable("root") { renderComposable("root") {
Style(RootStyleSheet)
DisposableEffect(null) {
scopeElement.classList.add(RootStyleSheet.rootClass)
onDispose {
scopeElement.classList.remove(RootStyleSheet.rootClass)
}
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val isSafeState = remember { mutableStateOf<Boolean?>(null) } val isSafeState = remember { mutableStateOf<Boolean?>(null) }
val logsState = remember { mutableStateListOf<Any?>() } val logsState = remember { mutableStateListOf<Any?>() }
@ -87,6 +103,7 @@ fun main() {
P() P()
Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}") Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}")
H3 { Text("Emoji status management") }
val emojiStatusAccessState = remember { mutableStateOf(false) } val emojiStatusAccessState = remember { mutableStateOf(false) }
webApp.onEmojiStatusAccessRequested { webApp.onEmojiStatusAccessRequested {
emojiStatusAccessState.value = it.isAllowed emojiStatusAccessState.value = it.isAllowed
@ -110,7 +127,7 @@ fun main() {
userId ?.let { userId -> userId ?.let { userId ->
Button({ Button({
onClick { onClick {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
client.post("$baseUrl/setCustomEmoji") { client.post("$baseUrl/setCustomEmoji") {
parameter(userIdField, userId.long) parameter(userIdField, userId.long)
setBody( setBody(
@ -127,10 +144,12 @@ fun main() {
} }
} }
} }
P()
H3 { Text("Call server method with webAppQueryIdField") }
Button({ Button({
onClick { onClick {
scope.launchSafelyWithoutExceptions { scope.launchLoggingDropExceptions {
handleResult({ "Clicked" }) { handleResult({ "Clicked" }) {
client.post("${window.location.origin.removeSuffix("/")}/inline") { client.post("${window.location.origin.removeSuffix("/")}/inline") {
parameter(webAppQueryIdField, it) parameter(webAppQueryIdField, it)
@ -145,10 +164,11 @@ fun main() {
} }
P() P()
H3 { Text("User info") }
Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}") Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}")
P() P()
Text("Alerts:") H3 { Text("Alerts") }
Button({ Button({
onClick { onClick {
webApp.showPopup( webApp.showPopup(
@ -186,8 +206,22 @@ fun main() {
}) { }) {
Text("Alert") 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() P()
H3 { Text("Write access callbacks") }
Button({ Button({
onClick { onClick {
webApp.requestWriteAccess() webApp.requestWriteAccess()
@ -206,6 +240,7 @@ fun main() {
} }
P() P()
H3 { Text("Request contact") }
Button({ Button({
onClick { onClick {
webApp.requestContact() webApp.requestContact()
@ -220,24 +255,9 @@ fun main() {
}) { }) {
Text("Request contact with callback") 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() P()
H3 { Text("Closing confirmation") }
val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) } val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) }
Button({ Button({
onClick { onClick {
@ -255,7 +275,7 @@ fun main() {
} }
P() P()
H3 { Text("Colors") }
val headerColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } val headerColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateHeaderColor() { fun updateHeaderColor() {
val (r, g, b) = Random.nextUBytes(3) val (r, g, b) = Random.nextUBytes(3)
@ -280,7 +300,6 @@ fun main() {
} }
P() P()
val backgroundColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } val backgroundColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBackgroundColor() { fun updateBackgroundColor() {
val (r, g, b) = Random.nextUBytes(3) val (r, g, b) = Random.nextUBytes(3)
@ -305,7 +324,6 @@ fun main() {
} }
P() P()
val bottomBarColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } val bottomBarColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) }
fun updateBottomBarColor() { fun updateBottomBarColor() {
val (r, g, b) = Random.nextUBytes(3) val (r, g, b) = Random.nextUBytes(3)
@ -329,60 +347,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 { remember {
webApp.apply { webApp.apply {
@ -432,9 +396,10 @@ fun main() {
} }
} }
} }
P()
let { // Accelerometer P()
let {
H3 { Text("Accelerometer") }
val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) } val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) }
webApp.onAccelerometerStarted { enabledState.value = true } webApp.onAccelerometerStarted { enabledState.value = true }
webApp.onAccelerometerStopped { enabledState.value = false } webApp.onAccelerometerStopped { enabledState.value = false }
@ -475,7 +440,8 @@ fun main() {
} }
P() P()
let { // Gyroscope let {
H3 { Text("Gyroscope") }
val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) } val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) }
webApp.onGyroscopeStarted { enabledState.value = true } webApp.onGyroscopeStarted { enabledState.value = true }
webApp.onGyroscopeStopped { enabledState.value = false } webApp.onGyroscopeStopped { enabledState.value = false }
@ -514,9 +480,10 @@ fun main() {
Text("z: ${zState.value}") Text("z: ${zState.value}")
} }
} }
P()
let { // DeviceOrientation P()
let {
H3 { Text("Device Orientation") }
val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) } val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) }
webApp.onDeviceOrientationStarted { enabledState.value = true } webApp.onDeviceOrientationStarted { enabledState.value = true }
webApp.onDeviceOrientationStopped { enabledState.value = false } webApp.onDeviceOrientationStopped { enabledState.value = false }
@ -555,8 +522,181 @@ fun main() {
Text("gamma: ${gammaState.value}") 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() 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 -> EventType.values().forEach { eventType ->
when (eventType) { when (eventType) {
EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ } EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ }

@ -1,4 +1,5 @@
import dev.inmo.kslog.common.* import dev.inmo.kslog.common.*
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.server.createKtorServer import dev.inmo.micro_utils.ktor.server.createKtorServer
import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery 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("reply_markup", "Use to get reply markup keyboard with web app trigger"),
BotCommand("inline", "Use to get inline 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(it)
} }
println(getMe()) println(getMe())

@ -14,8 +14,8 @@ allprojects {
nativePartTemplate = "${rootProject.projectDir.absolutePath}/native_template.gradle" nativePartTemplate = "${rootProject.projectDir.absolutePath}/native_template.gradle"
} }
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
google()
if (project.hasProperty("GITHUB_USER") && project.hasProperty("GITHUB_TOKEN")) { if (project.hasProperty("GITHUB_USER") && project.hasProperty("GITHUB_TOKEN")) {
maven { maven {
url "https://maven.pkg.github.com/InsanusMokrassar/TelegramBotAPI" 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()
} }
} }

@ -5,9 +5,9 @@ org.gradle.jvmargs=-Xmx3148m
kotlin.daemon.jvmargs=-Xmx3g -Xms500m kotlin.daemon.jvmargs=-Xmx3g -Xms500m
kotlin_version=2.1.10 kotlin_version=2.1.20
telegram_bot_api_version=23.2.0 telegram_bot_api_version=25.0.0
micro_utils_version=0.24.6 micro_utils_version=0.25.3
serialization_version=1.8.0 serialization_version=1.8.0
ktor_version=3.1.0 ktor_version=3.1.1
compose_version=1.7.3 compose_version=1.7.3