ScalaBot/scalabot-app/src/main/kotlin/AppMain.kt
LamGC e1c87aeae4
feat: 将 TelegramBots 升级至 8.0.0, 并适配 TelegramBots 的新改动.
将 TelegramBots 升级至新版本, 以支持新的 API.
由于 TelegramBots 发生无法兼容旧版本的重大变更, 因此 ScalaBot 将随着此次更新一同进行重大更改.

BREAKING CHANGE: ScalaBot 所依赖的 TelegramBots 发生重大更改, 所有扩展都需要进行适配.
  有关 TelegramBots 的重大变更说明请参考官方文档.
  ScalaBot 的最低 Java 版本已全部升级至 Java 17 (这是 TelegramBots 的最低兼容性要求), 所有扩展都应该至少迁移至 Java 17 版本.
  ScalaBot 的重大更改:
   - scalabot-extension
     - `net.lamgc.scalabot.extension.util.AbilityBots.getBotAccountId(BaseAbilityBot): long` 已被移除, 由于 BaseAbilityBot 不再允许获取 botToken, 因此该方法被移除. 作为代替, 请通过 `net.lamgc.scalabot.extension.BotExtensionFactory.createExtensionInstance` 所得到的 `BotExtensionCreateOptions` 中获取 botAccountId.

  另外, scalabot-extension 中的 `org.jetbrains.kotlinx.binary-compatibility-validator` 似乎不再对 Java 代码起作用, 因此移除该插件, 并在后续寻找替代品.
  TelegramBots 文档: https://rubenlagus.github.io/TelegramBotsDocumentation/how-to-update-7.html
2024-12-10 23:32:29 +08:00

256 lines
9.8 KiB
Kotlin

