feat(meta): 新增 meta 模块.

将与配置相关的内容迁移到 scalabot-meta 模块.
其他项目可以使用 meta 模块来生成 ScalaBot 的配置文件, 通过配置文件管理 ScalaBot 的运行.

BREAKING CHANGE: 与配置有关的类迁移到了 scalabot-meta 模块.

目前仅所有配置类 (以 `Config` 结尾的 Class) 和相应的序列化类 (以 `Serializer` 结尾的) 都迁移到了 meta 模块, 但其工具方法则作为扩展函数保留在 app 模块中.
这么做的好处是为了方便其他应用 (例如 ScalaBot 外部管理程序) 根据需要生成配置文件.
scalabot-meta 将会作为依赖项发布, 可根据需要获取 ScalaBot-meta 生成 ScalaBot 的配置.
 + 此次改动普通用户无需迁移。
This commit is contained in:
2022-06-27 19:24:52 +08:00
committed by GitHub
19 changed files with 1862 additions and 859 deletions

View File

@ -5,14 +5,14 @@ import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import mu.KotlinLogging
import net.lamgc.scalabot.util.*
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.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.ApiConstants
import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets
@ -22,154 +22,65 @@ import kotlin.system.exitProcess
private val log = KotlinLogging.logger { }
/**
* 机器人帐号信息.
* @property name 机器人名称, 建议与实际设定的名称相同.
* @property token 机器人 API Token.
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
*/
internal data class BotAccount(
val name: String,
val token: String,
val creatorId: Long = -1
) {
val id
// 不要想着每次获取都要从 token 里取出有性能损耗.
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
get() = token.substringBefore(":").toLong()
internal fun ProxyType.toTelegramBotsType(): DefaultBotOptions.ProxyType {
return when (this) {
ProxyType.NO_PROXY -> DefaultBotOptions.ProxyType.NO_PROXY
ProxyType.HTTP -> DefaultBotOptions.ProxyType.HTTP
ProxyType.HTTPS -> DefaultBotOptions.ProxyType.HTTP
ProxyType.SOCKS4 -> DefaultBotOptions.ProxyType.SOCKS4
ProxyType.SOCKS5 -> DefaultBotOptions.ProxyType.SOCKS5
}
}
/**
* 机器人配置.
* @property account 机器人帐号信息, 用于访问 API.
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
* @property extensions 该机器人启用的扩展.
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
*/
internal data class BotConfig(
val enabled: Boolean = true,
val account: BotAccount,
val disableBuiltInAbility: Boolean = false,
val autoUpdateCommandList: Boolean = false,
/*
* 使用构件坐标来选择机器人所使用的扩展包.
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目时一定会设置的,
* 所以就直接用了. :P
*/
val extensions: Set<Artifact>,
val proxy: ProxyConfig? = ProxyConfig(),
val baseApiUrl: String? = ApiConstants.BASE_URL
)
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)
}
/**
* 代理配置.
* @property type 代理类型.
* @property host 代理服务端地址.
* @property port 代理服务端端口.
*/
internal data class ProxyConfig(
val type: DefaultBotOptions.ProxyType = DefaultBotOptions.ProxyType.NO_PROXY,
val host: String = "127.0.0.1",
val port: Int = 1080
) {
fun toAetherProxy(): Proxy? {
return if (type == DefaultBotOptions.ProxyType.HTTP) {
Proxy(Proxy.TYPE_HTTP, host, port)
} else {
null
}
internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
val builder =
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
if (proxy != null) {
builder.setProxy(proxy)
} else if (proxyConfig.type == ProxyType.HTTP) {
builder.setProxy(proxyConfig.toAetherProxy())
}
}
internal data class MetricsConfig(
val enable: Boolean = false,
val port: Int = 9386,
val bindAddress: String? = "0.0.0.0",
val authenticator: UsernameAuthenticator? = null
)
/**
* Maven 远端仓库配置.
* @property url 仓库地址.
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
*/
internal data class MavenRepositoryConfig(
val id: String? = null,
val url: URL,
val proxy: Proxy? = null,
val layout: String = "default",
val enableReleases: Boolean = true,
val enableSnapshots: Boolean = true,
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
val authentication: Authentication? = null
) {
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
val builder =
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
if (proxy != null) {
builder.setProxy(proxy)
} else if (proxyConfig.type == DefaultBotOptions.ProxyType.HTTP) {
builder.setProxy(proxyConfig.toAetherProxy())
}
builder.setReleasePolicy(
RepositoryPolicy(
enableReleases,
RepositoryPolicy.UPDATE_POLICY_NEVER,
RepositoryPolicy.CHECKSUM_POLICY_FAIL
)
builder.setReleasePolicy(
RepositoryPolicy(
enableReleases,
RepositoryPolicy.UPDATE_POLICY_NEVER,
RepositoryPolicy.CHECKSUM_POLICY_FAIL
)
builder.setSnapshotPolicy(
RepositoryPolicy(
enableSnapshots,
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN
)
)
builder.setSnapshotPolicy(
RepositoryPolicy(
enableSnapshots,
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN
)
)
return builder.build()
}
private companion object {
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 repoNumber = AtomicInteger(1)
fun createDefaultRepositoryId(): String {
return "Repository-${repoNumber.getAndIncrement()}"
}
}
return builder.build()
}
/**
* ScalaBot App 配置.
*
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
* @property proxy Telegram API 代理配置.
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
* @property mavenRepositories Maven 远端仓库配置.
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
*/
internal data class AppConfig(
val proxy: ProxyConfig = ProxyConfig(),
val metrics: MetricsConfig = MetricsConfig(),
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
val mavenLocalRepository: String? = null
)
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()}"
}
/**
* 需要用到的路径.
@ -222,7 +133,7 @@ internal enum class AppPaths(
GsonConst.botConfigGson.toJson(
setOf(
BotConfig(
enabled = false,
enabled = true,
proxy = ProxyConfig(),
account = BotAccount(
"Bot Username",
@ -317,14 +228,15 @@ private object GsonConst {
.create()
val appConfigGson: Gson = baseGson.newBuilder()
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
.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(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
.registerTypeAdapter(ProxyType::class.java, ProxyTypeSerializer)
.registerTypeAdapter(BotConfig::class.java, BotConfigSerializer)
.registerTypeAdapter(Artifact::class.java, ArtifactSerializer)
.create()
}

View File

@ -3,6 +3,10 @@ package net.lamgc.scalabot
import io.prometheus.client.exporter.HTTPServer
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import net.lamgc.scalabot.config.AppConfig
import net.lamgc.scalabot.config.BotConfig
import net.lamgc.scalabot.config.MetricsConfig
import net.lamgc.scalabot.config.ProxyType
import net.lamgc.scalabot.util.registerShutdownHook
import org.eclipse.aether.repository.LocalRepository
import org.telegram.telegrambots.bots.DefaultBotOptions
@ -71,9 +75,9 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
private fun getMavenLocalRepository(): LocalRepository {
val localPath =
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
if (config.mavenLocalRepository != null && config.mavenLocalRepository!!.isNotEmpty()) {
val repoPath = AppPaths.DATA_ROOT.file.toPath()
.resolve(config.mavenLocalRepository)
.resolve(config.mavenLocalRepository!!)
.apply {
if (!exists()) {
if (!parent.isWritable() || !parent.isReadable()) {
@ -133,23 +137,21 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
val botOption = DefaultBotOptions().apply {
val proxyConfig =
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
if (botConfig.proxy.type != ProxyType.NO_PROXY) {
botConfig.proxy
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
} else if (config.proxy.type != ProxyType.NO_PROXY) {
config.proxy
} else {
null
}
if (proxyConfig != null) {
proxyType = proxyConfig.type
proxyType = proxyConfig.type.toTelegramBotsType()
proxyHost = config.proxy.host
proxyPort = config.proxy.port
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
}
if (botConfig.baseApiUrl != null) {
baseUrl = botConfig.baseApiUrl
}
baseUrl = botConfig.baseApiUrl
}
val account = botConfig.account

View File

@ -2,6 +2,7 @@ package net.lamgc.scalabot
import com.google.common.io.Files
import mu.KotlinLogging
import net.lamgc.scalabot.config.BotAccount
import net.lamgc.scalabot.util.toHexString
import org.mapdb.DB
import org.mapdb.DBException

View File

@ -4,6 +4,7 @@ import io.prometheus.client.Counter
import io.prometheus.client.Gauge
import io.prometheus.client.Summary
import mu.KotlinLogging
import net.lamgc.scalabot.config.BotConfig
import org.eclipse.aether.artifact.Artifact
import org.telegram.abilitybots.api.bot.AbilityBot
import org.telegram.abilitybots.api.db.DBContext

View File

@ -1,127 +0,0 @@
package net.lamgc.scalabot.util
import com.google.gson.*
import net.lamgc.scalabot.MavenRepositoryConfig
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.repository.Authentication
import org.eclipse.aether.repository.Proxy
import org.eclipse.aether.util.repository.AuthenticationBuilder
import org.telegram.telegrambots.bots.DefaultBotOptions
import java.lang.reflect.Type
import java.net.URL
internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
JsonSerializer<DefaultBotOptions.ProxyType> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext?
): DefaultBotOptions.ProxyType {
if (json.isJsonNull) {
return DefaultBotOptions.ProxyType.NO_PROXY
}
if (!json.isJsonPrimitive) {
throw JsonParseException("Wrong configuration value type.")
}
val value = json.asString.trim()
try {
return DefaultBotOptions.ProxyType.valueOf(value.uppercase())
} catch (e: IllegalArgumentException) {
throw JsonParseException("Invalid value: $value")
}
}
override fun serialize(
src: DefaultBotOptions.ProxyType,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonPrimitive(src.toString())
}
}
internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
if (!src.extension.equals("jar")) {
gavBuilder.append(':').append(src.extension)
}
if (src.classifier.isNotEmpty()) {
gavBuilder.append(':').append(src.classifier)
}
return JsonPrimitive(gavBuilder.append(':').append(src.version).toString())
}
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Artifact {
if (!json.isJsonPrimitive) {
throw JsonParseException("Wrong configuration value type.")
}
return DefaultArtifact(json.asString.trim())
}
}
internal object AuthenticationSerializer : JsonDeserializer<Authentication> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
if (json !is JsonObject) {
throw JsonParseException("Unsupported JSON type.")
}
val username = SerializerUtils.checkJsonKey(json, "username")
val password = SerializerUtils.checkJsonKey(json, "password")
val builder = AuthenticationBuilder()
builder.addUsername(username)
builder.addPassword(password)
return builder.build()
}
}
private object SerializerUtils {
fun checkJsonKey(json: JsonObject, key: String): String {
if (!json.has(key)) {
throw JsonParseException("Required field does not exist: $key")
} else if (!json.get(key).isJsonPrimitive) {
throw JsonParseException("Wrong field `$key` type: ${json.get(key)::class.java}")
}
return json.get(key).asString
}
}
internal object MavenRepositoryConfigSerializer
: JsonDeserializer<MavenRepositoryConfig> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): MavenRepositoryConfig {
return when (json) {
is JsonObject -> {
MavenRepositoryConfig(
id = json.get("id")?.asString,
url = URL(SerializerUtils.checkJsonKey(json, "url")),
proxy = if (json.has("proxy") && json.get("proxy").isJsonObject)
context.deserialize<Proxy>(
json.getAsJsonObject("proxy"), Proxy::class.java
) else null,
layout = json.get("layout")?.asString ?: "default",
enableReleases = json.get("enableReleases")?.asBoolean ?: true,
enableSnapshots = json.get("enableSnapshots")?.asBoolean ?: true,
authentication = if (json.has("authentication") && json.get("authentication").isJsonObject)
context.deserialize<Authentication>(
json.getAsJsonObject("authentication"), Authentication::class.java
) else null
)
}
is JsonPrimitive -> {
MavenRepositoryConfig(url = URL(json.asString))
}
else -> {
throw JsonParseException("Unsupported Maven warehouse configuration type.")
}
}
}
}

View File

@ -1,65 +0,0 @@
package net.lamgc.scalabot.util
import com.google.gson.*
import com.sun.net.httpserver.BasicAuthenticator
import java.lang.reflect.Type
class UsernameAuthenticator(private val username: String, private val password: String) :
BasicAuthenticator("metrics") {
override fun checkCredentials(username: String?, password: String?): Boolean =
this.username == username && this.password == password
fun toJsonObject(): JsonObject = JsonObject().apply {
addProperty("username", username)
addProperty("password", password)
}
override fun equals(other: Any?): Boolean {
return other is UsernameAuthenticator && this.username == other.username && this.password == other.password
}
override fun hashCode(): Int {
var result = username.hashCode()
result = 31 * result + password.hashCode()
return result
}
}
object UsernameAuthenticatorSerializer : JsonSerializer<UsernameAuthenticator>,
JsonDeserializer<UsernameAuthenticator> {
override fun serialize(
src: UsernameAuthenticator,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return src.toJsonObject()
}
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext?
): UsernameAuthenticator? {
if (json.isJsonNull) {
return null
} else if (!json.isJsonObject) {
throw JsonParseException("Invalid attribute value type.")
}
val jsonObj = json.asJsonObject
if (jsonObj["username"]?.isJsonPrimitive != true) {
throw JsonParseException("Invalid attribute value: username")
} else if (jsonObj["password"]?.isJsonPrimitive != true) {
throw JsonParseException("Invalid attribute value: password")
}
if (jsonObj["username"].asString.isEmpty() || jsonObj["password"].asString.isEmpty()) {
throw JsonParseException("`username` or `password` is empty.")
}
return UsernameAuthenticator(jsonObj["username"].asString, jsonObj["password"].asString)
}
}