mirror of
https://github.com/amalthea-mc/spigot-tg-bridge.git
synced 2024-11-23 10:52:26 +00:00
Add basic IgnAuth functionality
This commit is contained in:
parent
d6404d3be2
commit
8c401887ad
@ -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<Job> = mutableListOf()
|
||||||
|
|
||||||
|
override fun onEnable() {
|
||||||
|
runBlocking { onEnableAsync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisable() {
|
||||||
|
runBlocking {
|
||||||
|
onDisableAsync()
|
||||||
|
jobs.joinAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun onEnableAsync() = Unit
|
||||||
|
|
||||||
|
open suspend fun onDisableAsync() = Unit
|
||||||
|
|
||||||
|
fun <T> launch(f: suspend () -> T) = scope.launch {
|
||||||
|
f()
|
||||||
|
}.also {
|
||||||
|
jobs.add(it)
|
||||||
|
}
|
||||||
|
}
|
28
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt
Normal file
28
src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Auth.kt
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
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
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
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)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warning(e.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config?.let { config ->
|
|
||||||
if (!config.isEnabled) return
|
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?.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,14 @@ 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 {
|
eventHandler = EventHandler(this, config, bot).also {
|
||||||
server.pluginManager.registerEvents(it, this)
|
server.pluginManager.registerEvents(it, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,75 +13,84 @@ 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>>>?
|
||||||
|
|
||||||
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?, suspend (u: Update) -> Unit> =
|
||||||
mapOf(
|
config.commands.run {
|
||||||
online to ::onlineHandler,
|
mapOf(
|
||||||
time to ::timeHandler,
|
online to ::onlineHandler,
|
||||||
chatID to ::chatIdHandler,
|
time to ::timeHandler,
|
||||||
)
|
chatID to ::chatIdHandler,
|
||||||
}
|
linkIgn to ::linkIgnHandler,
|
||||||
|
getAllLinked to ::getLinkedUsersHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
client = OkHttpClient
|
client = OkHttpClient
|
||||||
.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})$""".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,
|
||||||
if (!updates.isEmpty()) {
|
timeout = config.pollTimeout,
|
||||||
updates.forEach { updateChan.send(it) }
|
).result?.let { updates ->
|
||||||
currentOffset = updates.last().updateId + 1
|
if (!updates.isEmpty()) {
|
||||||
}
|
updates.forEach { updateChan.send(it) }
|
||||||
|
currentOffset = updates.last().updateId + 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
when (e) {
|
when (e) {
|
||||||
is CancellationException -> break@loop
|
is CancellationException -> break@loop
|
||||||
@ -98,7 +104,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,28 +114,20 @@ 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
|
||||||
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])?.let { it(update) }
|
||||||
commandMap.get(command)?.let { it(update) }
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
onTextHandler(update)
|
onTextHandler(update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
private suspend fun timeHandler(update: Update) {
|
||||||
runBlocking {
|
|
||||||
pollJob.cancelAndJoin()
|
|
||||||
handlerJob.join()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun timeHandler(update: TgApiService.Update) {
|
|
||||||
val msg = update.message!!
|
val msg = update.message!!
|
||||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||||
return
|
return
|
||||||
@ -156,7 +154,7 @@ 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(update: Update) {
|
||||||
val msg = update.message!!
|
val msg = update.message!!
|
||||||
if (!config.allowedChats.contains(msg.chat.id)) {
|
if (!config.allowedChats.contains(msg.chat.id)) {
|
||||||
return
|
return
|
||||||
@ -172,7 +170,7 @@ 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(update: Update) {
|
||||||
val msg = update.message!!
|
val msg = update.message!!
|
||||||
val chatId = msg.chat.id
|
val chatId = msg.chat.id
|
||||||
val text = """
|
val text = """
|
||||||
@ -186,7 +184,24 @@ 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(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!!
|
val msg = update.message!!
|
||||||
if (!config.logFromTGtoMC || msg.from == null)
|
if (!config.logFromTGtoMC || msg.from == null)
|
||||||
return
|
return
|
||||||
@ -197,22 +212,34 @@ class TgBot(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessageToTelegram(
|
private suspend fun getLinkedUsersHandler(update: Update) {
|
||||||
text: String,
|
val linkedUsers = plugin.ignAuth?.run {
|
||||||
username: String? = null,
|
getAllLinkedUsers()
|
||||||
blocking: Boolean = false,
|
} ?: listOf()
|
||||||
) {
|
if (linkedUsers.isEmpty()) {
|
||||||
|
api.sendMessage(update.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(update.message!!.chat.id, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendMessageToTelegram(text: String, username: String? = null) {
|
||||||
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 {
|
|
||||||
if (blocking) runBlocking { it.join() }
|
|
||||||
}
|
}
|
||||||
|
// plugin.launch {
|
||||||
|
// config.allowedChats.forEach { chatId ->
|
||||||
|
// api.sendMessage(chatId, formatted)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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("&", "&")
|
.replace("&", "&")
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user