mirror of
https://github.com/amalthea-mc/spigot-tg-bridge.git
synced 2024-11-08 11:41:05 +00:00
Merge pull request #21 from kraftwerk28/ign-auth
Basic IGNAuth implementation (still not ready) + stability fixes
This commit is contained in:
commit
a8aa799b96
@ -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() }
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<String, Any>?
|
||||
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<String, Any>?
|
||||
pollTimeout = getInt("pollTimeout", 30)
|
||||
|
||||
logJoinLeave = getBoolean("logJoinLeave", false)
|
||||
onlineString = getString("strings.online", "Online")!!
|
||||
nobodyOnlineString = getString(
|
||||
|
@ -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 {
|
||||
|
59
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt
Normal file
59
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Entities.kt
Normal 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,
|
||||
)
|
@ -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, "<i>$username</i>")
|
||||
tgBot.sendMessageToTelegram(text)
|
||||
sendMessage(text)
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +54,12 @@ class EventHandler(
|
||||
if (event.bedEnterResult != PlayerBedEnterEvent.BedEnterResult.OK)
|
||||
return
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt
Normal file
90
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/IgnAuth.kt
Normal 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() }
|
||||
}
|
@ -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,15 @@ 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 {
|
||||
bot.startPolling()
|
||||
eventHandler = EventHandler(this, config, bot).also {
|
||||
server.pluginManager.registerEvents(it, this)
|
||||
}
|
||||
}
|
||||
|
@ -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<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")
|
||||
suspend fun deleteWebhook(
|
||||
@Query("drop_pending_updates") dropPendingUpdates: Boolean
|
||||
|
@ -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,27 +13,36 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.time.Duration
|
||||
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(
|
||||
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<TgApiService.Update>()
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
private val pollJob: Job
|
||||
private val handlerJob: Job
|
||||
private val updateChan = Channel<Update>()
|
||||
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 {
|
||||
private var me: User? = null
|
||||
private var commandRegex: Regex? = null
|
||||
private val commandMap: Map<String?, CmdHandler> = config.commands.run {
|
||||
mapOf(
|
||||
online to ::onlineHandler,
|
||||
time to ::timeHandler,
|
||||
chatID to ::chatIdHandler,
|
||||
// TODO:
|
||||
// linkIgn to ::linkIgnHandler,
|
||||
// getAllLinked to ::getLinkedUsersHandler,
|
||||
)
|
||||
}
|
||||
|
||||
@ -45,46 +51,54 @@ class TgBot(
|
||||
.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})(?:\s+(.+))?$""".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 +112,7 @@ class TgBot(
|
||||
updateChan.close()
|
||||
}
|
||||
|
||||
private fun initHandler() = scope.launch {
|
||||
private fun initHandler() = plugin.launch {
|
||||
updateChan.consumeEach {
|
||||
try {
|
||||
handleUpdate(it)
|
||||
@ -108,29 +122,30 @@ 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
|
||||
var ctx = HandlerContext(
|
||||
update,
|
||||
update.message,
|
||||
update.message?.chat,
|
||||
listOf(),
|
||||
)
|
||||
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])?.run {
|
||||
val args = it[2].split("\\s+".toRegex())
|
||||
this(ctx.copy(commandArgs = args))
|
||||
}
|
||||
} ?: run {
|
||||
onTextHandler(update)
|
||||
onTextHandler(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
runBlocking {
|
||||
pollJob.cancelAndJoin()
|
||||
handlerJob.join()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun timeHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
private suspend fun timeHandler(ctx: HandlerContext) {
|
||||
val msg = ctx.message!!
|
||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||
return
|
||||
}
|
||||
@ -156,8 +171,8 @@ class TgBot(
|
||||
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
private suspend fun onlineHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
private suspend fun onlineHandler(ctx: HandlerContext) {
|
||||
val msg = ctx.message!!
|
||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||
return
|
||||
}
|
||||
@ -172,8 +187,8 @@ class TgBot(
|
||||
api.sendMessage(msg.chat.id, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
private suspend fun chatIdHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
private suspend fun chatIdHandler(ctx: HandlerContext) {
|
||||
val msg = ctx.message!!
|
||||
val chatId = msg.chat.id
|
||||
val text = """
|
||||
|Chat ID: <code>$chatId</code>.
|
||||
@ -186,8 +201,45 @@ class TgBot(
|
||||
api.sendMessage(chatId, text, replyToMessageId = msg.messageId)
|
||||
}
|
||||
|
||||
private suspend fun onTextHandler(update: TgApiService.Update) {
|
||||
val msg = update.message!!
|
||||
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 = 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)
|
||||
return
|
||||
plugin.sendMessageToMinecraft(
|
||||
@ -197,22 +249,19 @@ class TgBot(
|
||||
)
|
||||
}
|
||||
|
||||
fun sendMessageToTelegram(
|
||||
text: String,
|
||||
username: String? = null,
|
||||
blocking: Boolean = false,
|
||||
) {
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -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 <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
|
||||
}
|
||||
|
@ -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: "<i>%username%</i>: %message%"
|
||||
strings:
|
||||
online: "<b>Online</b>"
|
||||
@ -17,6 +17,6 @@ strings:
|
||||
joined: "<i>%username%</i> joined."
|
||||
left: "<i>%username%</i> left."
|
||||
commands:
|
||||
time: 'time'
|
||||
online: 'online'
|
||||
chat_id: 'chat_id'
|
||||
time: "time"
|
||||
online: "online"
|
||||
chat_id: "chat_id"
|
||||
|
@ -1,9 +1,8 @@
|
||||
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.
|
||||
load: STARTUP
|
||||
commands:
|
||||
tgbridge_reload:
|
||||
description: "Reload Spigot TG bridge plugin"
|
||||
|
Loading…
Reference in New Issue
Block a user