package net.lamgc.scalabot
import com.google.gson.JsonParseException
import io.prometheus.client.exporter.HTTPServer
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import net.lamgc.scalabot.config.*
import net.lamgc.scalabot.util.registerShutdownHook
import okhttp3.OkHttpClient
import org.eclipse.aether.repository.LocalRepository
import org.telegram.telegrambots.client.okhttp.OkHttpTelegramClient
import org.telegram.telegrambots.longpolling.BotSession
import org.telegram.telegrambots.longpolling.TelegramBotsLongPollingApplication
import org.telegram.telegrambots.meta.api.methods.GetMe
import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException
import java.io.File
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import kotlin.io.path.createDirectories
import kotlin.io.path.exists
import kotlin.io.path.isReadable
import kotlin.io.path.isWritable
import kotlin.system.exitProcess
private val log = KotlinLogging.logger { }
fun main(args: Array<String>): Unit = runBlocking {
log.info { "ScalaBot 正在启动中..." }
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
log.debug { "Kotlin: ${KotlinVersion.CURRENT}, JVM: ${Runtime.version()}" }
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
if (initialFiles()) {
exitProcess(1)
}
val launcher = Launcher()
.registerShutdownHook()
startMetricsServer()?.registerShutdownHook()
if (!launcher.launch()) {
exitProcess(1)
}
}
/**
* 启动运行指标服务器.
* 使用 Prometheus 指标格式.
*/
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics): HTTPServer? {
if (!config.enable) {
log.debug { "运行指标服务器已禁用." }
return null
}
val builder = HTTPServer.Builder()
.withDaemonThreads(true)
.withAuthenticator(config.authenticator)
.withPort(config.port)
.withHostname(config.bindAddress)
val httpServer = builder
.build()
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
return httpServer
}
internal class Launcher(
private val config: AppConfig = Const.config,
private val configFile: File = AppPaths.CONFIG_APPLICATION.file,
) : AutoCloseable {
companion object {
@JvmStatic
private val log = KotlinLogging.logger { }
}
private val botApi = TelegramBotsLongPollingApplication()
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
private val mavenLocalRepository = getMavenLocalRepository()
private fun getMavenLocalRepository(): LocalRepository {
val localPath =
if (config.mavenLocalRepository != null && config.mavenLocalRepository!!.isNotEmpty()) {
val repoPath = configFile.toPath().resolve(config.mavenLocalRepository!!).apply {
if (!exists()) {
if (!parent.isWritable() || !parent.isReadable()) {
throw IOException("Unable to read and write the directory where Maven repository is located.")
}
if (System.getProperty("os.name").lowercase().startsWith("windows")) {
createDirectories()
} else {
val fileAttributes = setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.OTHERS_READ,
)
createDirectories(PosixFilePermissions.asFileAttribute(fileAttributes))
}
}
}
.toRealPath()
.toFile()
repoPath
} else {
File("${System.getProperty("user.home")}/.m2/repository")
}
if (!localPath.exists()) {
localPath.mkdirs()
}
return LocalRepository(localPath)
}
@Synchronized
fun launch(): Boolean {
val botConfigs = loadBotConfigJson() ?: return false
if (botConfigs.isEmpty) {
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
return false
}
var launchedCounts = 0
for (botConfigJson in botConfigs) {
val botConfig = try {
GsonConst.botConfigGson.fromJson(botConfigJson, BotConfig::class.java)
} catch (e: JsonParseException) {
val botName = try {
botConfigJson.asJsonObject.get("account")?.asJsonObject?.get("name")?.asString ?: "Unknown"
} catch (e: Exception) {
"Unknown"
}
log.error(e) { "机器人 `$botName` 配置有误, 跳过该机器人的启动." }
continue
}
try {
launchBot(botConfig)
launchedCounts++
} catch (e: Exception) {
if (e is TelegramApiRequestException && e.errorCode == 401) {
log.error { "机器人 `${botConfig.account.name}` 的 Bot Token 无效, 请检查配置: [${e.errorCode}] ${e.apiResponse}" }
} else {
log.error(e) { "机器人 `${botConfig.account.name}` 启动时发生错误." }
}
}
}
botApi.start()
botApi.registerShutdownHook()
return if (launchedCounts != 0) {
log.info { "已启动 $launchedCounts 个机器人." }
true
} else {
log.warn { "未启动任何机器人, 请检查配置并至少启用一个机器人." }
false
}
}
private fun launchBot(botConfig: BotConfig) {
if (!botConfig.enabled) {
log.debug { "机器人 `${botConfig.account.name}` 已禁用, 跳过启动." }
return
}
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
val proxyConfig =
if (botConfig.proxy.type != ProxyType.NO_PROXY) {
log.debug { "[Bot ${botConfig.account.name}] 使用独立代理: ${botConfig.proxy.type}" }
botConfig.proxy
} else if (config.proxy.type != ProxyType.NO_PROXY) {
log.debug { "[Bot ${botConfig.account.name}] 使用全局代理: ${botConfig.proxy.type}" }
config.proxy
} else {
log.debug { "[Bot ${botConfig.account.name}] 不使用代理." }
ProxyConfig(type = ProxyType.NO_PROXY)
}
val okhttpClientBuilder = OkHttpClient.Builder()
if (proxyConfig.type != ProxyType.NO_PROXY) {
val proxyType = proxyConfig.type.toJavaProxyType()
val proxyAddress = InetSocketAddress.createUnresolved(proxyConfig.host, proxyConfig.port)
okhttpClientBuilder.proxy(Proxy(proxyType, proxyAddress))
}
val account = botConfig.account
val telegramClient =
OkHttpTelegramClient(okhttpClientBuilder.build(), account.token, botConfig.getBaseApiTelegramUrl())
val remoteRepositories = config.mavenRepositories
.map { it.toRemoteRepository(proxyConfig) }
.toMutableList().apply {
if (this.none {
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
}) {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = proxyConfig.toAetherProxy()))
}
}.toList()
val extensionPackageFinders = setOf(
MavenRepositoryExtensionFinder(
localRepository = mavenLocalRepository,
remoteRepositories = remoteRepositories,
proxy = config.proxy.toAetherProxy()
)
)
val bot = ScalaBot(
BotDBMaker.getBotDbInstance(account),
telegramClient,
extensionPackageFinders,
botConfig
)
val botUser = bot.telegramClient.execute(GetMe())
log.debug { "已验证 Bot Token 有效性, Bot Username: ${botUser.userName}" }
botSessionMap[bot] = botApi.registerBot(botConfig.account.token, bot)
log.info { "机器人 `${bot.botUsername}` 已启动." }
if (botConfig.autoUpdateCommandList) {
log.debug { "[Bot ${botConfig.account.name}] 正在自动更新命令列表..." }
try {
val result = bot.updateCommandList()
if (result) {
log.info { "[Bot ${botConfig.account.name}] 已成功更新 Bot 命令列表." }
} else {
log.warn { "[Bot ${botConfig.account.name}] 自动更新 Bot 命令列表失败!" }
}
} catch (e: Exception) {
log.warn(e) { "命令列表自动更新失败." }
}
}
}
@Synchronized
override fun close() {
botSessionMap.forEach {
try {
if (!it.value.isRunning) {
return@forEach
}
log.info { "正在关闭机器人 `${it.key.botUsername}` ..." }
it.value.stop()
log.info { "已关闭机器人 `${it.key.botUsername}`." }
} catch (e: Exception) {
log.error(e) { "机器人 `${it.key.botUsername}` 关闭时发生异常." }
}
}
}
}