diff --git a/build.gradle.kts b/build.gradle.kts index fb2fae3..78b070b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import java.net.URI plugins { - kotlin("jvm") version "1.9.23" + kotlin("jvm") version "1.9.24" id("org.jetbrains.dokka") version "1.9.20" idea `maven-publish` @@ -9,7 +9,7 @@ plugins { } group = "io.github.com6235" -version = "1.0.2" +version = "1.0.3" repositories { mavenCentral() @@ -18,9 +18,9 @@ repositories { dependencies { testImplementation(kotlin("test")) - api("org.telegram:telegrambots-longpolling:7.2.1") - api("org.telegram:telegrambots-webhook:7.2.1") - api("org.telegram:telegrambots-client:7.2.1") + implementation("org.telegram:telegrambots-longpolling:7.2.1") + implementation("org.telegram:telegrambots-webhook:7.2.1") + implementation("org.telegram:telegrambots-client:7.2.1") api("org.telegram:telegrambots-meta:7.2.1") implementation("org.slf4j:slf4j-api:2.0.13") @@ -34,6 +34,13 @@ kotlin { jvmToolchain(17) } +idea { + module { + isDownloadJavadoc = true + isDownloadSources = true + } +} + tasks.register("dokkaJavadocJar") { dependsOn(tasks.dokkaJavadoc) from(tasks.dokkaJavadoc.flatMap { it.outputDirectory }) @@ -45,13 +52,6 @@ tasks.register("sourcesJar") { archiveClassifier.set("sources") } -idea { - module { - isDownloadJavadoc = true - isDownloadSources = true - } -} - configurations { create("javadoc") create("sources") diff --git a/src/main/kotlin/CommandManager.kt b/src/main/kotlin/CommandManager.kt index 9882ea8..7721ba5 100644 --- a/src/main/kotlin/CommandManager.kt +++ b/src/main/kotlin/CommandManager.kt @@ -1,18 +1,33 @@ package io.github.com6235.tgbotter +import io.github.com6235.tgbotter.common.Bot +import io.github.com6235.tgbotter.common.Command +import io.github.com6235.tgbotter.common.CommandHandler +import io.github.com6235.tgbotter.common.Listener import org.telegram.telegrambots.meta.api.objects.message.Message import org.telegram.telegrambots.meta.generics.TelegramClient /** * Manager for all commands. Runs commands first, before any other event */ -class CommandManager(private val telegramClient: TelegramClient) { - internal val handle = Handle(this.telegramClient) +class CommandManager(private val bot: Bot) { + internal val handle = Handle(this.bot.telegramClient) + internal val commandRegex = Regex("[a-z0-9_]+") /** - * Adds a [Command] to bot's command list. Does not update the menu in Telegram. + * Adds a [Command] to bot's command list. */ - fun addCommand(command: Command) = Handle.commands.add(command) + fun addCommand(vararg commands: Command) { + Handle.commands.addAll(commands) + for (command in commands) { + if (commandRegex.matchEntire(command.name) == null) { + bot.logger.error("${command.name} has an invalid name! " + + "Command's name should contain only lowercase english letters, digits and underscores. " + + "${command.name} will not be registered as a valid command in Telegram command list." + ) + } + } + } internal class Handle(private val telegramClient: TelegramClient) : Listener { override fun onMessage(message: Message, telegramClient: TelegramClient) { diff --git a/src/main/kotlin/LongPollingBot.kt b/src/main/kotlin/LongPollingBot.kt index a83831f..a8c9d85 100644 --- a/src/main/kotlin/LongPollingBot.kt +++ b/src/main/kotlin/LongPollingBot.kt @@ -1,76 +1,57 @@ package io.github.com6235.tgbotter -import org.slf4j.LoggerFactory -import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient +import io.github.com6235.tgbotter.common.Bot +import io.github.com6235.tgbotter.common.BotCreationOptions +import io.github.com6235.tgbotter.common.Listener import org.telegram.telegrambots.longpolling.BotSession import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication import org.telegram.telegrambots.longpolling.interfaces.LongPollingUpdateConsumer +import org.telegram.telegrambots.meta.api.methods.commands.DeleteMyCommands import org.telegram.telegrambots.meta.api.objects.Update import org.telegram.telegrambots.meta.exceptions.TelegramApiException -import org.telegram.telegrambots.meta.generics.TelegramClient import java.util.concurrent.Executor import java.util.concurrent.Executors +import kotlin.system.exitProcess /** * Class for creating long-polling bots using Telegram Bot API */ -class LongPollingBot(private val options: BotCreationOptions) { - private val telegramClient: TelegramClient = OkHttpTelegramClient(this.options.token) - private val application = TelegramBotsLongPollingApplication() - private val listeners: MutableList = mutableListOf() - internal val logger = LoggerFactory.getLogger(this::class.java) +class LongPollingBot(private val options: BotCreationOptions) : Bot(options) { + override val application = TelegramBotsLongPollingApplication() private lateinit var botSession: BotSession - /** - * This bot's command manager. - * Commands run first, before any other listener (change with [BotCreationOptions.runCommandsThroughOnMessage]) - */ - val commandManager = CommandManager(this.telegramClient) - - init { - this.listeners.add(commandManager.handle) - } - - /** - * Adds a listener to the bot, so the bot can work with it - * - * @see Listener - */ - fun addListener(listener: Listener) { this.listeners.add(listener) } - internal fun getListeners(): MutableList { - val s = mutableListOf(); s.addAll(this.listeners) - return s - } - /** * Starts the bot session. */ - fun start() { + override fun start(autoSetCommands: Boolean, hookStopToShutdown: Boolean, exitOnError: Boolean) { try { botSession = application.registerBot(this.options.token, Consumer(this)) + setNameAndCommands(hookStopToShutdown, autoSetCommands) botSession.start() logger.info("Bot started successfully!") } catch (e: TelegramApiException) { logger.error("There was an error starting the bot: ${e.message}") + stop() + if (exitOnError) exitProcess(1) } } /** * Stops the bot session */ - fun stop() { + override fun stop() { try { botSession.stop() + telegramClient.execute(DeleteMyCommands.builder().build()) application.unregisterBot(this.options.token) logger.info("Bot stopped successfully!") } catch (e: TelegramApiException) { logger.error("There was an error stopping the bot: ${e.message}") } - } private class Consumer(private val bot: LongPollingBot) : LongPollingUpdateConsumer { - private val updatesProcessorExecutor: Executor = Executors.newSingleThreadExecutor() + private val updatesProcessorExecutor: Executor = Executors.newCachedThreadPool() override fun consume(updates: List) { updates.forEach { @@ -81,7 +62,7 @@ class LongPollingBot(private val options: BotCreationOptions) { fun consume(update: Update) { if (update.hasMessage() && update.message.hasText() && update.message.text.startsWith("/")) { bot.getListeners().first().onMessage(update.message, bot.telegramClient) - logUpdate(update.updateId, "Command") + afterUpdate(update, "Command", null) if (!bot.options.runCommandsThroughOnMessage) { return } @@ -90,83 +71,85 @@ class LongPollingBot(private val options: BotCreationOptions) { when { update.hasBusinessConnection() -> { it.onBusinessConnection(update.businessConnection, bot.telegramClient) - logUpdate(update.updateId, "BusinessConnection") + afterUpdate(update, "BusinessConnection", it) } update.hasBusinessMessage() -> { it.onBusinessMessage(update.businessMessage, bot.telegramClient) - logUpdate(update.updateId, "BusinessMessage") + afterUpdate(update, "BusinessMessage", it) } update.hasCallbackQuery() -> { it.onCallbackQuery(update.callbackQuery, bot.telegramClient) - logUpdate(update.updateId, "CallbackQuery") + afterUpdate(update, "CallbackQuery", it) } update.hasChannelPost() -> { it.onChannelPost(update.channelPost, bot.telegramClient) - logUpdate(update.updateId, "ChannelPost") + afterUpdate(update, "ChannelPost", it) } update.hasChatJoinRequest() -> { it.onChatJoinRequest(update.chatJoinRequest, bot.telegramClient) - logUpdate(update.updateId, "ChatJoinRequest") + afterUpdate(update, "ChatJoinRequest", it) } update.hasChatMember() -> { it.onChatMember(update.chatMember, bot.telegramClient) - logUpdate(update.updateId, "ChatMember") + afterUpdate(update, "ChatMember", it) } update.hasChosenInlineQuery() -> { it.onChosenInlineQuery(update.chosenInlineQuery, bot.telegramClient) - logUpdate(update.updateId, "ChosenInlineQuery") + afterUpdate(update, "ChosenInlineQuery", it) } update.hasDeletedBusinessMessage() -> { it.onDeletedBusinessMessage(update.deletedBusinessMessages, bot.telegramClient) - logUpdate(update.updateId, "DeletedBusinessMessage") + afterUpdate(update, "DeletedBusinessMessage", it) } update.hasEditedBusinessMessage() -> { it.onEditedBusinessMessage(update.editedBuinessMessage, bot.telegramClient) - logUpdate(update.updateId, "EditedBusinessMessage") + afterUpdate(update, "EditedBusinessMessage", it) } update.hasEditedChannelPost() -> { it.onEditedChannelPost(update.editedChannelPost, bot.telegramClient) - logUpdate(update.updateId, "EditedChannelPost") + afterUpdate(update, "EditedChannelPost", it) } update.hasEditedMessage() -> { it.onEditedMessage(update.editedMessage, bot.telegramClient) - logUpdate(update.updateId, "EditedMessage") + afterUpdate(update, "EditedMessage", it) } update.hasInlineQuery() -> { it.onInlineQuery(update.inlineQuery, bot.telegramClient) - logUpdate(update.updateId, "InlineQuery") + afterUpdate(update, "InlineQuery", it) } update.hasMessage() -> { it.onMessage(update.message, bot.telegramClient) - logUpdate(update.updateId, "Message") + afterUpdate(update, "Message", it) } update.hasMyChatMember() -> { it.onMyChatMember(update.myChatMember, bot.telegramClient) - logUpdate(update.updateId, "MyChatMember") + afterUpdate(update, "MyChatMember", it) } update.hasPoll() -> { it.onPoll(update.poll, bot.telegramClient) - logUpdate(update.updateId, "Poll") + afterUpdate(update, "Poll", it) } update.hasPollAnswer() -> { it.onPollAnswer(update.pollAnswer, bot.telegramClient) - logUpdate(update.updateId, "PollAnswer") + afterUpdate(update, "PollAnswer", it) } update.hasPreCheckoutQuery() -> { it.onPreCheckoutQuery(update.preCheckoutQuery, bot.telegramClient) - logUpdate(update.updateId, "PreCheckoutQuery") + afterUpdate(update, "PreCheckoutQuery", it) } update.hasShippingQuery() -> { it.onShippingQuery(update.shippingQuery, bot.telegramClient) - logUpdate(update.updateId, "ShippingQuery") + afterUpdate(update, "ShippingQuery", it) } } } } - private fun logUpdate(updateId: Int, type: String) { + private fun afterUpdate(update: Update, type: String, listener: Listener?) { + (listener ?: CommandManager.Handle(bot.telegramClient)).afterUpdate(update, bot.telegramClient) + if (!bot.options.logUpdates) return - bot.logger.info("$updateId - $type") + bot.logger.info("${update.updateId} - $type") } } } \ No newline at end of file diff --git a/src/main/kotlin/common/Bot.kt b/src/main/kotlin/common/Bot.kt new file mode 100644 index 0000000..f2b5edf --- /dev/null +++ b/src/main/kotlin/common/Bot.kt @@ -0,0 +1,99 @@ +package io.github.com6235.tgbotter.common + +import io.github.com6235.tgbotter.CommandManager +import io.github.com6235.tgbotter.CommandManager.Handle.Companion.commands +import org.slf4j.LoggerFactory +import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient +import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands +import org.telegram.telegrambots.meta.api.methods.description.GetMyDescription +import org.telegram.telegrambots.meta.api.methods.description.GetMyShortDescription +import org.telegram.telegrambots.meta.api.methods.description.SetMyDescription +import org.telegram.telegrambots.meta.api.methods.description.SetMyShortDescription +import org.telegram.telegrambots.meta.api.methods.name.GetMyName +import org.telegram.telegrambots.meta.api.methods.name.SetMyName +import org.telegram.telegrambots.meta.api.objects.commands.BotCommand +import org.telegram.telegrambots.meta.generics.TelegramClient + +abstract class Bot(private val options: BotCreationOptions) { + internal val telegramClient: TelegramClient = OkHttpTelegramClient(this.options.token) + protected abstract val application: AutoCloseable + private val listeners: MutableList = mutableListOf() + internal val logger = LoggerFactory.getLogger(options.loggerName) + + /** + * This bot's command manager. + * Commands run first, before any other listener. + * Also commands doesn't run through onMessage event (change with [BotCreationOptions.runCommandsThroughOnMessage]) + */ + val commandManager = CommandManager(this) + + init { + this.listeners.add(commandManager.handle) + } + + protected fun setNameAndCommands(hookStopToShutdown: Boolean, autoSetCommands: Boolean) { + val commandsToSend = mutableListOf() + for (command in commands) { + if (commandManager.commandRegex.matchEntire(command.name) != null) { + commandsToSend.add(BotCommand(command.name, command.description)) + } + } + if (commandsToSend.isNotEmpty() && autoSetCommands) + telegramClient.execute(SetMyCommands.builder().commands(commandsToSend).build()) + + val name: String = try { + telegramClient.execute(GetMyName.builder().build()).name + } catch (_: Exception) { options.botName ?: "" } + + val description: String = try { + telegramClient.execute(GetMyDescription.builder().build()).description + } catch (_: Exception) { options.botDescription ?: "" } + + val shortDescription: String = try { + telegramClient.execute(GetMyShortDescription.builder().build()).shortDescription + } catch (_: Exception) { options.botName ?: "" } + + if (options.botName != null && options.botName != name) + telegramClient.execute(SetMyName.builder().name(options.botName).build()) + + if (options.botDescription != null && options.botDescription != description) + telegramClient.execute( + SetMyDescription.builder() + .description(options.botDescription) + .build() + ) + if (options.botShortDescription != null && options.botShortDescription != shortDescription) + telegramClient.execute( + SetMyShortDescription.builder() + .shortDescription(options.botShortDescription) + .build() + ) + + if (hookStopToShutdown) { + Runtime.getRuntime().addShutdownHook(Thread { + stop() + }) + } + } + + /** + * Adds a listener to the bot, so the bot can work with it + * + * @see Listener + */ + fun addListener(vararg listener: Listener) { this.listeners.addAll(listener) } + internal fun getListeners(): MutableList { + val s = mutableListOf(); s.addAll(this.listeners) + return s + } + + /** + * Starts the bot session. + */ + abstract fun start(autoSetCommands: Boolean = true, hookStopToShutdown: Boolean = false, exitOnError: Boolean = true) + + /** + * Stops the bot session + */ + abstract fun stop() +} \ No newline at end of file diff --git a/src/main/kotlin/DataClasses.kt b/src/main/kotlin/common/DataClasses.kt similarity index 51% rename from src/main/kotlin/DataClasses.kt rename to src/main/kotlin/common/DataClasses.kt index e2157a5..2eb4f96 100644 --- a/src/main/kotlin/DataClasses.kt +++ b/src/main/kotlin/common/DataClasses.kt @@ -1,4 +1,4 @@ -package io.github.com6235.tgbotter +package io.github.com6235.tgbotter.common import org.telegram.telegrambots.meta.api.objects.message.Message import org.telegram.telegrambots.meta.generics.TelegramClient @@ -7,21 +7,31 @@ import org.telegram.telegrambots.meta.generics.TelegramClient * Options for creating bots. * * @property token Bot token + * @property botName Bot's name (doesn't update name if `null`) + * @property botDescription Bot's description (doesn't update name if `null`) + * @property botShortDescription Bot's short description (doesn't update name if `null`) * @property runCommandsThroughOnMessage Should commands be run through onMessage event? + * @property logUpdates Should bot log all updates? + * @property loggerName Name for bot's SLF4J logger */ data class BotCreationOptions( val token: String, + val botName: String? = null, + val botDescription: String? = null, + val botShortDescription: String? = null, val runCommandsThroughOnMessage: Boolean = false, val logUpdates: Boolean = false, + val loggerName: String = Bot::class.qualifiedName!!, ) /** * Class for commands * * @property name Name of the command, so bot will be able to identify it + * @property description Description of the command that will be shown in Telegram's command menu * @property handler Command handler */ -data class Command(val name: String, val handler: CommandHandler.() -> Unit) +data class Command(val name: String, val description: String, val handler: CommandHandler.() -> Unit) /** * Class, that is given to all commands. diff --git a/src/main/kotlin/Listener.kt b/src/main/kotlin/common/Listener.kt similarity index 92% rename from src/main/kotlin/Listener.kt rename to src/main/kotlin/common/Listener.kt index 1a37c13..e528443 100644 --- a/src/main/kotlin/Listener.kt +++ b/src/main/kotlin/common/Listener.kt @@ -1,7 +1,8 @@ -package io.github.com6235.tgbotter +package io.github.com6235.tgbotter.common import org.telegram.telegrambots.meta.api.objects.CallbackQuery import org.telegram.telegrambots.meta.api.objects.ChatJoinRequest +import org.telegram.telegrambots.meta.api.objects.Update import org.telegram.telegrambots.meta.api.objects.business.BusinessConnection import org.telegram.telegrambots.meta.api.objects.business.BusinessMessagesDeleted import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMemberUpdated @@ -17,7 +18,7 @@ import org.telegram.telegrambots.meta.generics.TelegramClient /** * Interface for creating event listeners. * - * After creating your event listener, remember to add them to your [LongPollingBot] using [LongPollingBot.addListener] + * After creating your event listener, remember to add them to your [Bot] using [Bot.addListener] */ interface Listener { fun onBusinessConnection(businessConnection: BusinessConnection, telegramClient: TelegramClient) {} @@ -38,4 +39,5 @@ interface Listener { fun onPollAnswer(pollAnswer: PollAnswer, telegramClient: TelegramClient) {} fun onPreCheckoutQuery(preCheckoutQuery: PreCheckoutQuery, telegramClient: TelegramClient) {} fun onShippingQuery(shippingQuery: ShippingQuery, telegramClient: TelegramClient) {} + fun afterUpdate(update: Update, telegramClient: TelegramClient) {} }