Merge pull request #21 from kraftwerk28/ign-auth

Basic IGNAuth implementation (still not ready) + stability fixes
This commit is contained in:
Vsevolod 2021-07-10 13:55:25 +03:00 committed by GitHub
commit a8aa799b96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 434 additions and 148 deletions

View File

@ -0,0 +1,31 @@
package org.kraftwerk28.spigot_tg_bridge
import kotlinx.coroutines.CoroutineScope
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() {
private val scope = CoroutineScope(Dispatchers.Default)
override fun onEnable() {
runBlocking { onEnableAsync() }
}
override fun onDisable() {
runBlocking {
onDisableAsync()
scope.coroutineContext[Job]?.cancelAndJoin()
}
}
open suspend fun onEnableAsync() = Unit
open suspend fun onDisableAsync() = Unit
fun <T> launch(f: suspend () -> T) = scope.launch { f() }
}

View File

@ -6,12 +6,16 @@ class BotCommands(cfg: FileConfiguration) {
val time: String? val time: String?
val online: String? val online: String?
val chatID: String? val chatID: String?
val linkIgn: String?
val getAllLinked: String?
init { init {
cfg.run { cfg.run {
time = getString("commands.time") time = getString("commands.time")
online = getString("commands.online") online = getString("commands.online")
chatID = getString("commands.chat_id") chatID = getString("commands.chat_id")
linkIgn = getString("commands.link_ign")
getAllLinked = getString("commands.list_linked")
} }
} }
} }

View File

