From 8c401887add00c5f28a193e30ca77aa0e734f2ca Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Fri, 9 Jul 2021 15:51:57 +0300 Subject: [PATCH 1/6] Add basic IgnAuth functionality --- .../spigot_tg_bridge/AsyncJavaPlugin.kt | 35 ++++ .../org/kraftwerk28/spigot_tg_bridge/Auth.kt | 28 +++ .../spigot_tg_bridge/BotCommands.kt | 4 + .../spigot_tg_bridge/CommandHandler.kt | 2 +- .../spigot_tg_bridge/Configuration.kt | 6 + .../kraftwerk28/spigot_tg_bridge/Entities.kt | 59 +++++++ .../spigot_tg_bridge/EventHandler.kt | 19 +- .../kraftwerk28/spigot_tg_bridge/IgnAuth.kt | 90 ++++++++++ .../kraftwerk28/spigot_tg_bridge/Plugin.kt | 55 +++--- .../spigot_tg_bridge/TgApiService.kt | 40 ----- .../org/kraftwerk28/spigot_tg_bridge/TgBot.kt | 167 ++++++++++-------- .../org/kraftwerk28/spigot_tg_bridge/Utils.kt | 73 +++++++- 12 files changed, 437 insertions(+), 141 deletions(-) create mode 100644 src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt create mode 100644 src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt create mode 100644 src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt create mode 100644 src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt new file mode 100644 index 0000000..7e01bd4 --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt @@ -0,0 +1,35 @@ +package org.kraftwerk28.spigot_tg_bridge + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.bukkit.plugin.java.JavaPlugin + +open class AsyncJavaPlugin : JavaPlugin() { + private val scope = CoroutineScope(Dispatchers.Default) + private val jobs: MutableList = mutableListOf() + + override fun onEnable() { + runBlocking { onEnableAsync() } + } + + override fun onDisable() { + runBlocking { + onDisableAsync() + jobs.joinAll() + } + } + + open suspend fun onEnableAsync() = Unit + + open suspend fun onDisableAsync() = Unit + + fun launch(f: suspend () -> T) = scope.launch { + f() + }.also { + jobs.add(it) + } +} diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt new file mode 100644 index 0000000..e9b9416 --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt @@ -0,0 +1,28 @@ +package org.kraftwerk28.spigot_tg_bridge + +import java.sql.Connection +import java.sql.DriverManager +import java.sql.SQLException + +class Auth() { + var conn: Connection? = null + + suspend fun connect() { + try { + val connString = "jdbc:sqlite:spigot-tg-bridge.sqlite" + val initDbQuery = """ + create table if not exists ign_links ( + telegram_id bigint, + telegram_username varchar, + minecraft_ign varchar, + linked_timestamp int + ) + """ + conn = DriverManager.getConnection(connString).apply { + createStatement().execute(initDbQuery) + } + } catch (e: SQLException) { + e.printStackTrace() + } + } +} 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 638c79e..45baca9 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/BotCommands.kt @@ -6,12 +6,16 @@ class BotCommands(cfg: FileConfiguration) { val time: String? val online: String? val chatID: String? + val linkIgn: String? + val getAllLinked: String? init { cfg.run { time = getString("commands.time") online = getString("commands.online") chatID = getString("commands.chat_id") + linkIgn = getString("commands.link_ign") + getAllLinked = getString("commands.list_linked") } } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt index a3d79ce..d795ffc 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/CommandHandler.kt @@ -16,7 +16,7 @@ class CommandHandler(private val plugin: Plugin) : CommandExecutor { if (sender !is ConsoleCommandSender) return false return when (label) { C.COMMANDS.PLUGIN_RELOAD -> { - plugin.reload() + plugin.launch { plugin.reload() } true } else -> false diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt index ddaad24..d17b2d4 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Configuration.kt @@ -17,6 +17,7 @@ class Configuration(plugin: Plugin) { val logPlayerAsleep: Boolean val onlineString: String val nobodyOnlineString: String + val enableIgnAuth: Boolean // Telegram bot stuff val botToken: String @@ -24,6 +25,7 @@ class Configuration(plugin: Plugin) { val logFromTGtoMC: Boolean val allowWebhook: Boolean val webhookConfig: Map? + val pollTimeout: Int var commands: BotCommands @@ -78,10 +80,14 @@ class Configuration(plugin: Plugin) { )!! // isEnabled = getBoolean("enable", true) allowedChats = getLongList("chats") + enableIgnAuth = getBoolean("enableIgnAuth", false) + botToken = getString("botToken") ?: throw Exception(C.WARN.noToken) allowWebhook = getBoolean("useWebhook", false) @Suppress("unchecked_cast") webhookConfig = get("webhookConfig") as Map? + pollTimeout = getInt("pollTimeout", 30) + logJoinLeave = getBoolean("logJoinLeave", false) onlineString = getString("strings.online", "Online")!! nobodyOnlineString = getString( diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt new file mode 100644 index 0000000..6855400 --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt @@ -0,0 +1,59 @@ +package org.kraftwerk28.spigot_tg_bridge + +import java.sql.Date +import com.google.gson.annotations.SerializedName as Name + +data class TgResponse( + val ok: Boolean, + val result: T?, + val description: String?, +) + +data class WebhookOptions(val drop_pending_updates: Boolean) + +data class User( + @Name("id") val id: Long, + @Name("is_bot") val isBot: Boolean, + @Name("first_name") val firstName: String, + @Name("last_name") val lastName: String? = null, + @Name("username") val username: String? = null, + @Name("language_code") val languageCode: String? = null, +) + +data class Chat( + val id: Long, + val type: String, + val title: String? = null, + val username: String? = null, + @Name("first_name") val firstName: String? = null, + @Name("last_name") val lastName: String? = null, +) + +data class Message( + @Name("message_id") val messageId: Long, + val from: User? = null, + @Name("sender_chat") val senderChat: Chat? = null, + val date: Long, + val chat: Chat, + @Name("reply_to_message") val replyToMessage: Message? = null, + val text: String? = null, +) + +data class Update( + @Name("update_id") val updateId: Long, + val message: Message? = null, +) + +data class BotCommand(val command: String, val description: String) + +data class SetMyCommands(val commands: List) + +data class DbLinkedUser( + val tgId: Long, + val tgFirstName: String, + val tgLastName: String?, + val tgUsername: String?, + val minecraftUuid: String, + val minecraftUsername: String, + val createdTimestamp: Date, +) 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 a76e804..d66bb7d 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/EventHandler.kt @@ -9,15 +9,16 @@ import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent class EventHandler( + private val plugin: Plugin, + private val config: Configuration, private val tgBot: TgBot, - private val config: Configuration ) : Listener { @EventHandler fun onPlayerChat(event: AsyncPlayerChatEvent) { if (!config.logFromMCtoTG) return event.run { - tgBot.sendMessageToTelegram(message, player.displayName) + sendMessage(message, player.displayName) } } @@ -26,7 +27,7 @@ class EventHandler( if (!config.logJoinLeave) return val username = event.player.displayName.fullEscape() val text = config.joinString.replace("%username%", username) - tgBot.sendMessageToTelegram(text) + sendMessage(text) } @EventHandler @@ -34,7 +35,7 @@ class EventHandler( if (!config.logJoinLeave) return val username = event.player.displayName.fullEscape() val text = config.leaveString.replace("%username%", username) - tgBot.sendMessageToTelegram(text) + sendMessage(text) } @EventHandler @@ -43,7 +44,7 @@ class EventHandler( event.deathMessage?.let { val username = event.entity.displayName.fullEscape() val text = it.replace(username, "$username") - tgBot.sendMessageToTelegram(text) + sendMessage(text) } } @@ -53,6 +54,12 @@ class EventHandler( if (event.bedEnterResult != PlayerBedEnterEvent.BedEnterResult.OK) return val text = "${event.player.displayName} fell asleep." - tgBot.sendMessageToTelegram(text) + sendMessage(text) + } + + private fun sendMessage(text: String, username: String? = null) { + plugin.launch { + tgBot.sendMessageToTelegram(text, username) + } } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt new file mode 100644 index 0000000..76eca98 --- /dev/null +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt @@ -0,0 +1,90 @@ +package org.kraftwerk28.spigot_tg_bridge + +import java.sql.Connection +import java.sql.DriverManager +import java.sql.SQLException + +const val INIT_DB_QUERY = """ +create table if not exists user ( + tg_id bigint not null primary key, + tg_username varchar, + tg_first_name varchar not null, + tg_last_name varchar, + mc_username varchar, + mc_uuid varchar, + linked_timestamp datetime default current_timestamp +); +""" + +class IgnAuth( + fileName: String, + val plugin: Plugin, + var conn: Connection? = null, +) { + init { + plugin.launch { + initializeConnection(fileName) + } + } + + suspend fun initializeConnection(fileName: String) = try { + DriverManager.getConnection("jdbc:sqlite:$fileName").apply { + createStatement().execute(INIT_DB_QUERY) + }.also { + conn = it + } + } catch (e: SQLException) { + plugin.logger.info(e.message) + } + + suspend fun close() = conn?.run { + close() + } + + suspend fun linkUser( + tgId: Long, + tgUsername: String? = null, + tgFirstName: String, + tgLastName: String? = null, + minecraftUsername: String?, + minecraftUuid: String, + ): Boolean = conn?.stmt( + """ + insert into user ( + tg_id, + tg_username, + tg_first_name, + tg_last_name, + mc_uuid, + mc_username, + ) + values (?, ?, ?, ?, ?, ?) + """, + tgId, + tgUsername, + tgFirstName, + tgLastName, + minecraftUuid, + minecraftUsername, + )?.run { + execute() + } ?: false + + suspend fun getLinkedUserByIgn(ign: String) = + conn?.stmt("select * from user where mc_uuid = ?", ign)?.first { + toLinkedUser() + } + + suspend fun getLinkedUserByTgId(id: Long) = + conn?.stmt("select * from user where tg_id = ?", id)?.first { + toLinkedUser() + } + + suspend fun unlinkUserByTgId(id: Long) = + conn?.stmt("delete from user where tg_id = ?", id)?.run { + executeUpdate() > 0 + } + + suspend fun getAllLinkedUsers() = + conn?.stmt("select * from user")?.map { toLinkedUser() } +} 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 d9aef46..67fba90 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt @@ -1,44 +1,53 @@ package org.kraftwerk28.spigot_tg_bridge import org.bukkit.event.HandlerList -import org.bukkit.plugin.java.JavaPlugin import java.lang.Exception import org.kraftwerk28.spigot_tg_bridge.Constants as C -class Plugin : JavaPlugin() { +class Plugin : AsyncJavaPlugin() { var tgBot: TgBot? = null - var eventHandler: EventHandler? = null + private var eventHandler: EventHandler? = null var config: Configuration? = null + var ignAuth: IgnAuth? = null - override fun onEnable() { - try { - config = Configuration(this) - } catch (e: Exception) { - logger.warning(e.message) - return - } - - config?.let { config -> + override suspend fun onEnableAsync() = try { + config = Configuration(this).also { config -> if (!config.isEnabled) return - val cmdHandler = CommandHandler(this) + + if (config.enableIgnAuth) { + val dbFilePath = dataFolder.resolve("spigot-tg-bridge.sqlite") + ignAuth = IgnAuth( + fileName = dbFilePath.absolutePath, + plugin = this, + ) + } + tgBot?.run { stop() } tgBot = TgBot(this, config).also { bot -> - eventHandler = EventHandler(bot, config).also { + bot.startPolling() + eventHandler = EventHandler(this, config, bot).also { server.pluginManager.registerEvents(it, this) } } - getCommand(C.COMMANDS.PLUGIN_RELOAD)?.setExecutor(cmdHandler) - config.serverStartMessage?.let { message -> - tgBot?.sendMessageToTelegram(message) + + getCommand(C.COMMANDS.PLUGIN_RELOAD)?.run { + setExecutor(CommandHandler(this@Plugin)) + } + config.serverStartMessage?.let { + tgBot?.sendMessageToTelegram(it) } } + } catch (e: Exception) { + // Configuration file is missing or incomplete + logger.warning(e.message) } - override fun onDisable() { - config?.let { config -> - if (!config.isEnabled) return + override suspend fun onDisableAsync() { + config?.let fn@{ config -> + if (!config.isEnabled) + return@fn config.serverStopMessage?.let { - tgBot?.sendMessageToTelegram(it, blocking = true) + tgBot?.sendMessageToTelegram(it) } eventHandler?.let { HandlerList.unregisterAll(it) } tgBot?.run { stop() } @@ -66,14 +75,14 @@ class Plugin : JavaPlugin() { .also { server.broadcastMessage(it) } } - fun reload() { + suspend fun reload() { config = Configuration(this).also { config -> if (!config.isEnabled) return logger.info(C.INFO.reloading) eventHandler?.let { HandlerList.unregisterAll(it) } tgBot?.run { stop() } tgBot = TgBot(this, config).also { bot -> - eventHandler = EventHandler(bot, config).also { + eventHandler = EventHandler(this, config, bot).also { server.pluginManager.registerEvents(it, this) } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgApiService.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgApiService.kt index 7a3aaf1..8f1ccf2 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgApiService.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgApiService.kt @@ -4,48 +4,8 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Query -import com.google.gson.annotations.SerializedName as Name interface TgApiService { - data class TgResponse(val ok: Boolean, val result: T?, val description: String?) - data class WebhookOptions(val drop_pending_updates: Boolean) - - data class User( - @Name("id") val id: Long, - @Name("is_bot") val isBot: Boolean, - @Name("first_name") val firstName: String, - @Name("last_name") val lastName: String? = null, - @Name("username") val username: String? = null, - @Name("language_code") val languageCode: String? = null, - ) - - data class Chat( - val id: Long, - val type: String, - val title: String? = null, - val username: String? = null, - @Name("first_name") val firstName: String? = null, - @Name("last_name") val lastName: String? = null, - ) - - data class Message( - @Name("message_id") val messageId: Long, - val from: User? = null, - @Name("sender_chat") val senderChat: Chat? = null, - val date: Long, - val chat: Chat, - @Name("reply_to_message") val replyToMessage: Message? = null, - val text: String? = null, - ) - - data class Update( - @Name("update_id") val updateId: Long, - val message: Message? = null, - ) - - data class BotCommand(val command: String, val description: String) - data class SetMyCommands(val commands: List) - @GET("deleteWebhook") suspend fun deleteWebhook( @Query("drop_pending_updates") dropPendingUpdates: Boolean 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 deadae1..b36e237 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt @@ -1,14 +1,11 @@ package org.kraftwerk28.spigot_tg_bridge import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient import retrofit2.Call import retrofit2.Retrofit @@ -16,75 +13,84 @@ import retrofit2.converter.gson.GsonConverterFactory import java.time.Duration import org.kraftwerk28.spigot_tg_bridge.Constants as C -typealias UpdateRequest = Call>>? +typealias UpdateRequest = Call>>? class TgBot( private val plugin: Plugin, private val config: Configuration, - private val pollTimeout: Int = 30, ) { private val api: TgApiService private val client: OkHttpClient - private val updateChan = Channel() - private val scope = CoroutineScope(Dispatchers.Default) - private val pollJob: Job - private val handlerJob: Job + private val updateChan = Channel() + private var pollJob: Job? = null + private var handlerJob: Job? = null private var currentOffset: Long = -1 - private var me: TgApiService.User - private var commandRegex: Regex - private val commandMap = config.commands.run { - mapOf( - online to ::onlineHandler, - time to ::timeHandler, - chatID to ::chatIdHandler, - ) - } + private var me: User? = null + private var commandRegex: Regex? = null + private val commandMap: Map Unit> = + config.commands.run { + mapOf( + online to ::onlineHandler, + time to ::timeHandler, + chatID to ::chatIdHandler, + linkIgn to ::linkIgnHandler, + getAllLinked to ::getLinkedUsersHandler, + ) + } init { client = OkHttpClient .Builder() .readTimeout(Duration.ZERO) .build() - api = Retrofit.Builder() .baseUrl("https://api.telegram.org/bot${config.botToken}/") .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() .create(TgApiService::class.java) + } - runBlocking { - me = api.getMe().result!! - // I intentionally don't put optional @username in regex - // since bot is only used in group chats - commandRegex = """^\/(\w+)(?:@${me.username})$""".toRegex() - val commands = config.commands.run { listOf(time, online, chatID) } - .zip( - C.COMMAND_DESC.run { - listOf(timeDesc, onlineDesc, chatIDDesc) - } - ) - .map { TgApiService.BotCommand(it.first!!, it.second) } - .let { TgApiService.SetMyCommands(it) } - - api.deleteWebhook(true) - api.setMyCommands(commands) - } + private suspend fun initialize() { + me = api.getMe().result!! + // I intentionally don't put optional @username in regex + // since bot is only used in group chats + commandRegex = """^\/(\w+)(?:@${me!!.username})$""".toRegex() + val commands = config.commands.run { listOf(time, online, chatID) } + .zip( + C.COMMAND_DESC.run { + listOf(timeDesc, onlineDesc, chatIDDesc) + } + ) + .map { BotCommand(it.first!!, it.second) } + .let { SetMyCommands(it) } + api.deleteWebhook(dropPendingUpdates = true) + api.setMyCommands(commands) + } + suspend fun startPolling() { + initialize() pollJob = initPolling() handlerJob = initHandler() } - private fun initPolling() = scope.launch { + suspend fun stop() { + pollJob?.cancelAndJoin() + handlerJob?.join() + } + + private fun initPolling() = plugin.launch { loop@ while (true) { try { - api.getUpdates(offset = currentOffset, timeout = pollTimeout) - .result?.let { updates -> - if (!updates.isEmpty()) { - updates.forEach { updateChan.send(it) } - currentOffset = updates.last().updateId + 1 - } + api.getUpdates( + offset = currentOffset, + timeout = config.pollTimeout, + ).result?.let { updates -> + if (!updates.isEmpty()) { + updates.forEach { updateChan.send(it) } + currentOffset = updates.last().updateId + 1 } + } } catch (e: Exception) { when (e) { is CancellationException -> break@loop @@ -98,7 +104,7 @@ class TgBot( updateChan.close() } - private fun initHandler() = scope.launch { + private fun initHandler() = plugin.launch { updateChan.consumeEach { try { handleUpdate(it) @@ -108,28 +114,20 @@ class TgBot( } } - suspend fun handleUpdate(update: TgApiService.Update) { + suspend fun handleUpdate(update: Update) { // Ignore PM or channel if (listOf("private", "channel").contains(update.message?.chat?.type)) return update.message?.text?.let { - commandRegex.matchEntire(it)?.groupValues?.let { - val (command) = it - commandMap.get(command)?.let { it(update) } + commandRegex?.matchEntire(it)?.groupValues?.let { + commandMap.get(it[1])?.let { it(update) } } ?: run { onTextHandler(update) } } } - fun stop() { - runBlocking { - pollJob.cancelAndJoin() - handlerJob.join() - } - } - - private suspend fun timeHandler(update: TgApiService.Update) { + private suspend fun timeHandler(update: Update) { val msg = update.message!! if (!config.allowedChats.contains(msg.chat.id)) { return @@ -156,7 +154,7 @@ class TgBot( api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) } - private suspend fun onlineHandler(update: TgApiService.Update) { + private suspend fun onlineHandler(update: Update) { val msg = update.message!! if (!config.allowedChats.contains(msg.chat.id)) { return @@ -172,7 +170,7 @@ class TgBot( api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) } - private suspend fun chatIdHandler(update: TgApiService.Update) { + private suspend fun chatIdHandler(update: Update) { val msg = update.message!! val chatId = msg.chat.id val text = """ @@ -186,7 +184,24 @@ class TgBot( api.sendMessage(chatId, text, replyToMessageId = msg.messageId) } - private suspend fun onTextHandler(update: TgApiService.Update) { + private suspend fun linkIgnHandler(update: Update) { + val tgUser = update.message!!.from!! + val mcUuid = getMinecraftUuidByUsername(update.message.text!!) + if (mcUuid == null) { + // Respond... + return + } + val linked = plugin.ignAuth?.linkUser( + tgId = tgUser.id, + tgFirstName = tgUser.firstName, + tgLastName = tgUser.lastName, + minecraftUsername = tgUser.username, + minecraftUuid = mcUuid, + ) + println(tgUser.toString()) + } + + private suspend fun onTextHandler(update: Update) { val msg = update.message!! if (!config.logFromTGtoMC || msg.from == null) return @@ -197,22 +212,34 @@ class TgBot( ) } - fun sendMessageToTelegram( - text: String, - username: String? = null, - blocking: Boolean = false, - ) { + private suspend fun getLinkedUsersHandler(update: Update) { + val linkedUsers = plugin.ignAuth?.run { + getAllLinkedUsers() + } ?: listOf() + if (linkedUsers.isEmpty()) { + api.sendMessage(update.message!!.chat.id, "No linked users.") + } else { + val text = "Linked users:\n" + + linkedUsers.mapIndexed { i, dbUser -> + "${i + 1}. ${dbUser.fullName()}" + }.joinToString("\n") + api.sendMessage(update.message!!.chat.id, text) + } + } + + suspend fun sendMessageToTelegram(text: String, username: String? = null) { val formatted = username?.let { config.telegramFormat .replace(C.USERNAME_PLACEHOLDER, username.fullEscape()) .replace(C.MESSAGE_TEXT_PLACEHOLDER, text.escapeHtml()) } ?: text - scope.launch { - config.allowedChats.forEach { chatId -> - api.sendMessage(chatId, formatted) - } - }.also { - if (blocking) runBlocking { it.join() } + config.allowedChats.forEach { chatId -> + api.sendMessage(chatId, formatted) } + // plugin.launch { + // config.allowedChats.forEach { chatId -> + // api.sendMessage(chatId, formatted) + // } + // } } } diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Utils.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Utils.kt index ba02df8..61a2bad 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Utils.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Utils.kt @@ -1,6 +1,12 @@ package org.kraftwerk28.spigot_tg_bridge import com.vdurmont.emoji.EmojiParser +import java.io.BufferedReader +import java.net.HttpURLConnection +import java.net.URL +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet fun String.escapeHtml() = this .replace("&", "&") @@ -18,7 +24,72 @@ fun String.fullEscape() = escapeHTML().escapeColorCodes() fun String.escapeEmoji() = EmojiParser.parseToAliases(this) -fun TgApiService.User.rawUserMention(): String = +fun User.rawUserMention(): String = (if (firstName.length < 2) null else firstName) ?: username ?: lastName!! + +fun DbLinkedUser.fullName() = tgFirstName + (tgLastName?.let { " $it" } ?: "") + +fun Connection.stmt(query: String, vararg args: Any?) = + prepareStatement(query).apply { + args.zip(1..args.size).forEach { (arg, i) -> + when (arg) { + is String -> setString(i, arg) + is Long -> setLong(i, arg) + is Int -> setInt(i, arg) + } + } + } + +suspend fun checkMinecraftLicense(playerUuid: String): Boolean = try { + val urlString = "https://api.mojang.com/user/profiles/$playerUuid/names" + val conn = (URL(urlString).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + } + conn.responseCode == 200 +} catch (e: Exception) { + e.printStackTrace() + false +} + +suspend fun getMinecraftUuidByUsername(username: String): String? = try { + val urlString = "https://api.mojang.com/users/profiles/minecraft/$username" + val conn = (URL(urlString).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + } + val regex = "\"name\"\\s*:\\s*\"(\\w+)\"".toRegex() + val body = BufferedReader(conn.inputStream.reader()).readText() + regex.matchEntire(body)?.groupValues?.get(1) +} catch (e: Exception) { + e.printStackTrace() + null +} + +fun ResultSet.toLinkedUser() = DbLinkedUser( + tgId = getLong("tg_id"), + tgUsername = getString("tg_username"), + tgFirstName = getString("tg_first_name"), + tgLastName = getString("tg_last_name"), + createdTimestamp = getDate("linked_timestamp"), + minecraftUsername = getString("mc_username"), + minecraftUuid = getString("mc_uuid"), +) + +fun PreparedStatement.first(convertFn: ResultSet.() -> T): T? { + val result = executeQuery() + return if (result.next()) { + result.convertFn() + } else { + null + } +} + +fun PreparedStatement.map(convertFn: ResultSet.() -> T): List { + val resultSet = executeQuery() + val result = mutableListOf() + while (resultSet.next()) { + result.add(resultSet.convertFn()) + } + return result +} From 5b4374f0c9abbdb03d310260e1efa839152c21ec Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Sat, 10 Jul 2021 02:00:22 +0300 Subject: [PATCH 2/6] fix: stability issues --- .../spigot_tg_bridge/AsyncJavaPlugin.kt | 12 +- .../kraftwerk28/spigot_tg_bridge/Plugin.kt | 1 + .../org/kraftwerk28/spigot_tg_bridge/TgBot.kt | 108 +++++++++++------- 3 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt index 7e01bd4..71950cf 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt @@ -2,15 +2,13 @@ package org.kraftwerk28.spigot_tg_bridge import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.joinAll +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.bukkit.plugin.java.JavaPlugin open class AsyncJavaPlugin : JavaPlugin() { private val scope = CoroutineScope(Dispatchers.Default) - private val jobs: MutableList = mutableListOf() override fun onEnable() { runBlocking { onEnableAsync() } @@ -19,7 +17,7 @@ open class AsyncJavaPlugin : JavaPlugin() { override fun onDisable() { runBlocking { onDisableAsync() - jobs.joinAll() + scope.cancel() } } @@ -27,9 +25,5 @@ open class AsyncJavaPlugin : JavaPlugin() { open suspend fun onDisableAsync() = Unit - fun launch(f: suspend () -> T) = scope.launch { - f() - }.also { - jobs.add(it) - } + fun launch(f: suspend () -> T) = scope.launch { f() } } 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 67fba90..4880eba 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt @@ -82,6 +82,7 @@ class Plugin : AsyncJavaPlugin() { eventHandler?.let { HandlerList.unregisterAll(it) } tgBot?.run { stop() } tgBot = TgBot(this, config).also { bot -> + bot.startPolling() eventHandler = EventHandler(this, config, bot).also { server.pluginManager.registerEvents(it, this) } 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 b36e237..725c532 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt @@ -14,6 +14,14 @@ import java.time.Duration import org.kraftwerk28.spigot_tg_bridge.Constants as C typealias UpdateRequest = Call>>? +typealias CmdHandler = suspend (HandlerContext) -> Unit + +data class HandlerContext( + val update: Update, + val message: Message?, + val chat: Chat?, + val commandArgs: List, +) class TgBot( private val plugin: Plugin, @@ -27,16 +35,16 @@ class TgBot( private var currentOffset: Long = -1 private var me: User? = null private var commandRegex: Regex? = null - private val commandMap: Map Unit> = - config.commands.run { - mapOf( - online to ::onlineHandler, - time to ::timeHandler, - chatID to ::chatIdHandler, - linkIgn to ::linkIgnHandler, - getAllLinked to ::getLinkedUsersHandler, - ) - } + private val commandMap: Map = config.commands.run { + mapOf( + online to ::onlineHandler, + time to ::timeHandler, + chatID to ::chatIdHandler, + // TODO: + // linkIgn to ::linkIgnHandler, + // getAllLinked to ::getLinkedUsersHandler, + ) + } init { client = OkHttpClient @@ -55,7 +63,7 @@ class TgBot( me = api.getMe().result!! // I intentionally don't put optional @username in regex // since bot is only used in group chats - commandRegex = """^\/(\w+)(?:@${me!!.username})$""".toRegex() + commandRegex = """^\/(\w+)(?:@${me!!.username})(?:\s+(.+))?$""".toRegex() val commands = config.commands.run { listOf(time, online, chatID) } .zip( C.COMMAND_DESC.run { @@ -118,17 +126,26 @@ class TgBot( // Ignore PM or channel if (listOf("private", "channel").contains(update.message?.chat?.type)) return + var ctx = HandlerContext( + update, + update.message, + update.message?.chat, + listOf(), + ) update.message?.text?.let { commandRegex?.matchEntire(it)?.groupValues?.let { - commandMap.get(it[1])?.let { it(update) } + commandMap.get(it[1])?.run { + val args = it[2].split("\\s+".toRegex()) + this(ctx.copy(commandArgs = args)) + } } ?: run { - onTextHandler(update) + onTextHandler(ctx) } } } - private suspend fun timeHandler(update: Update) { - val msg = update.message!! + private suspend fun timeHandler(ctx: HandlerContext) { + val msg = ctx.message!! if (!config.allowedChats.contains(msg.chat.id)) { return } @@ -154,8 +171,8 @@ class TgBot( api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) } - private suspend fun onlineHandler(update: Update) { - val msg = update.message!! + private suspend fun onlineHandler(ctx: HandlerContext) { + val msg = ctx.message!! if (!config.allowedChats.contains(msg.chat.id)) { return } @@ -170,8 +187,8 @@ class TgBot( api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) } - private suspend fun chatIdHandler(update: Update) { - val msg = update.message!! + private suspend fun chatIdHandler(ctx: HandlerContext) { + val msg = ctx.message!! val chatId = msg.chat.id val text = """ |Chat ID: $chatId. @@ -184,25 +201,45 @@ class TgBot( api.sendMessage(chatId, text, replyToMessageId = msg.messageId) } - private suspend fun linkIgnHandler(update: Update) { - val tgUser = update.message!!.from!! - val mcUuid = getMinecraftUuidByUsername(update.message.text!!) - if (mcUuid == null) { + private suspend fun linkIgnHandler(ctx: HandlerContext) { + val tgUser = ctx.message!!.from!! + val mcUuid = getMinecraftUuidByUsername(ctx.message.text!!) + if (mcUuid == null || ctx.commandArgs.size < 1) { // Respond... return } + val (minecraftIgn) = ctx.commandArgs val linked = plugin.ignAuth?.linkUser( tgId = tgUser.id, tgFirstName = tgUser.firstName, tgLastName = tgUser.lastName, - minecraftUsername = tgUser.username, + minecraftUsername = minecraftIgn, minecraftUuid = mcUuid, - ) - println(tgUser.toString()) + ) ?: false + if (linked) { + // TODO + } } - private suspend fun onTextHandler(update: Update) { - val msg = update.message!! + private suspend fun getLinkedUsersHandler(ctx: HandlerContext) { + val linkedUsers = plugin.ignAuth?.run { + getAllLinkedUsers() + } ?: listOf() + if (linkedUsers.isEmpty()) { + api.sendMessage(ctx.message!!.chat.id, "No linked users.") + } else { + val text = "Linked users:\n" + + linkedUsers.mapIndexed { i, dbUser -> + "${i + 1}. ${dbUser.fullName()}" + }.joinToString("\n") + api.sendMessage(ctx.message!!.chat.id, text) + } + } + + private suspend fun onTextHandler( + @Suppress("unused_parameter") ctx: HandlerContext + ) { + val msg = ctx.message!! if (!config.logFromTGtoMC || msg.from == null) return plugin.sendMessageToMinecraft( @@ -212,21 +249,6 @@ class TgBot( ) } - private suspend fun getLinkedUsersHandler(update: Update) { - val linkedUsers = plugin.ignAuth?.run { - getAllLinkedUsers() - } ?: listOf() - if (linkedUsers.isEmpty()) { - api.sendMessage(update.message!!.chat.id, "No linked users.") - } else { - val text = "Linked users:\n" + - linkedUsers.mapIndexed { i, dbUser -> - "${i + 1}. ${dbUser.fullName()}" - }.joinToString("\n") - api.sendMessage(update.message!!.chat.id, text) - } - } - suspend fun sendMessageToTelegram(text: String, username: String? = null) { val formatted = username?.let { config.telegramFormat From b0bdac4f84d9b1610df5729b281d3bac61fd784e Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Sat, 10 Jul 2021 02:12:04 +0300 Subject: [PATCH 3/6] Update version --- .../org/kraftwerk28/spigot_tg_bridge/Constants.kt | 2 +- src/main/resources/config.yml | 10 +++++----- src/main/resources/plugin.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 563c9b2..4050127 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Constants.kt @@ -8,7 +8,7 @@ object Constants { const val noUsername = "Bot username must be defined." } object INFO { - const val reloading = "Reloading plugin... This may take some time." + const val reloading = "Reloading..." const val reloadComplete = "Reload completed." } object TIMES_OF_DAY { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index dc98202..286bee5 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,9 +7,9 @@ serverStopMessage: "Server stopped." logJoinLeave: true logFromMCtoTG: true logFromTGtoMC: true -logPlayerDeath: false +logPlayerDeath: true logPlayerAsleep: false -minecraftFormat: "<%username%>: %message%" +minecraftFormat: "§6§l%username%§r (from §o%chat%§r): §b%message%§r" telegramFormat: "%username%: %message%" strings: online: "Online" @@ -17,6 +17,6 @@ strings: joined: "%username% joined." left: "%username% left." commands: - time: 'time' - online: 'online' - chat_id: 'chat_id' + time: "time" + online: "online" + chat_id: "chat_id" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b16df02..8396a56 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: SpigotTGBridge -version: "0.18" +version: "0.19" api-version: "1.15" main: org.kraftwerk28.spigot_tg_bridge.Plugin description: Telegram <-> Minecraft communication plugin for Spigot. From 8279fc9022979ff0406d3a4e835fc3d8a8f96b90 Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Sat, 10 Jul 2021 02:14:12 +0300 Subject: [PATCH 4/6] Remove unused file --- .../org/kraftwerk28/spigot_tg_bridge/Auth.kt | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt deleted file mode 100644 index e9b9416..0000000 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.kraftwerk28.spigot_tg_bridge - -import java.sql.Connection -import java.sql.DriverManager -import java.sql.SQLException - -class Auth() { - var conn: Connection? = null - - suspend fun connect() { - try { - val connString = "jdbc:sqlite:spigot-tg-bridge.sqlite" - val initDbQuery = """ - create table if not exists ign_links ( - telegram_id bigint, - telegram_username varchar, - minecraft_ign varchar, - linked_timestamp int - ) - """ - conn = DriverManager.getConnection(connString).apply { - createStatement().execute(initDbQuery) - } - } catch (e: SQLException) { - e.printStackTrace() - } - } -} From df7accf52d5e2ec92531e6976616bdc6a61485dc Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Sat, 10 Jul 2021 13:51:58 +0300 Subject: [PATCH 5/6] Cancel and join parent job --- .../org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt index 71950cf..1d3e24e 100644 --- a/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt +++ b/src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import org.bukkit.plugin.java.JavaPlugin open class AsyncJavaPlugin : JavaPlugin() { @@ -17,7 +19,7 @@ open class AsyncJavaPlugin : JavaPlugin() { override fun onDisable() { runBlocking { onDisableAsync() - scope.cancel() + scope.coroutineContext[Job]?.cancelAndJoin() } } From 6660b255699999fe0156a6d29727cf6c76ccdccd Mon Sep 17 00:00:00 2001 From: kraftwerk28 Date: Sat, 10 Jul 2021 13:52:12 +0300 Subject: [PATCH 6/6] Late plugin startup --- src/main/resources/plugin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 8396a56..abd297d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,7 +3,6 @@ version: "0.19" 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"