Skip to content

Commit ee28986

Browse files
committed
Implement config hot-reloading
Add `silentMessages` option Add ability to change bot API endpoint
1 parent 4bcd274 commit ee28986

File tree

10 files changed

+145
-89
lines changed

10 files changed

+145
-89
lines changed

README.md

+23-20
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424
6. Add you bot to chats, where you plan to use it. In each of them, run `/chat_id` command. The bot should respond and give special value - __chat id__. Now, open `config.yml` and paste this ID under `chats` section, so it will look like this:
2525
```yaml
2626
botToken: abcdefghijklmnopq123123123
27-
chats:
28-
# Note about doubling minus sign. This is not a mistake, first one means list element, the second one - actual minus
29-
- -123456789
30-
- 987654321
27+
chats: [
28+
-123456789,
29+
987654321,
3130
# other chat id's...
31+
]
3232
```
3333

3434
7. You can extend `config.yml` with more tweaks, which are described in the table below, but it's not nesessary, plugin will use default values instead, if they're missing. Also, check out the [example](src/main/resources/config.yml).
@@ -38,22 +38,25 @@
3838

3939
## Plugin configuration:
4040

41-
| Field | Description | Type | Required | Default |
42-
|:-----:|:------------|:----:|:--------:|:-------:|
43-
| enable | If plugin should be enabled | `boolean` | :x: | `true` |
44-
| botToken | Telegram bot token ([How to create bot](https://core.telegram.org/bots#3-how-do-i-create-a-bot)) | `string` | :heavy_check_mark: | - |
45-
| chats | Chats, where bot will work (to prevent using bot by unknown chats) | `number[] or string[]` | :heavy_check_mark: | `[]` |
46-
| serverStartMessage | What will be sent to chats when server starts | `string` | :x: | `'Server started.'` |
47-
| serverStopMessage | What will be sent to chats when server stops | `string` | :x: | `'Server stopped.'` |
48-
| logJoinLeave | If true, plugin will send corresponding messages to chats, when player joins or leaves server | `boolean` | :x: | `true` |
49-
| logFromMCtoTG | If true, plugin will send messages from players on server, to Telegram chats | `boolean` | :x: | `true` |
50-
| logFromTGtoMC | If true, plugin will send messages from chats, to Minecraft server | `boolean` | :x: | `true` |
51-
| logPlayerDeath | If true, plugin will send message to Telegram if player died | `boolean` | :x: | `false` |
52-
| logPlayerAsleep | If true, plugin will send message to Telegram if player fell asleep | `boolean` | :x: | `false` |
53-
| strings | Dictionary of tokens - strings for plugin i18n | `Map<string, string>` | :x: | See default config |
54-
| commands | Dictionary of command text used in Telegram bot | `Map<string, string>` | :heavy_check_mark: | See below |
55-
| telegramMessageFormat | Format string for TGtoMC chat message | `string` | :x: | See default config |
56-
| minecraftMessageFormat | Format string for MCtoTG chat message | `string` | :x: | See default config |
41+
| Field | Description | Type | Required | Default |
42+
|:----------------------:|:-------------------------------------------------------------------------------------------------|:----------------------:|:--------:|:------------------------:|
43+
| enable | If plugin should be enabled | `boolean` | :x: | `true` |
44+
| botToken | Telegram bot token ([How to create bot](https://core.telegram.org/bots#3-how-do-i-create-a-bot)) | `string` | :heavy_check_mark: | - |
45+
| chats | Chats, where bot will work (to prevent using bot by unknown chats) | `number[] or string[]` | :heavy_check_mark: | `[]` |
46+
| serverStartMessage | What will be sent to chats when server starts | `string` | :x: | `'Server started.'` |
47+
| serverStopMessage | What will be sent to chats when server stops | `string` | :x: | `'Server stopped.'` |
48+
| logJoinLeave | If true, plugin will send corresponding messages to chats, when player joins or leaves server | `boolean` | :x: | `true` |
49+
| logFromMCtoTG | If true, plugin will send messages from players on server, to Telegram chats | `boolean` | :x: | `true` |
50+
| logFromTGtoMC | If true, plugin will send messages from chats, to Minecraft server | `boolean` | :x: | `true` |
51+
| logPlayerDeath | If true, plugin will send message to Telegram if player died | `boolean` | :x: | `false` |
52+
| logPlayerAsleep | If true, plugin will send message to Telegram if player fell asleep | `boolean` | :x: | `false` |
53+
| strings | Dictionary of tokens - strings for plugin i18n | `Map<string, string>` | :x: | See default config |
54+
| commands | Dictionary of command text used in Telegram bot | `Map<string, string>` | :heavy_check_mark: | See below |
55+
| telegramMessageFormat | Format string for TGtoMC chat message | `string` | :x: | See default config |
56+
| minecraftMessageFormat | Format string for MCtoTG chat message | `string` | :x: | See default config |
57+
| silentMessages | Disable notification in Telegram chats | `boolean` | :x: | See default config |
58+
| apiOrigin | Use different API endpoint for the bot | `string` | :x: | https://api.telegram.org |
59+
| disableConfigWatch | Do not watch the config for changes | `string` | :x: | https://api.telegram.org |
5760

5861

5962
## Telegram bot commands:

build.gradle.kts

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ buildscript {
1616
}
1717

1818
plugins {
19-
id("org.jetbrains.kotlin.jvm") version "1.4.31"
19+
id("org.jetbrains.kotlin.jvm") version "1.6.10"
2020
id("com.github.johnrengelman.shadow") version "5.2.0"
2121
id("org.jlleitschuh.gradle.ktlint") version "10.1.0"
2222
}
@@ -60,8 +60,8 @@ tasks {
6060
}
6161
register<Copy>("copyArtifacts") {
6262
val dest = File(
63-
System.getProperty("user.home"),
64-
"MinecraftServers/spigot_1.17/plugins/",
63+
System.getenv("HOME"),
64+
"projects/minecraft/spigot/spigot-1.18.1/plugins",
6565
)
6666
from(shadowJar)
6767
into(dest)

src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/AsyncJavaPlugin.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package org.kraftwerk28.spigot_tg_bridge
22

33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.Dispatchers
5-
import kotlinx.coroutines.launch
6-
import kotlinx.coroutines.runBlocking
75
import kotlinx.coroutines.Job
86
import kotlinx.coroutines.cancelAndJoin
7+
import kotlinx.coroutines.launch
8+
import kotlinx.coroutines.runBlocking
99
import org.bukkit.plugin.java.JavaPlugin
1010

1111
open class AsyncJavaPlugin : JavaPlugin() {
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package org.kraftwerk28.spigot_tg_bridge
22

3+
import kotlinx.coroutines.CancellationException
4+
import kotlinx.coroutines.runInterruptible
5+
import org.bukkit.configuration.file.YamlConfiguration
36
import java.io.File
7+
import java.nio.file.FileSystems
8+
import java.nio.file.StandardWatchEventKinds
49
import org.kraftwerk28.spigot_tg_bridge.Constants as C
510

6-
class Configuration(plugin: Plugin) {
11+
class Configuration(plugin: Plugin) : YamlConfiguration() {
712
val isEnabled: Boolean
813
val logFromMCtoTG: Boolean
914
val telegramFormat: String
@@ -18,6 +23,7 @@ class Configuration(plugin: Plugin) {
1823
val onlineString: String
1924
val nobodyOnlineString: String
2025
val enableIgnAuth: Boolean
26+
val silentMessages: Boolean?
2127

2228
// Telegram bot stuff
2329
val botToken: String
@@ -26,6 +32,8 @@ class Configuration(plugin: Plugin) {
2632
val allowWebhook: Boolean
2733
val webhookConfig: Map<String, Any>?
2834
val pollTimeout: Int
35+
val apiOrigin: String
36+
val debugHttp: Boolean
2937

3038
var commands: BotCommands
3139

@@ -37,74 +45,105 @@ class Configuration(plugin: Plugin) {
3745
// plugin.saveResource(C.configFilename, false);
3846
throw Exception(C.WARN.noConfigWarning)
3947
}
40-
val pluginConfig = plugin.config
41-
pluginConfig.load(cfgFile)
4248

43-
pluginConfig.getString("minecraftMessageFormat")?.let {
49+
load(cfgFile)
50+
51+
if (!getBoolean("disableConfigWatch", false)) {
52+
try {
53+
val watchService = FileSystems.getDefault().newWatchService()
54+
val cfgPath = cfgFile.parentFile.toPath()
55+
val pathKey = cfgPath.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY)
56+
plugin.launch {
57+
loop@ while (true) {
58+
try {
59+
val watchKey = runInterruptible { watchService.take() }
60+
val events = watchKey.pollEvents()
61+
events.find {
62+
it.kind() == StandardWatchEventKinds.ENTRY_MODIFY
63+
}?.let {
64+
plugin.reload()
65+
}
66+
} catch (e: Exception) {
67+
when (e) {
68+
is CancellationException -> break@loop
69+
else -> {
70+
e.printStackTrace()
71+
continue@loop
72+
}
73+
}
74+
}
75+
}
76+
pathKey.cancel()
77+
}
78+
} catch (e: Exception) {
79+
plugin.logger.info("Failed to set up watch on config file")
80+
}
81+
}
82+
83+
getString("minecraftMessageFormat")?.let {
4484
plugin.logger.warning(
4585
"""
4686
Config option "minecraftMessageFormat" is deprecated.
4787
Moved it to new key "telegramFormat"
4888
""".trimIndent().replace('\n', ' ')
4989
)
50-
pluginConfig.set("telegramFormat", it)
51-
pluginConfig.set("minecraftMessageFormat", null)
90+
set("telegramFormat", it)
91+
set("minecraftMessageFormat", null)
5292
plugin.saveConfig()
5393
}
5494

55-
pluginConfig.getString("telegramMessageFormat")?.let {
95+
getString("telegramMessageFormat")?.let {
5696
plugin.logger.warning(
5797
"""
5898
Config option "telegramMessageFormat" is deprecated.
5999
Moved it to new key "minecraftFormat"
60100
""".trimIndent().replace('\n', ' ')
61101
)
62-
pluginConfig.set("minecraftFormat", it)
63-
pluginConfig.set("telegramMessageFormat", null)
102+
set("minecraftFormat", it)
103+
set("telegramMessageFormat", null)
64104
plugin.saveConfig()
65105
}
66106

67-
pluginConfig.run {
68-
isEnabled = getBoolean("enable", true)
69-
serverStartMessage = getString("serverStartMessage")
70-
serverStopMessage = getString("serverStopMessage")
71-
logFromTGtoMC = getBoolean("logFromTGtoMC", true)
72-
logFromMCtoTG = getBoolean("logFromMCtoTG", true)
73-
telegramFormat = getString(
74-
"telegramFormat",
75-
"<i>%username%</i>: %message%",
76-
)!!
77-
minecraftFormat = getString(
78-
"minecraftFormat",
79-
"<%username%>: %message%",
80-
)!!
81-
// isEnabled = getBoolean("enable", true)
82-
allowedChats = getLongList("chats")
83-
enableIgnAuth = getBoolean("enableIgnAuth", false)
107+
isEnabled = getBoolean("enable", true)
108+
serverStartMessage = getString("serverStartMessage")
109+
serverStopMessage = getString("serverStopMessage")
110+
logFromTGtoMC = getBoolean("logFromTGtoMC", true)
111+
logFromMCtoTG = getBoolean("logFromMCtoTG", true)
112+
telegramFormat = getString(
113+
"telegramFormat",
114+
"<i>%username%</i>: %message%",
115+
)!!
116+
minecraftFormat = getString(
117+
"minecraftFormat",
118+
"<%username%>: %message%",
119+
)!!
120+
// isEnabled = getBoolean("enable", true)
121+
allowedChats = getLongList("chats")
122+
enableIgnAuth = getBoolean("enableIgnAuth", false)
84123

85-
botToken = getString("botToken") ?: throw Exception(C.WARN.noToken)
86-
allowWebhook = getBoolean("useWebhook", false)
87-
@Suppress("unchecked_cast")
88-
webhookConfig = get("webhookConfig") as Map<String, Any>?
89-
pollTimeout = getInt("pollTimeout", 30)
90-
91-
logJoinLeave = getBoolean("logJoinLeave", false)
92-
onlineString = getString("strings.online", "Online")!!
93-
nobodyOnlineString = getString(
94-
"strings.nobodyOnline",
95-
"Nobody online",
96-
)!!
97-
joinString = getString(
98-
"strings.joined",
99-
"<i>%username%</i> joined.",
100-
)!!
101-
leaveString = getString("strings.left", "<i>%username%</i> left.")!!
102-
logDeath = getBoolean("logPlayerDeath", false)
103-
logPlayerAsleep = getBoolean("logPlayerAsleep", false)
104-
commands = BotCommands(this)
105-
}
106-
}
124+
botToken = getString("botToken") ?: throw Exception(C.WARN.noToken)
125+
allowWebhook = getBoolean("useWebhook", false)
126+
@Suppress("unchecked_cast")
127+
webhookConfig = get("webhookConfig") as Map<String, Any>?
128+
pollTimeout = getInt("pollTimeout", 30)
107129

108-
companion object {
130+
logJoinLeave = getBoolean("logJoinLeave", false)
131+
onlineString = getString("strings.online", "Online")!!
132+
nobodyOnlineString = getString(
133+
"strings.nobodyOnline",
134+
"Nobody online",
135+
)!!
136+
joinString = getString(
137+
"strings.joined",
138+
"<i>%username%</i> joined.",
139+
)!!
140+
leaveString = getString("strings.left", "<i>%username%</i> left.")!!
141+
logDeath = getBoolean("logPlayerDeath", false)
142+
logPlayerAsleep = getBoolean("logPlayerAsleep", false)
143+
commands = BotCommands(this)
144+
// NB: Setting to null, if false, because API expects either `true` or absent parameter
145+
silentMessages = getBoolean("silentMessages").let { if (!it) null else true }
146+
apiOrigin = getString("apiOrigin", "https://api.telegram.org")!!
147+
debugHttp = getBoolean("debugHttp", false)
109148
}
110149
}

src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Plugin.kt

-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package org.kraftwerk28.spigot_tg_bridge
22

3-
import kotlinx.coroutines.delay
43
import org.bukkit.event.HandlerList
54
import java.lang.Exception
6-
import kotlin.system.measureTimeMillis
75
import org.kraftwerk28.spigot_tg_bridge.Constants as C
86

97
class Plugin : AsyncJavaPlugin() {

src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgApiService.kt

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface TgApiService {
1616
@Query("chat_id") chatId: Long,
1717
@Query("text") text: String,
1818
@Query("reply_to_message_id") replyToMessageId: Long? = null,
19+
@Query("disable_notification") disableNotification: Boolean? = null,
1920
): TgResponse<Message>
2021

2122
@GET("getUpdates")

src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/TgBot.kt

+22-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlinx.coroutines.cancelAndJoin
66
import kotlinx.coroutines.channels.Channel
77
import kotlinx.coroutines.channels.consumeEach
88
import okhttp3.OkHttpClient
9+
import okhttp3.logging.HttpLoggingInterceptor
910
import retrofit2.Retrofit
1011
import retrofit2.converter.gson.GsonConverterFactory
1112
import java.time.Duration
@@ -26,10 +27,18 @@ class TgBot(
2627
) {
2728
private val client: OkHttpClient = OkHttpClient
2829
.Builder()
30+
// Disable timeout to make long-polling possible
2931
.readTimeout(Duration.ZERO)
32+
.addInterceptor(
33+
HttpLoggingInterceptor().apply {
34+
level =
35+
if (config.debugHttp) HttpLoggingInterceptor.Level.BODY
36+
else HttpLoggingInterceptor.Level.NONE
37+
}
38+
)
3039
.build()
3140
private val api = Retrofit.Builder()
32-
.baseUrl("https://api.telegram.org/bot${config.botToken}/")
41+
.baseUrl("${config.apiOrigin}/bot${config.botToken}/")
3342
.client(client)
3443
.addConverterFactory(GsonConverterFactory.create())
3544
.build()
@@ -183,11 +192,13 @@ class TgBot(
183192
val chatId = msg.chat.id
184193
val text = """
185194
|Chat ID: <code>$chatId</code>.
186-
|Copy this id to <code>chats</code> section in your <b>config.yml</b> file so it will look like this:
187-
|
188-
|<pre>chats:
189-
| # other ids...
190-
| - $chatId</pre>
195+
|Copy this id to <code>chats</code> section in your <b>config.yml</b> file so it looks like this:
196+
|<pre>
197+
|chats: [
198+
| $chatId,
199+
| # other chat ids...
200+
|]
201+
|</pre>
191202
""".trimMargin()
192203
api.sendMessage(chatId, text, replyToMessageId = msg.messageId)
193204
}
@@ -247,7 +258,11 @@ class TgBot(
247258
.replace(C.MESSAGE_TEXT_PLACEHOLDER, text.escapeHtml())
248259
} ?: text
249260
config.allowedChats.forEach { chatId ->
250-
api.sendMessage(chatId, formatted)
261+
try {
262+
api.sendMessage(chatId, formatted, disableNotification = config.silentMessages)
263+
} catch (e: Exception) {
264+
e.printStackTrace()
265+
}
251266
}
252267
}
253268
}

src/main/kotlin/org/kraftwerk28/spigot_tg_bridge/Utils.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ fun User.rawUserMention(): String =
3131

3232
fun DbLinkedUser.fullName() = tgFirstName + (tgLastName?.let { " $it" } ?: "")
3333

34-
fun Connection.stmt(query: String, vararg args: Any?) =
34+
fun Connection.stmt(query: String, vararg args: Any?): PreparedStatement =
3535
prepareStatement(query).apply {
3636
args.zip(1..args.size).forEach { (arg, i) ->
3737
when (arg) {
@@ -42,7 +42,7 @@ fun Connection.stmt(query: String, vararg args: Any?) =
4242
}
4343
}
4444

45-
suspend fun checkMinecraftLicense(playerUuid: String): Boolean = try {
45+
fun checkMinecraftLicense(playerUuid: String): Boolean = try {
4646
val urlString = "https://api.mojang.com/user/profiles/$playerUuid/names"
4747
val conn = (URL(urlString).openConnection() as HttpURLConnection).apply {
4848
requestMethod = "GET"
@@ -53,7 +53,7 @@ suspend fun checkMinecraftLicense(playerUuid: String): Boolean = try {
5353
false
5454
}
5555

56-
suspend fun getMinecraftUuidByUsername(username: String): String? = try {
56+
fun getMinecraftUuidByUsername(username: String): String? = try {
5757
val urlString = "https://api.mojang.com/users/profiles/minecraft/$username"
5858
val conn = (URL(urlString).openConnection() as HttpURLConnection).apply {
5959
requestMethod = "GET"

src/main/resources/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
enable: true
22
botToken: abcdef123456789
3-
chats:
4-
- -1234567890123
3+
chats: []
54
serverStartMessage: "Server started."
65
serverStopMessage: "Server stopped."
76
logJoinLeave: true
@@ -11,6 +10,7 @@ logPlayerDeath: true
1110
logPlayerAsleep: false
1211
minecraftFormat: "§6§l%username%§r (from §o%chat%§r): §b%message%§r"
1312
telegramFormat: "<i>%username%</i>: %message%"
13+
silentMessages: false
1414
strings:
1515
online: "<b>Online</b>"
1616
nobodyOnline: "<b>Nobody online...</b>"

0 commit comments

Comments
 (0)