diff --git a/FSMBot/README.md b/FSMBot/README.md new file mode 100644 index 0000000..9ee12ce --- /dev/null +++ b/FSMBot/README.md @@ -0,0 +1,10 @@ +# FSM + +This bot contains an example of working with FSM included in project +[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) + +## Launch + +```bash +../gradlew run --args="BOT_TOKEN" +``` diff --git a/FSMBot/build.gradle b/FSMBot/build.gradle new file mode 100644 index 0000000..090f499 --- /dev/null +++ b/FSMBot/build.gradle @@ -0,0 +1,22 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName="SimpleFSMBotKt" + + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + + implementation "dev.inmo:tgbotapi:$telegram_bot_api_version" + implementation "dev.inmo:micro_utils.fsm.common:$micro_utils_version" +} diff --git a/FSMBot/src/main/kotlin/SimpleFSMBot.kt b/FSMBot/src/main/kotlin/SimpleFSMBot.kt new file mode 100644 index 0000000..28912c9 --- /dev/null +++ b/FSMBot/src/main/kotlin/SimpleFSMBot.kt @@ -0,0 +1,90 @@ +import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.micro_utils.fsm.common.dsl.buildFSM +import dev.inmo.micro_utils.fsm.common.dsl.strictlyOn +import dev.inmo.tgbotapi.extensions.api.send.media.sendMediaGroup +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.command +import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams +import dev.inmo.tgbotapi.extensions.utils.formatting.* +import dev.inmo.tgbotapi.extensions.utils.shortcuts.chat +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +sealed interface State : State +data class ExpectContentOrStopState(override val context: ChatId, val sourceMessage: CommonMessage) : State +data class StopState(override val context: ChatId) : State + +fun TextContent.containsStopCommand() = parseCommandsWithParams().keys.firstOrNull { it == "stop" } != null + +suspend fun main(args: Array) { + val botToken = args.first() + + telegramBotWithBehaviour(botToken, CoroutineScope(Dispatchers.IO)) { + val fsm = buildFSM { + strictlyOn { + sendMessage( + it.context, + buildEntities { + +"Send me some content or " + botCommand("stop") + +" if you want to stop sending" + } + ) + + val content = oneOf( + parallel { + waitContentMessage(includeMediaGroups = false) { if (chat.id == it.context) content else null }.also(::println) + }, + parallel { + waitMediaGroup { chat ?.id == it.context }.also(::println) + }, + parallel { + waitText { if (content.containsStopCommand()) content else null }.also(::println) + } + ).first() + + when { + content is TextContent && content.containsStopCommand() -> StopState(it.context) // assume we got "stop" command + content is List<*> -> { // assume it is media group + val casted = (content as List) + + reply(it.sourceMessage, "Ok, I got this media group and now will resend it to you") + sendMediaGroup(it.context, casted.map { it.toMediaGroupMemberInputMedia() }) + + it + } + content is MessageContent -> { + + reply(it.sourceMessage, "Ok, I got this content and now will resend it to you") + execute(content.createResend(it.context)) + + it + } + else -> { + sendMessage(it.context, "Unknown internal error") + it + } + } + } + strictlyOn { + sendMessage(it.context, "You have stopped sending of content") + + null + } + } + + command("start") { + fsm.startChain(ExpectContentOrStopState(it.chat.id, it)) + } + + fsm.start(this) + }.second.join() +} diff --git a/FilesLoaderBot/src/main/kotlin/FilesLoaderBot.kt b/FilesLoaderBot/src/main/kotlin/FilesLoaderBot.kt index a3acbec..d6b6a1e 100644 --- a/FilesLoaderBot/src/main/kotlin/FilesLoaderBot.kt +++ b/FilesLoaderBot/src/main/kotlin/FilesLoaderBot.kt @@ -1,14 +1,12 @@ -import dev.inmo.micro_utils.coroutines.safely -import dev.inmo.tgbotapi.bot.Ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.downloadFile import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo -import dev.inmo.tgbotapi.extensions.utils.flatMap -import dev.inmo.tgbotapi.extensions.utils.shortcuts.* -import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling -import dev.inmo.tgbotapi.types.message.content.abstracts.MediaContent +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviour +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMedia import dev.inmo.tgbotapi.utils.filenameFromUrl -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import java.io.File /** @@ -19,24 +17,15 @@ suspend fun main(args: Array) { val directoryOrFile = args.getOrNull(1) ?.let { File(it) } ?: File("") directoryOrFile.mkdirs() - val bot = telegramBot(botToken) - val scope = CoroutineScope(Dispatchers.Default) - - bot.longPolling(scope = scope) { - val flow = merge ( - filterContentMessages(), - mediaGroupMessages().flatMap() - ) - flow.onEach { - safely({ it.printStackTrace() }) { - val pathedFile = bot.getFileAdditionalInfo(it.content.media) - File(directoryOrFile, pathedFile.filePath.filenameFromUrl).apply { - createNewFile() - writeBytes(bot.downloadFile(pathedFile)) - } + telegramBotWithBehaviour(botToken, CoroutineScope(Dispatchers.IO)) { + onMedia(includeMediaGroups = true) { + val pathedFile = bot.getFileAdditionalInfo(it.content.media) + val file = File(directoryOrFile, pathedFile.filePath.filenameFromUrl).apply { + createNewFile() + writeBytes(bot.downloadFile(pathedFile)) } - }.launchIn(scope) - } - - scope.coroutineContext[Job]!!.join() + reply(it, "Saved to ${file.absolutePath}") + } + onContentMessage { println(it) } + }.second.join() } diff --git a/ForwardInfoSenderBot/src/main/kotlin/ForwardInfoSenderBot.kt b/ForwardInfoSenderBot/src/main/kotlin/ForwardInfoSenderBot.kt index 04230e4..b3b93ae 100644 --- a/ForwardInfoSenderBot/src/main/kotlin/ForwardInfoSenderBot.kt +++ b/ForwardInfoSenderBot/src/main/kotlin/ForwardInfoSenderBot.kt @@ -1,17 +1,12 @@ -import dev.inmo.micro_utils.coroutines.safely +import dev.inmo.micro_utils.coroutines.defaultSafelyExceptionHandler import dev.inmo.tgbotapi.bot.Ktor.telegramBot -import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage -import dev.inmo.tgbotapi.extensions.api.telegramBot -import dev.inmo.tgbotapi.extensions.utils.* -import dev.inmo.tgbotapi.extensions.utils.formatting.codeMarkdownV2 -import dev.inmo.tgbotapi.extensions.utils.formatting.regularMarkdownV2 -import dev.inmo.tgbotapi.extensions.utils.shortcuts.mediaGroupMessages -import dev.inmo.tgbotapi.extensions.utils.updates.asContentMessagesFlow -import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.longPolling +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviour +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage +import dev.inmo.tgbotapi.extensions.utils.formatting.* import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 import dev.inmo.tgbotapi.types.message.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* /** * This bot will always return message about forwarder. In cases when sent message was not a forward message it will @@ -20,26 +15,25 @@ import kotlinx.coroutines.flow.* suspend fun main(vararg args: String) { val botToken = args.first() - val bot = telegramBot(botToken) - - val scope = CoroutineScope(Dispatchers.Default) - - bot.longPolling(scope = scope) { - (merge(messageFlow.asContentMessagesFlow(), mediaGroupMessages(scope).flatMap())).mapNotNull { it.asPossiblyForwardedMessage() }.onEach { message -> - safely({ it.printStackTrace() }) { - val toAnswer = when (val forwardInfo = message.forwardInfo) { - null -> "There is no forward info" - is AnonymousForwardInfo -> "Anonymous user which signed as \"${forwardInfo.senderName.codeMarkdownV2()}\"" - is UserForwardInfo -> forwardInfo.from.let { user -> - "User ${user.id.chatId.toString().codeMarkdownV2()} " + "(${user.firstName} ${user.lastName}: ${user.username ?.username ?: "Without username"})".regularMarkdownV2() + telegramBotWithBehaviour(botToken, CoroutineScope(Dispatchers.IO)) { + onContentMessage(includeMediaGroups = true) { + val toAnswer = buildEntities { + when (val forwardInfo = it.forwardInfo) { + null -> +"There is no forward info" + is AnonymousForwardInfo -> { + regular("Anonymous user which signed as \"") + code(forwardInfo.senderName) + "\"" } - is ForwardFromChannelInfo -> "Channel (".regularMarkdownV2() + (forwardInfo.channelChat).title.codeMarkdownV2() + ")".regularMarkdownV2() - is ForwardFromSupergroupInfo -> "Supergroup (".regularMarkdownV2() + (forwardInfo.group).title.codeMarkdownV2() + ")".regularMarkdownV2() + is UserForwardInfo -> { + val user = forwardInfo.from + regular("User ") + code(user.id.chatId.toString()) + " (${user.firstName} ${user.lastName}: ${user.username ?.username ?: "Without username"})" + } + is ForwardFromChannelInfo -> regular("Channel (") + code((forwardInfo.channelChat).title) + ")" + is ForwardFromSupergroupInfo -> regular("Supergroup (") + code((forwardInfo.group).title) + ")" } - bot.sendTextMessage(message.chat, toAnswer, MarkdownV2) } - }.launchIn(scope) - } - - scope.coroutineContext[Job]!!.join() + reply(it, toAnswer) + coroutineContext.job.invokeOnCompletion { println("completance of onContentMessage") } + } + coroutineContext.job.invokeOnCompletion { println("Completed :)") } + }.second.join() } diff --git a/HelloBot/src/main/kotlin/HelloBot.kt b/HelloBot/src/main/kotlin/HelloBot.kt index 52eaebf..3f171a5 100644 --- a/HelloBot/src/main/kotlin/HelloBot.kt +++ b/HelloBot/src/main/kotlin/HelloBot.kt @@ -3,6 +3,8 @@ import dev.inmo.tgbotapi.extensions.api.chat.get.getChat import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage import dev.inmo.tgbotapi.bot.Ktor.telegramBot +import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviour +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage import dev.inmo.tgbotapi.extensions.utils.asChannelChat import dev.inmo.tgbotapi.extensions.utils.formatting.linkMarkdownV2 import dev.inmo.tgbotapi.extensions.utils.formatting.textMentionMarkdownV2 @@ -21,41 +23,30 @@ import kotlinx.coroutines.flow.onEach suspend fun main(vararg args: String) { val botToken = args.first() - val bot = telegramBot(botToken) - - val scope = CoroutineScope(Dispatchers.Default) - - bot.longPolling(scope = scope) { - messageFlow.onEach { - safely { - val message = it.data - val chat = message.chat - val answerText = "Oh, hi, " + when (chat) { - is PrivateChat -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) - is User -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) - is SupergroupChat -> (chat.username ?.username ?: bot.getChat(chat).inviteLink) ?.let { - chat.title.linkMarkdownV2(it) - } ?: chat.title - is GroupChat -> bot.getChat(chat).inviteLink ?.let { - chat.title.linkMarkdownV2(it) - } ?: chat.title - else -> "Unknown :(".escapeMarkdownV2Common() - } - bot.reply( - message, - answerText, - MarkdownV2 - ) + telegramBotWithBehaviour(botToken, CoroutineScope(Dispatchers.IO)) { + onContentMessage { message -> + val chat = message.chat + if (chat is ChannelChat) { + val answer = "Hi everybody in this channel \"${chat.title}\"" + sendTextMessage(chat, answer, MarkdownV2) + return@onContentMessage } - }.launchIn(scope) - channelPostFlow.onEach { - safely { - val chat = it.data.chat - val message = "Hi everybody in this channel \"${(chat.asChannelChat()) ?.title}\"" - bot.sendTextMessage(chat, message, MarkdownV2) + val answerText = "Oh, hi, " + when (chat) { + is PrivateChat -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) + is User -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) + is SupergroupChat -> (chat.username ?.username ?: getChat(chat).inviteLink) ?.let { + chat.title.linkMarkdownV2(it) + } ?: chat.title + is GroupChat -> bot.getChat(chat).inviteLink ?.let { + chat.title.linkMarkdownV2(it) + } ?: chat.title + else -> "Unknown :(".escapeMarkdownV2Common() } - }.launchIn(scope) - } - - scope.coroutineContext[Job]!!.join() -} \ No newline at end of file + reply( + message, + answerText, + MarkdownV2 + ) + } + }.second.join() +} diff --git a/gradle.properties b/gradle.properties index 7dc6ed8..ffda2a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,7 @@ kotlin.code.style=official org.gradle.parallel=true -kotlin_version=1.5.10 -telegram_bot_api_version=0.35.0 + +kotlin_version=1.5.20 +telegram_bot_api_version=0.35.1 +micro_utils_version=0.5.15 diff --git a/settings.gradle b/settings.gradle index 48cdeb2..fb925d4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,3 +6,4 @@ include ":FilesLoaderBot" include ":ResenderBot:ResenderBotLib" include ":ResenderBot:jvm_launcher" include ":SlotMachineDetectorBot" +include ":FSMBot"