diff --git a/README.md b/README.md index 9aca713..9e65424 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ |:-----:|:------------|:----:|:--------:|:-------:| | enable | If plugin should be enabled | `boolean` | :x: | `true` | | botToken | Telegram bot token ([How to create bot](https://core.telegram.org/bots#3-how-do-i-create-a-bot)) | `string` | :heavy_check_mark: | - | -| botUsername | Telegram bot username | string | :heavy_check_mark: | - | +| botUsername | Telegram bot username | `string` | :heavy_check_mark: | - | | chats | Chats, where bot will work (to prevent using bot by unknown chats) | `number[] or string[]` | :heavy_check_mark: | `[]` | | serverStartMessage | What will be sent to chats when server starts | `string` | :x: | `'Server started.'` | | serverStopMessage | What will be sent to chats when server stops | `string` | :x: | `'Server stopped.'` | @@ -35,7 +35,7 @@ | commands | Dictionary of command text used in Telegram bot | `Map` | :x: | See default config | | telegramMessageFormat | Format string for TGtoMC chat message | `string` | :x: | See default config | -## Commands: +## Telegram bot commands: Commands are customizeable through config, but there are default values for them as well @@ -48,4 +48,10 @@ Commands are customizeable through config, but there are default values for them Must contain `%username%` and `%message` inside. You can customize message color with it. See [message color codes](https://www.digminecraft.com/lists/color_list_pc.php). -P. S.: related to [this issue](https://github.com/kraftwerk28/spigot-tg-bridge/issues/6) \ No newline at end of file +P. S.: related to [this issue](https://github.com/kraftwerk28/spigot-tg-bridge/issues/6) + +## Plugin commands: + +| Command | Description | +|:-------:|:------------| +| `/tgbridge_reload` | Reload plugin configuration w/o need to stop the server. Works only through server console | diff --git a/build.gradle b/build.gradle index 92dd6b0..cde8754 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,7 @@ plugins { group 'org.kraftwerk28' def pluginConfigFile = new File("src/main/resources/plugin.yml").newInputStream() def cfg = (Map)new Yaml().load(pluginConfigFile) -def v = cfg.get("version") -version "$v-SNAPSHOT" +version cfg.get("version") repositories { maven { @@ -42,6 +41,10 @@ task cpArtifacts(type: Copy) { from shadowJar into "$homeDir/$plugDir" } + +shadowJar { + archiveClassifier = null +} shadowJar.finalizedBy cpArtifacts dependencies { diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt index 1ff346e..de8b700 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt @@ -1,20 +1,14 @@ package org.kraftwerk28.spigot_tg_bridge -import org.kraftwerk28.spigot_tg_bridge.Constants as C +import org.bukkit.configuration.file.YamlConfiguration -class Commands(plugin: Plugin) { +class Commands(yamlCfg: YamlConfiguration) { val time: String val online: String init { - plugin.config.run { - time = getString( - C.FIELDS.COMMANDS.TIME, - C.DEFS.COMMANDS.TIME - )!! - online = getString( - C.FIELDS.COMMANDS.ONLINE, - C.DEFS.COMMANDS.ONLINE - )!! + yamlCfg.run { + time = getString("commands.time", "time")!! + online = getString("commands.online", "online")!! } } } \ No newline at end of file diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt new file mode 100644 index 0000000..a0af498 --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt @@ -0,0 +1,25 @@ +package org.kraftwerk28.spigot_tg_bridge + +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender +import org.bukkit.command.ConsoleCommandSender +import org.kraftwerk28.spigot_tg_bridge.Constants as C + +class CommandHandler(private val plugin: Plugin) : CommandExecutor { + override fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array + ): Boolean { + if (sender !is ConsoleCommandSender) return false + return when (label) { + C.COMMANDS.PLUGIN_RELOAD -> { + plugin.reload() + true + } + else -> false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt new file mode 100644 index 0000000..fd48d6f --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt @@ -0,0 +1,80 @@ +package org.kraftwerk28.spigot_tg_bridge + +import org.bukkit.configuration.file.YamlConfiguration +import java.io.File +import org.kraftwerk28.spigot_tg_bridge.Constants as C + +class Configuration(plugin: Plugin) { + private lateinit var yamlCfg: YamlConfiguration + + var isEnabled: Boolean = false + var logFromMCtoTG: Boolean = false + var telegramMessageFormat: String = "" + var serverStartMessage: String? = null + var serverStopMessage: String? = null + + // Telegram bot stuff + var botToken: String = "" + var botUsername: String = "" + var allowedChats: List = listOf() + var logFromTGtoMC: Boolean = false + var allowWebhook: Boolean = false + var webhookConfig: Map? = null + + var logJoinLeave: Boolean = false + var joinString: String? = null + var leaveString: String? = null + var logDeath: Boolean = false + var logPlayerAsleep: Boolean = false + var onlineString: String = "" + var nobodyOnlineString: String = "" + + lateinit var commands: Commands + + init { + reload(plugin) + } + + fun reload(plugin: Plugin) { + val cfgFile = File(plugin.dataFolder, C.configFilename); + if (!cfgFile.exists()) { + cfgFile.parentFile.mkdirs() + plugin.saveResource(C.configFilename, false); + throw Exception() + } + + yamlCfg = YamlConfiguration() + yamlCfg.load(cfgFile) + + yamlCfg.run { + isEnabled = getBoolean("enable", true) + logFromTGtoMC = getBoolean("logFromTGtoMC", true) + logFromMCtoTG = getBoolean("logFromMCtoTG", true) + telegramMessageFormat = getString("telegramMessageFormat", "<%username%>: %message%")!! + allowedChats = getLongList("chats") + serverStartMessage = getString("serverStartMessage") + serverStopMessage = getString("serverStopMessage") + + botToken = getString("botToken") ?: throw Exception(C.WARN.noToken) + botUsername = getString("botUsername") ?: throw Exception(C.WARN.noUsername) + + allowWebhook = getBoolean("useWebhook", false) + val whCfg = get("webhookConfig") + if (whCfg is Map<*, *>) { + @Suppress("UNCHECKED_CAST") + webhookConfig = whCfg as Map? + } + + logJoinLeave = getBoolean("logJoinLeave", false) + onlineString = getString("strings.online", "Online")!! + nobodyOnlineString = getString("strings.offline", "Nobody online")!! + joinString = getString("strings.joined", "joined") + leaveString = getString("strings.left", "left") + logDeath = getBoolean("logPlayerDeath", false) + logPlayerAsleep = getBoolean("logPlayerAsleep", false) + + } + + commands = Commands(yamlCfg) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt index 90e05ad..371c746 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt @@ -2,56 +2,15 @@ package org.kraftwerk28.spigot_tg_bridge object Constants { const val configFilename = "config.yml" - // Config field names - object FIELDS { - const val ENABLE = "enable" - const val BOT_TOKEN = "botToken" - const val BOT_USERNAME = "botUsername" - const val ALLOWED_CHATS = "chats" - const val LOG_FROM_MC_TO_TG = "logFromMCtoTG" - const val LOG_FROM_TG_TO_MC = "logFromTGtoMC" - const val SERVER_START_MSG = "serverStartMessage" - const val SERVER_STOP_MSG = "serverStopMessage" - const val LOG_JOIN_LEAVE = "logJoinLeave" - const val LOG_PLAYER_DEATH = "logPlayerDeath" - const val LOG_PLAYER_ASLEEP = "logPlayerAsleep" - const val USE_WEBHOOK = "useWebhook" - const val WEBHOOK_CONFIG = "webhookConfig" - object STRINGS { - const val ONLINE = "strings.online" - const val OFFLINE = "strings.offline" - const val JOINED = "strings.joined" - const val LEFT = "strings.left" - } - object COMMANDS { - const val TIME = "commands.time" - const val ONLINE = "commands.online" - } - const val TELEGRAM_MESSAGE_FORMAT = "telegramMessageFormat" - } - object DEFS { - const val logFromMCtoTG = false - const val logFromTGtoMC = false - const val logJoinLeave = false - const val logPlayerDeath = false - const val logPlayerAsleep = false - object COMMANDS { - const val TIME = "time" - const val ONLINE = "online" - } - const val playersOnline = "Online" - const val nobodyOnline = "Nobody online" - const val playerJoined = "joined" - const val playerLeft = "left" - const val useWebhook = false - const val enable = true - const val telegramMessageFormat = "<%username%>: %message%" - } object WARN { const val noConfigWarning = "No config file found! Writing default config to config.yml." const val noToken = "Bot token must be defined." const val noUsername = "Bot username must be defined." } + object INFO { + const val reloading = "Reloading plugin... This may take some time" + const val reloadComplete = "Reload completed." + } object TIMES_OF_DAY { const val day = "\uD83C\uDFDE Day" const val sunset = "\uD83C\uDF06 Sunset" @@ -60,4 +19,7 @@ object Constants { } const val USERNAME_PLACEHOLDER = "%username%" const val MESSAGE_TEXT_PLACEHOLDER = "%message%" + object COMMANDS { + const val PLUGIN_RELOAD = "tgbridge_reload" + } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt index 1c3f90f..45492d1 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt @@ -7,29 +7,15 @@ import org.bukkit.event.player.AsyncPlayerChatEvent import org.bukkit.event.player.PlayerBedEnterEvent import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent -import org.kraftwerk28.spigot_tg_bridge.Constants as C -class EventHandler(private val plugin: Plugin) : Listener { - - private val joinStr: String - private val leftStr: String - private val logJoinLeave: Boolean - private val logDeathMessage: Boolean - private val logPlayerAsleep: Boolean - - init { - plugin.config.run { - joinStr = getString(C.FIELDS.STRINGS.JOINED, C.DEFS.playerJoined)!! - leftStr = getString(C.FIELDS.STRINGS.LEFT, C.DEFS.playerLeft)!! - logJoinLeave = getBoolean(C.FIELDS.LOG_JOIN_LEAVE, C.DEFS.logJoinLeave) - logDeathMessage = getBoolean(C.FIELDS.LOG_PLAYER_DEATH, C.DEFS.logPlayerDeath) - logPlayerAsleep = getBoolean(C.FIELDS.LOG_PLAYER_ASLEEP, C.DEFS.logPlayerAsleep) - } - } +class EventHandler( + private val plugin: Plugin, + private val config: Configuration +) : Listener { @EventHandler fun onPlayerChat(event: AsyncPlayerChatEvent) { - if (plugin.chatToTG) { + if (config.logFromMCtoTG) { plugin.tgBot.sendMessageToTGFrom( event.player.displayName, event.message ) @@ -38,21 +24,23 @@ class EventHandler(private val plugin: Plugin) : Listener { @EventHandler fun onPlayerJoin(event: PlayerJoinEvent) { - if (!logJoinLeave) return - val text = "${TgBot.escapeHTML(event.player.displayName)} $joinStr." + if (!config.logJoinLeave) return + val text = "${TgBot.escapeHTML(event.player.displayName)} " + + "${config.joinString}." plugin.tgBot.broadcastToTG(text) } @EventHandler fun onPlayerLeave(event: PlayerQuitEvent) { - if (!logJoinLeave) return - val text = "${TgBot.escapeHTML(event.player.displayName)} $leftStr." + if (!config.logJoinLeave) return + val text = "${TgBot.escapeHTML(event.player.displayName)} " + + "${config.leaveString}." plugin.tgBot.broadcastToTG(text) } @EventHandler fun onPlayerDied(event: PlayerDeathEvent) { - if (!logDeathMessage) return + if (!config.logDeath) return event.deathMessage?.let { val plName = event.entity.displayName val text = it.replace(plName, "$plName") @@ -62,7 +50,7 @@ class EventHandler(private val plugin: Plugin) : Listener { @EventHandler fun onPlayerAsleep(event: PlayerBedEnterEvent) { - if (!logPlayerAsleep) return + if (!config.logPlayerAsleep) return if (event.bedEnterResult != PlayerBedEnterEvent.BedEnterResult.OK) return val text = "${event.player.displayName} fell asleep." plugin.tgBot.broadcastToTG(text) diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt index f0d5edf..50d53f9 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt @@ -1,57 +1,42 @@ package org.kraftwerk28.spigot_tg_bridge import com.vdurmont.emoji.EmojiParser -import org.bukkit.ChatColor import org.bukkit.plugin.java.JavaPlugin -import java.io.File +import java.lang.Exception import org.kraftwerk28.spigot_tg_bridge.Constants as C class Plugin : JavaPlugin() { lateinit var tgBot: TgBot - val chatToTG: Boolean - var _isEnabled: Boolean = false - val telegramMessageFormat: String - - init { - config.run { - chatToTG = getBoolean( - C.FIELDS.LOG_FROM_MC_TO_TG, - C.DEFS.logFromMCtoTG - ) - _isEnabled = getBoolean(C.FIELDS.ENABLE, C.DEFS.enable) - telegramMessageFormat = getString( - C.FIELDS.TELEGRAM_MESSAGE_FORMAT, - C.DEFS.telegramMessageFormat - )!! - } - } + lateinit var config: Configuration override fun onEnable() { - if (!_isEnabled) return - val configFile = File( - server.pluginManager.getPlugin(name)!!.dataFolder, - C.configFilename - ) - if (!configFile.exists()) { + try { + config = Configuration(this) + } catch (e: Exception) { logger.warning(C.WARN.noConfigWarning) - saveDefaultConfig() return } - tgBot = TgBot(this) - server.pluginManager.registerEvents(EventHandler(this), this) + if (!config.isEnabled) return - // Notify everything about server start - config.getString(C.FIELDS.SERVER_START_MSG, null)?.let { + val cmdHandler = CommandHandler(this) + val eventHandler = EventHandler(this, config) + + tgBot = TgBot(this, config) + getCommand(C.COMMANDS.PLUGIN_RELOAD)?.setExecutor(cmdHandler) + server.pluginManager.registerEvents(eventHandler, this) + + // Notify Telegram groups about server start + config.serverStartMessage?.let { tgBot.broadcastToTG(it) } logger.info("Plugin started.") } override fun onDisable() { - if (!_isEnabled) return - config.getString(C.FIELDS.SERVER_STOP_MSG, null)?.let { + if (!config.isEnabled) return + config.serverStopMessage?.let { tgBot.broadcastToTG(it) } logger.info("Plugin stopped.") @@ -63,11 +48,19 @@ class Plugin : JavaPlugin() { } fun sendMessageToMCFrom(username: String, text: String) { - val prepared = telegramMessageFormat + val prepared = config.telegramMessageFormat .replace(C.USERNAME_PLACEHOLDER, emojiEsc(username)) .replace(C.MESSAGE_TEXT_PLACEHOLDER, emojiEsc(text)) server.broadcastMessage(prepared) } fun emojiEsc(text: String) = EmojiParser.parseToAliases(text) + + fun reload() { + logger.info(C.INFO.reloading) + config.reload(this) + tgBot.stop() + tgBot.start(this, config) + logger.info(C.INFO.reloadComplete) + } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt index 767b33a..9ef41c3 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt @@ -12,35 +12,20 @@ import okhttp3.logging.HttpLoggingInterceptor import java.net.InetAddress import org.kraftwerk28.spigot_tg_bridge.Constants as C -class TgBot(val plugin: Plugin) { +class TgBot(private val plugin: Plugin, private val config: Configuration) { - private val commands = Commands(plugin) - private val bot: Bot - private val allowedChats: List - private val chatToMC: Boolean - private val botToken: String - private val botUsername: String - private val allowWebhook: Boolean - private var webhookConfig: Map? = null + private lateinit var bot: Bot init { - plugin.config.run { - allowedChats = getLongList(C.FIELDS.ALLOWED_CHATS) - chatToMC = getBoolean(C.FIELDS.LOG_FROM_TG_TO_MC, C.DEFS.logFromTGtoMC) - botToken = getString(C.FIELDS.BOT_TOKEN) ?: throw Exception(C.WARN.noToken) - botUsername = getString(C.FIELDS.BOT_USERNAME) ?: throw Exception(C.WARN.noUsername) - allowWebhook = getBoolean(C.FIELDS.USE_WEBHOOK, C.DEFS.useWebhook) + start(plugin, config) + } - val whCfg = get(C.FIELDS.WEBHOOK_CONFIG) - if (whCfg is Map<*, *>) { - @Suppress("UNCHECKED_CAST") - webhookConfig = whCfg as Map? - } - } + fun start(plugin: Plugin, config: Configuration) { val slashRegex = "^/+".toRegex() + val commands = config.commands bot = bot { - token = botToken + token = config.botToken logLevel = HttpLoggingInterceptor.Level.NONE dispatch { command(commands.time.replace(slashRegex, ""), ::time) @@ -48,25 +33,39 @@ class TgBot(val plugin: Plugin) { text(null, ::onText) } } + skipUpdates() plugin.logger.info("Server address: ${InetAddress.getLocalHost().hostAddress}.") - webhookConfig?.let { config -> + config.webhookConfig?.let { _ -> plugin.logger.info("Running in webhook mode.") } ?: run { bot.startPolling() } } + fun stop() { + bot.stopPolling() + } + private fun time(bot: Bot, update: Update) { - val t = plugin.server.worlds[0].time - var text = when { + val msg = update.message!! + if (plugin.server.worlds.isEmpty()) { + bot.sendMessage( + msg.chat.id, + "No worlds available", + replyToMessageId = msg.messageId + ) + return + } + + val t = plugin.server.worlds.first().time + val text = when { t <= 12000 -> C.TIMES_OF_DAY.day t <= 13800 -> C.TIMES_OF_DAY.sunset t <= 22200 -> C.TIMES_OF_DAY.night t <= 24000 -> C.TIMES_OF_DAY.sunrise else -> "" - } - text += " ($t)" - val msg = update.message!! + } + " ($t)" + bot.sendMessage( msg.chat.id, text, replyToMessageId = msg.messageId, @@ -80,17 +79,9 @@ class TgBot(val plugin: Plugin) { .onlinePlayers .mapIndexed { i, s -> "${i + 1}. ${s.displayName}" } .joinToString("\n") - val onlineStr = plugin.config.getString( - C.FIELDS.STRINGS.ONLINE, - C.DEFS.playersOnline - )!! - val offlineStr = plugin.config.getString( - C.FIELDS.STRINGS.OFFLINE, - C.DEFS.nobodyOnline - )!! val text = - if (playerList.isNotEmpty()) "$onlineStr:\n$playerStr" - else offlineStr + if (playerList.isNotEmpty()) "${config.onlineString}:\n$playerStr" + else config.nobodyOnlineString val msg = update.message!! bot.sendMessage( msg.chat.id, text, @@ -100,13 +91,13 @@ class TgBot(val plugin: Plugin) { } fun broadcastToTG(text: String) { - allowedChats.forEach { chatID -> + config.allowedChats.forEach { chatID -> bot.sendMessage(chatID, text, parseMode = ParseMode.HTML) } } fun sendMessageToTGFrom(username: String, text: String) { - allowedChats.forEach { chatID -> + config.allowedChats.forEach { chatID -> bot.sendMessage( chatID, mcMessageStr(username, text), @@ -116,7 +107,7 @@ class TgBot(val plugin: Plugin) { } private fun onText(bot: Bot, update: Update) { - if (!chatToMC) return + if (!config.logFromTGtoMC) return val msg = update.message!! if (msg.text!!.startsWith("/")) return // Suppress command forwarding plugin.sendMessageToMCFrom(rawUserMention(msg.from!!), msg.text!!) @@ -130,6 +121,16 @@ class TgBot(val plugin: Plugin) { ?: user.username ?: user.lastName!! + private fun skipUpdates(lastUpdateID: Long = 0) { + val newUpdates = bot.getUpdates(lastUpdateID) + + if (newUpdates.isNotEmpty()) { + val lastUpd = newUpdates.last() + if (lastUpd !is Update) return + return skipUpdates(lastUpd.updateId + 1) + } + } + companion object { fun escapeHTML(s: String): String = s.replace("&", "&").replace(">", ">").replace("<", "<") diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bac913a..46aba92 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,10 @@ name: SpigotTGBridge -version: 0.0.9 +version: 0.0.10 api-version: '1.15' main: org.kraftwerk28.spigot_tg_bridge.Plugin description: Telegram <-> Minecraft communication plugin for Spigot. load: STARTUP +commands: + tgbridge_reload: + description: 'Reload Spigot TG bridge plugin' + usage: /