@ -16,7 +16,7 @@ class CommandHandler(private val plugin: Plugin) : CommandExecutor {
if (sender !is ConsoleCommandSender) return false if (sender !is ConsoleCommandSender) return false
return when (label) { return when (label) {
C.COMMANDS.PLUGIN_RELOAD -> { C.COMMANDS.PLUGIN_RELOAD -> {
plugin.reload() plugin.launch { plugin.reload() }
true true
} }
else -> false else -> false

View File

@ -17,6 +17,7 @@ class Configuration(plugin: Plugin) {
val logPlayerAsleep: Boolean val logPlayerAsleep: Boolean
val onlineString: String val onlineString: String
val nobodyOnlineString: String val nobodyOnlineString: String
val enableIgnAuth: Boolean
// Telegram bot stuff // Telegram bot stuff
val botToken: String val botToken: String
@ -24,6 +25,7 @@ class Configuration(plugin: Plugin) {
val logFromTGtoMC: Boolean val logFromTGtoMC: Boolean
val allowWebhook: Boolean val allowWebhook: Boolean
val webhookConfig: Map<String, Any>? val webhookConfig: Map<String, Any>?
val pollTimeout: Int
var commands: BotCommands var commands: BotCommands
@ -78,10 +80,14 @@ class Configuration(plugin: Plugin) {
)!! )!!
// isEnabled = getBoolean("enable", true) // isEnabled = getBoolean("enable", true)
allowedChats = getLongList("chats") allowedChats = getLongList("chats")
enableIgnAuth = getBoolean("enableIgnAuth", false)
botToken = getString("botToken") ?: throw Exception(C.WARN.noToken) botToken = getString("botToken") ?: throw Exception(C.WARN.noToken)
allowWebhook = getBoolean("useWebhook", false) allowWebhook = getBoolean("useWebhook", false)
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
webhookConfig = get("webhookConfig") as Map<String, Any>? webhookConfig = get("webhookConfig") as Map<String, Any>?
pollTimeout = getInt("pollTimeout", 30)
logJoinLeave = getBoolean("logJoinLeave", false) logJoinLeave = getBoolean("logJoinLeave", false)
onlineString = getString("strings.online", "Online")!! onlineString = getString("strings.online", "Online")!!
nobodyOnlineString = getString( nobodyOnlineString = getString(

View File

@ -8,7 +8,7 @@ object Constants {
const val noUsername = "Bot username must be defined." const val noUsername = "Bot username must be defined."
} }
object INFO { object INFO {
const val reloading = "Reloading plugin... This may take some time." const val reloading = "Reloading..."
const val reloadComplete = "Reload completed." const val reloadComplete = "Reload completed."
} }
object TIMES_OF_DAY { object TIMES_OF_DAY {

View File

@ -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<T>(
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<BotCommand>)
data class DbLinkedUser(
val tgId: Long,
val tgFirstName: String,
val tgLastName: String?,
val tgUsername: String?,
val minecraftUuid: String,
val minecraftUsername: String,
val createdTimestamp: Date,
)

View File

@ -9,15 +9,16 @@ import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerQuitEvent
class EventHandler( class EventHandler(
private val plugin: Plugin,
private val config: Configuration,
private val tgBot: TgBot, private val tgBot: TgBot,
private val config: Configuration
) : Listener { ) : Listener {
@EventHandler @EventHandler
fun onPlayerChat(event: AsyncPlayerChatEvent) { fun onPlayerChat(event: AsyncPlayerChatEvent) {
if (!config.logFromMCtoTG) return if (!config.logFromMCtoTG) return
event.run { event.run {
tgBot.sendMessageToTelegram(message, player.displayName) sendMessage(message, player.displayName)
} }
} }
@ -26,7 +27,7 @@ class EventHandler(
if (!config.logJoinLeave) return if (!config.logJoinLeave) return
val username = event.player.displayName.fullEscape() val username = event.player.displayName.fullEscape()
val text = config.joinString.replace("%username%", username) val text = config.joinString.replace("%username%", username)
tgBot.sendMessageToTelegram(text) sendMessage(text)
} }
@EventHandler @EventHandler
@ -34,7 +35,7 @@ class EventHandler(
if (!config.logJoinLeave) return if (!config.logJoinLeave) return
val username = event.player.displayName.fullEscape() val username = event.player.displayName.fullEscape()
val text = config.leaveString.replace("%username%", username) val text = config.leaveString.replace("%username%", username)
tgBot.sendMessageToTelegram(text) sendMessage(text)
} }
@EventHandler @EventHandler
@ -43,7 +44,7 @@ class EventHandler(
event.deathMessage?.let { event.deathMessage?.let {
val username = event.entity.displayName.fullEscape() val username = event.entity.displayName.fullEscape()
val text = it.replace(username, "<i>$username</i>") val text = it.replace(username, "<i>$username</i>")
tgBot.sendMessageToTelegram(text) sendMessage(text)
} }
} }
@ -53,6 +54,12 @@ class EventHandler(
if (event.bedEnterResult != PlayerBedEnterEvent.BedEnterResult.OK) if (event.bedEnterResult != PlayerBedEnterEvent.BedEnterResult.OK)
return return
val text = "<i>${event.player.displayName}</i> fell asleep." val text = "<i>${event.player.displayName}</i> fell asleep."
tgBot.sendMessageToTelegram(text) sendMessage(text)
}
private fun sendMessage(text: String, username: String? = null) {
plugin.launch {
tgBot.sendMessageToTelegram(text, username)
}
} }
} }

View File

@ -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() }
}

View File

@ -1,44 +1,53 @@
package org.kraftwerk28.spigot_tg_bridge package org.kraftwerk28.spigot_tg_bridge
import org.bukkit.event.HandlerList import org.bukkit.event.HandlerList
import org.bukkit.plugin.java.JavaPlugin
import java.lang.Exception import java.lang.Exception
import org.kraftwerk28.spigot_tg_bridge.Constants as C import org.kraftwerk28.spigot_tg_bridge.Constants as C
class Plugin : JavaPlugin() { class Plugin : AsyncJavaPlugin() {
var tgBot: TgBot? = null var tgBot: TgBot? = null
var eventHandler: EventHandler? = null private var eventHandler: EventHandler? = null
var config: Configuration? = null var config: Configuration? = null
var ignAuth: IgnAuth? = null
override fun onEnable() { override suspend fun onEnableAsync() = try {
try { config = Configuration(this).also { config ->
config = Configuration(this) if (!config.isEnabled) return
} catch (e: Exception) {
logger.warning(e.message) if (config.enableIgnAuth) {
return val dbFilePath = dataFolder.resolve("spigot-tg-bridge.sqlite")
ignAuth = IgnAuth(
fileName = dbFilePath.absolutePath,
plugin = this,
)
} }
config?.let { config ->
if (!config.isEnabled) return
val cmdHandler = CommandHandler(this)
tgBot?.run { stop() } tgBot?.run { stop() }
tgBot = TgBot(this, config).also { bot -> tgBot = TgBot(this, config).also { bot ->
eventHandler = EventHandler(bot, config).also { bot.startPolling()
eventHandler = EventHandler(this, config, bot).also {
server.pluginManager.registerEvents(it, this) server.pluginManager.registerEvents(it, this)
} }
} }
getCommand(C.COMMANDS.PLUGIN_RELOAD)?.setExecutor(cmdHandler)
config.serverStartMessage?.let { message -> getCommand(C.COMMANDS.PLUGIN_RELOAD)?.run {
tgBot?.sendMessageToTelegram(message) 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() { override suspend fun onDisableAsync() {
config?.let { config -> config?.let fn@{ config ->
if (!config.isEnabled) return if (!config.isEnabled)
return@fn
config.serverStopMessage?.let { config.serverStopMessage?.let {
tgBot?.sendMessageToTelegram(it, blocking = true) tgBot?.sendMessageToTelegram(it)
} }
eventHandler?.let { HandlerList.unregisterAll(it) } eventHandler?.let { HandlerList.unregisterAll(it) }
tgBot?.run { stop() } tgBot?.run { stop() }
@ -66,14 +75,15 @@ class Plugin : JavaPlugin() {
.also { server.broadcastMessage(it) } .also { server.broadcastMessage(it) }
} }
fun reload() { suspend fun reload() {
config = Configuration(this).also { config -> config = Configuration(this).also { config ->
if (!config.isEnabled) return if (!config.isEnabled) return
logger.info(C.INFO.reloading) logger.info(C.INFO.reloading)
eventHandler?.let { HandlerList.unregisterAll(it) } eventHandler?.let { HandlerList.unregisterAll(it) }
tgBot?.run { stop() } tgBot?.run { stop() }
tgBot = TgBot(this, config).also { bot -> tgBot = TgBot(this, config).also { bot ->
eventHandler = EventHandler(bot, config).also { bot.startPolling()
eventHandler = EventHandler(this, config, bot).also {
server.pluginManager.registerEvents(it, this) server.pluginManager.registerEvents(it, this)
} }
} }

View File

@ -4,48 +4,8 @@ import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
import com.google.gson.annotations.SerializedName as Name
interface TgApiService { interface TgApiService {
data class TgResponse<T>(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<BotCommand>)
@GET("deleteWebhook") @GET("deleteWebhook")
suspend fun deleteWebhook( suspend fun deleteWebhook(
@Query("drop_pending_updates") dropPendingUpdates: Boolean @Query("drop_pending_updates") dropPendingUpdates: Boolean

View File

@ -1,14 +1,11 @@
package org.kraftwerk28.spigot_tg_bridge package org.kraftwerk28.spigot_tg_bridge
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Call import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
@ -16,27 +13,36 @@ import retrofit2.converter.gson.GsonConverterFactory
import java.time.Duration import java.time.Duration
import org.kraftwerk28.spigot_tg_bridge.Constants as C import org.kraftwerk28.spigot_tg_bridge.Constants as C
typealias UpdateRequest = Call<TgApiService.TgResponse<List<TgApiService.Update>>>? typealias UpdateRequest = Call<TgResponse<List<Update>>>?
typealias CmdHandler = suspend (HandlerContext) -> Unit
data class HandlerContext(
val update: Update,
val message: Message?,
val chat: Chat?,
val commandArgs: List<String>,
)
class TgBot( class TgBot(
private val plugin: Plugin, private val plugin: Plugin,
private val config: Configuration, private val config: Configuration,
private val pollTimeout: Int = 30,
) { ) {
private val api: TgApiService private val api: TgApiService
private val client: OkHttpClient private val client: OkHttpClient
private val updateChan = Channel<TgApiService.Update>() private val updateChan = Channel<Update>()
private val scope = CoroutineScope(Dispatchers.Default) private var pollJob: Job? = null
private val pollJob: Job private var handlerJob: Job? = null
private val handlerJob: Job
private var currentOffset: Long = -1 private var currentOffset: Long = -1
private var me: TgApiService.User private var me: User? = null
private var commandRegex: Regex private var commandRegex: Regex? = null
private val commandMap = config.commands.run { private val commandMap: Map<String?, CmdHandler> = config.commands.run {
mapOf( mapOf(
online to ::onlineHandler, online to ::onlineHandler,
time to ::timeHandler, time to ::timeHandler,
chatID to ::chatIdHandler, chatID to ::chatIdHandler,
// TODO:
// linkIgn to ::linkIgnHandler,
// getAllLinked to ::getLinkedUsersHandler,
) )
} }
@ -45,41 +51,49 @@ class TgBot(
.Builder() .Builder()
.readTimeout(Duration.ZERO) .readTimeout(Duration.ZERO)
.build() .build()
api = Retrofit.Builder() api = Retrofit.Builder()
.baseUrl("https://api.telegram.org/bot${config.botToken}/") .baseUrl("https://api.telegram.org/bot${config.botToken}/")
.client(client) .client(client)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
.create(TgApiService::class.java) .create(TgApiService::class.java)
}
runBlocking { private suspend fun initialize() {
me = api.getMe().result!! me = api.getMe().result!!
// I intentionally don't put optional @username in regex // I intentionally don't put optional @username in regex
// since bot is only used in group chats // 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) } val commands = config.commands.run { listOf(time, online, chatID) }
.zip( .zip(
C.COMMAND_DESC.run { C.COMMAND_DESC.run {
listOf(timeDesc, onlineDesc, chatIDDesc) listOf(timeDesc, onlineDesc, chatIDDesc)
} }
) )
.map { TgApiService.BotCommand(it.first!!, it.second) } .map { BotCommand(it.first!!, it.second) }
.let { TgApiService.SetMyCommands(it) } .let { SetMyCommands(it) }
api.deleteWebhook(dropPendingUpdates = true)
api.deleteWebhook(true)
api.setMyCommands(commands) api.setMyCommands(commands)
} }
suspend fun startPolling() {
initialize()
pollJob = initPolling() pollJob = initPolling()
handlerJob = initHandler() handlerJob = initHandler()
} }
private fun initPolling() = scope.launch { suspend fun stop() {
pollJob?.cancelAndJoin()
handlerJob?.join()
}
private fun initPolling() = plugin.launch {
loop@ while (true) { loop@ while (true) {
try { try {
api.getUpdates(offset = currentOffset, timeout = pollTimeout) api.getUpdates(
.result?.let { updates -> offset = currentOffset,
timeout = config.pollTimeout,
).result?.let { updates ->
if (!updates.isEmpty()) { if (!updates.isEmpty()) {
updates.forEach { updateChan.send(it) } updates.forEach { updateChan.send(it) }
currentOffset = updates.last().updateId + 1 currentOffset = updates.last().updateId + 1
@ -98,7 +112,7 @@ class TgBot(
updateChan.close() updateChan.close()
} }
private fun initHandler() = scope.launch { private fun initHandler() = plugin.launch {
updateChan.consumeEach { updateChan.consumeEach {
try { try {
handleUpdate(it) handleUpdate(it)
@ -108,29 +122,30 @@ class TgBot(
} }
} }
suspend fun handleUpdate(update: TgApiService.Update) { suspend fun handleUpdate(update: Update) {
// Ignore PM or channel // Ignore PM or channel
if (listOf("private", "channel").contains(update.message?.chat?.type)) if (listOf("private", "channel").contains(update.message?.chat?.type))
return return
var ctx = HandlerContext(
update,
update.message,
update.message?.chat,
listOf(),
)
update.message?.text?.let { update.message?.text?.let {
commandRegex.matchEntire(it)?.groupValues?.let { commandRegex?.matchEntire(it)?.groupValues?.let {
val (command) = it commandMap.get(it[1])?.run {
commandMap.get(command)?.let { it(update) } val args = it[2].split("\\s+".toRegex())
this(ctx.copy(commandArgs = args))
}
} ?: run { } ?: run {
onTextHandler(update) onTextHandler(ctx)
} }
} }
} }
fun stop() { private suspend fun timeHandler(ctx: HandlerContext) {
runBlocking { val msg = ctx.message!!
pollJob.cancelAndJoin()
handlerJob.join()
}
}
private suspend fun timeHandler(update: TgApiService.Update) {
val msg = update.message!!
if (!config.allowedChats.contains(msg.chat.id)) { if (!config.allowedChats.contains(msg.chat.id)) {
return return
} }
@ -156,8 +171,8 @@ class TgBot(
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
} }
private suspend fun onlineHandler(update: TgApiService.Update) { private suspend fun onlineHandler(ctx: HandlerContext) {
val msg = update.message!! val msg = ctx.message!!
if (!config.allowedChats.contains(msg.chat.id)) { if (!config.allowedChats.contains(msg.chat.id)) {
return return
} }
@ -172,8 +187,8 @@ class TgBot(
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId) api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
} }
private suspend fun chatIdHandler(update: TgApiService.Update) { private suspend fun chatIdHandler(ctx: HandlerContext) {
val msg = update.message!! val msg = ctx.message!!
val chatId = msg.chat.id val chatId = msg.chat.id
val text = """ val text = """
|Chat ID: <code>$chatId</code>. |Chat ID: <code>$chatId</code>.
@ -186,8 +201,45 @@ class TgBot(
api.sendMessage(chatId, text, replyToMessageId = msg.messageId) api.sendMessage(chatId, text, replyToMessageId = msg.messageId)
} }
private suspend fun onTextHandler(update: TgApiService.Update) { private suspend fun linkIgnHandler(ctx: HandlerContext) {
val msg = update.message!! 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 = minecraftIgn,
minecraftUuid = mcUuid,
) ?: false
if (linked) {
// TODO
}
}
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 = "<b>Linked users:</b>\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) if (!config.logFromTGtoMC || msg.from == null)
return return
plugin.sendMessageToMinecraft( plugin.sendMessageToMinecraft(
@ -197,22 +249,19 @@ class TgBot(
) )
} }
fun sendMessageToTelegram( suspend fun sendMessageToTelegram(text: String, username: String? = null) {
text: String,
username: String? = null,
blocking: Boolean = false,
) {
val formatted = username?.let { val formatted = username?.let {
config.telegramFormat config.telegramFormat
.replace(C.USERNAME_PLACEHOLDER, username.fullEscape()) .replace(C.USERNAME_PLACEHOLDER, username.fullEscape())
.replace(C.MESSAGE_TEXT_PLACEHOLDER, text.escapeHtml()) .replace(C.MESSAGE_TEXT_PLACEHOLDER, text.escapeHtml())
} ?: text } ?: text
scope.launch {
config.allowedChats.forEach { chatId -> config.allowedChats.forEach { chatId ->
api.sendMessage(chatId, formatted) api.sendMessage(chatId, formatted)
} }
}.also { // plugin.launch {
if (blocking) runBlocking { it.join() } // config.allowedChats.forEach { chatId ->
} // api.sendMessage(chatId, formatted)
// }
// }
} }
} }

View File

@ -1,6 +1,12 @@
package org.kraftwerk28.spigot_tg_bridge package org.kraftwerk28.spigot_tg_bridge
import com.vdurmont.emoji.EmojiParser 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 fun String.escapeHtml() = this
.replace("&", "&amp;") .replace("&", "&amp;")
@ -18,7 +24,72 @@ fun String.fullEscape() = escapeHTML().escapeColorCodes()
fun String.escapeEmoji() = EmojiParser.parseToAliases(this) fun String.escapeEmoji() = EmojiParser.parseToAliases(this)
fun TgApiService.User.rawUserMention(): String = fun User.rawUserMention(): String =
(if (firstName.length < 2) null else firstName) (if (firstName.length < 2) null else firstName)
?: username ?: username
?: lastName!! ?: 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 <T> PreparedStatement.first(convertFn: ResultSet.() -> T): T? {
val result = executeQuery()
return if (result.next()) {
result.convertFn()
} else {
null
}
}
fun <T> PreparedStatement.map(convertFn: ResultSet.() -> T): List<T> {
val resultSet = executeQuery()
val result = mutableListOf<T>()
while (resultSet.next()) {
result.add(resultSet.convertFn())
}
return result
}

View File

@ -7,9 +7,9 @@ serverStopMessage: "Server stopped."
logJoinLeave: true logJoinLeave: true
logFromMCtoTG: true logFromMCtoTG: true
logFromTGtoMC: true logFromTGtoMC: true
logPlayerDeath: false logPlayerDeath: true
logPlayerAsleep: false logPlayerAsleep: false
minecraftFormat: "<%username%>: %message%" minecraftFormat: "§6§l%username%§r (from §o%chat%§r): §b%message%§r"
telegramFormat: "<i>%username%</i>: %message%" telegramFormat: "<i>%username%</i>: %message%"
strings: strings:
online: "<b>Online</b>" online: "<b>Online</b>"
@ -17,6 +17,6 @@ strings:
joined: "<i>%username%</i> joined." joined: "<i>%username%</i> joined."
left: "<i>%username%</i> left." left: "<i>%username%</i> left."
commands: commands:
time: 'time' time: "time"
online: 'online' online: "online"
chat_id: 'chat_id' chat_id: "chat_id"

View File

@ -1,9 +1,8 @@
name: SpigotTGBridge name: SpigotTGBridge
version: "0.18" version: "0.19"
api-version: "1.15" api-version: "1.15"
main: org.kraftwerk28.spigot_tg_bridge.Plugin main: org.kraftwerk28.spigot_tg_bridge.Plugin
description: Telegram <-> Minecraft communication plugin for Spigot. description: Telegram <-> Minecraft communication plugin for Spigot.
load: STARTUP
commands: commands:
tgbridge_reload: tgbridge_reload:
description: "Reload Spigot TG bridge plugin" description: "Reload Spigot TG bridge plugin"