ScalaBot/scalabot-app/src/main/kotlin/AppConfigs.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

365 lines
12 KiB
Kotlin

package net.lamgc.scalabot
import ch.qos.logback.core.PropertyDefinerBase
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import mu.KotlinLogging
import net.lamgc.scalabot.config.*
import net.lamgc.scalabot.config.serializer.*
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.repository.Authentication
import org.eclipse.aether.repository.Proxy
import org.eclipse.aether.repository.RemoteRepository
import org.eclipse.aether.repository.RepositoryPolicy
import org.slf4j.event.Level
import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Supplier
import kotlin.reflect.KProperty
private val log = KotlinLogging.logger { }
internal fun ProxyType.toJavaProxyType(): java.net.Proxy.Type? {
return when (this) {
ProxyType.NO_PROXY -> null
ProxyType.HTTP -> java.net.Proxy.Type.HTTP
ProxyType.HTTPS -> java.net.Proxy.Type.HTTP
ProxyType.SOCKS4 -> java.net.Proxy.Type.SOCKS
ProxyType.SOCKS5 -> java.net.Proxy.Type.SOCKS
}
}
internal fun ProxyConfig.toAetherProxy(): Proxy? {
val typeStr = when (type) {
ProxyType.HTTP -> Proxy.TYPE_HTTP
ProxyType.HTTPS -> Proxy.TYPE_HTTPS
else -> return null
}
return Proxy(typeStr, host, port)
}
internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig? = null): RemoteRepository {
val repositoryId = if (id == null) {
val generatedRepoId = createDefaultRepositoryId()
log.debug { "仓库 Url `$url` 未设置仓库 Id, 已分配缺省 Id: $generatedRepoId" }
generatedRepoId
} else if ("local".contentEquals(id, ignoreCase = true)) {
val generatedRepoId = createDefaultRepositoryId()
log.debug { "仓库 Url `$url` 不允许使用 `local` 作为仓库 Id, 已分配缺省 Id: $generatedRepoId" }
generatedRepoId
} else {
id
}
val builder = RemoteRepository.Builder(repositoryId, checkRepositoryLayout(layout), url.toString())
if (proxy != null) {
val selfProxy = proxy!!
builder.setProxy(selfProxy)
log.debug { "仓库 $repositoryId 已使用独立的代理配置: ${selfProxy.type}://${selfProxy.host}:${selfProxy.port}" }
} else if (proxyConfig != null) {
if (proxyConfig.type in (ProxyType.HTTP..ProxyType.HTTPS)) {
builder.setProxy(proxyConfig.toAetherProxy())
log.debug { "仓库 $repositoryId 已使用 全局/Bot 代理配置: $proxyConfig" }
} else {
log.debug { "仓库 $repositoryId 不支持 全局/Bot 的代理配置: `$proxyConfig` (仅支持 HTTP 和 HTTPS)" }
}
} else {
log.debug { "仓库 $repositoryId 不使用代理." }
}
builder.setReleasePolicy(
RepositoryPolicy(
enableReleases,
RepositoryPolicy.UPDATE_POLICY_NEVER,
RepositoryPolicy.CHECKSUM_POLICY_FAIL
)
)
builder.setSnapshotPolicy(
RepositoryPolicy(
enableSnapshots,
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN
)
)
return builder.build()
}
private fun checkRepositoryLayout(layoutType: String): String {
val type = layoutType.trim().lowercase()
if (type != "default" && type != "legacy") {
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
}
return type
}
private val repoNumberGenerator = AtomicInteger(1)
private fun createDefaultRepositoryId(): String {
return "Repository-${repoNumberGenerator.getAndIncrement()}"
}
/**
* 需要用到的路径.
*
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
*/
internal enum class AppPaths(
private val pathSupplier: PathSupplier,
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
private val fileSupplier: FileSupplier,
) {
/**
* 数据根目录.
*
* 所有运行数据的存放位置.
*
* 提示: 结尾不带 `/`.
*/
DATA_ROOT(fileSupplier = FileSupplier {
File(
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
?: System.getProperty("user.dir") ?: "."
)
}, initializer = {
val f = file
if (!f.exists()) {
f.mkdirs()
}
}),
CONFIG_APPLICATION(PathSupplier { "$DATA_ROOT/config.json" }, {
if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.appConfigGson.toJson(
AppConfig(
mavenRepositories = listOf(
MavenRepositoryConfig(
id = "central",
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
)
)
), it
)
}
}
}),
CONFIG_BOT(PathSupplier { "$DATA_ROOT/bot.json" }, {
if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.botConfigGson.toJson(
setOf(
BotConfig(
enabled = true,
proxy = ProxyConfig(),
account = BotAccount(
"Bot Username",
"Bot API Token",
-1
), extensions = emptySet()
)
), it
)
}
}
}),
DATA_DB({ "$DATA_ROOT/data/db/" }),
DATA_LOGS({ "$DATA_ROOT/data/logs/" }),
EXTENSIONS({ "$DATA_ROOT/extensions/" }),
DATA_EXTENSIONS({ "$DATA_ROOT/data/extensions/" }),
TEMP({ "$DATA_ROOT/tmp/" })
;
constructor(pathSupplier: PathSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
fileSupplier = FileSupplier { File(pathSupplier.path).canonicalFile },
pathSupplier = pathSupplier,
initializer = initializer
)
constructor(fileSupplier: FileSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
fileSupplier = fileSupplier,
pathSupplier = PathSupplier { fileSupplier.file.canonicalPath },
initializer = initializer
)
constructor(pathSupplier: () -> String) : this(
fileSupplier = FileSupplier { File(pathSupplier.invoke()).canonicalFile },
pathSupplier = PathSupplier { pathSupplier.invoke() }
)
val file: File by fileSupplier
val path: String by pathSupplier
private val initialized = AtomicBoolean(false)
@Synchronized
fun initial() {
if (!initialized.get()) {
initializer()
initialized.set(true)
}
}
/**
* 一个内部方法, 用于将 [initialized] 状态重置.
*
* 如果不重置该状态, 将使得单元测试无法让 AppPath 重新初始化文件.
*
* 警告: 该方法不应该被非测试代码调用.
*/
@Suppress("unused")
private fun reset() {
log.warn {
"初始化状态已重置: `${this.name}`, 如果在非测试环境中重置状态, 请报告该问题."
}
initialized.set(false)
}
override fun toString(): String {
return path
}
object PathConst {
const val PROP_DATA_PATH = "bot.path.data"
const val ENV_DATA_PATH = "BOT_DATA_PATH"
}
private class FileSupplier(private val supplier: Supplier<File>) {
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): File = supplier.get()
val file: File
get() = supplier.get()
}
private class PathSupplier(private val supplier: Supplier<String>) {
operator fun getValue(appPaths: AppPaths, property: KProperty<*>): String = supplier.get()
val path: String
get() = supplier.get()
}
}
/**
* 为 LogBack 提供日志目录路径.
*/
internal class LogDirectorySupplier : PropertyDefinerBase() {
override fun getPropertyValue(): String {
return AppPaths.DATA_LOGS.path
}
}
internal class LogLevelSupplier : PropertyDefinerBase() {
override fun getPropertyValue(): String {
val property = System.getProperty("scalabot.log.level", System.getenv("BOT_LOG_LEVEL"))
val level = if (property != null) {
try {
Level.valueOf(property.uppercase())
} catch (e: IllegalArgumentException) {
addWarn("Invalid log level: `$property`, the log will be output using the Info log level.")
Level.INFO
}
} else {
Level.INFO
}
return level.name
}
}
internal class NetworkVerboseLogSupplier : PropertyDefinerBase() {
override fun getPropertyValue(): String {
val propertyValue = System.getProperty("scalabot.log.network.verbose", "false")
return if (propertyValue.toBoolean()) {
"DEBUG"
} else {
"INFO"
}
}
}
internal object Const {
val config = loadAppConfig()
const val METRICS_NAMESPACE = "scalabot"
}
private fun AppPaths.defaultInitializer() {
val f = file
val p = path
if (!f.exists()) {
val result = if (p.endsWith("/")) {
f.mkdirs()
} else {
f.createNewFile()
}
if (!result) {
log.warn { "初始化文件(夹)失败: $p" }
}
}
}
/**
* 执行 AppPaths 所有项目的初始化, 并检查是否停止运行, 让用户编辑配置.
*
* @return 如果需要让用户编辑配置, 则返回 `true`.
*/
internal fun initialFiles(): Boolean {
val configFilesNotInitialized = !AppPaths.CONFIG_APPLICATION.file.exists()
&& !AppPaths.CONFIG_BOT.file.exists()
for (path in AppPaths.entries) {
path.initial()
}
if (configFilesNotInitialized) {
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
return true
}
return false
}
internal object GsonConst {
private val baseGson: Gson = GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create()
val appConfigGson: Gson = baseGson.newBuilder()
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
.registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer)
.create()
val botConfigGson: Gson = baseGson.newBuilder()
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
.registerTypeAdapter(BotAccount::class.java, BotAccountSerializer)
.create()
}
internal fun loadAppConfig(configFile: File = AppPaths.CONFIG_APPLICATION.file): AppConfig {
try {
configFile.bufferedReader(StandardCharsets.UTF_8).use {
return GsonConst.appConfigGson.fromJson(it, AppConfig::class.java)!!
}
} catch (e: Exception) {
log.error { "读取 config.json 时发生错误, 请检查配置格式是否正确." }
throw e
}
}
internal fun loadBotConfigJson(botConfigFile: File = AppPaths.CONFIG_BOT.file): JsonArray? {
try {
botConfigFile.bufferedReader(StandardCharsets.UTF_8).use {
return GsonConst.botConfigGson.fromJson(it, JsonArray::class.java)!!
}
} catch (e: Exception) {
log.error(e) { "读取 Bot 配置文件 (bot.json) 时发生错误, 请检查配置格式是否正确." }
return null
}
}