mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-04-29 14:17:30 +00:00
refactor(config): 将与配置相关的内容迁移到 scalabot-meta 模块.
通过将配置迁移到单独的模块, 可以方便使用其他程序扩展 ScaleBot, 而不仅仅是让 ScaleBot 成为扩展的平台. BREAKING CHANGE: 与配置有关的 Class 移动到了 scalabot-meta 模块. 目前仅所有配置类(以 `Config` 结尾的 Class)和相应的序列化类(以 `Serializer` 结尾的)都迁移到了 meta 模块, 但其工具方法则作为扩展函数保留在 app 模块中. 这么做的好处是为了方便其他应用(例如 ScalaBot 外部管理程序)根据需要生成配置文件. scalabot-meta 将会作为依赖项发布, 可根据需要获取 ScalaBot-meta 生成 ScalaBot 的配置. 此次改动普通用户无需迁移.
This commit is contained in:
parent
dbc4232dd6
commit
289b9678f2
@ -8,6 +8,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(":scalabot-meta"))
|
||||||
implementation(project(":scalabot-extension"))
|
implementation(project(":scalabot-extension"))
|
||||||
|
|
||||||
implementation("org.slf4j:slf4j-api:1.7.36")
|
implementation("org.slf4j:slf4j-api:1.7.36")
|
||||||
|
@ -5,14 +5,14 @@ import com.google.gson.Gson
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import mu.KotlinLogging
|
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.artifact.Artifact
|
||||||
import org.eclipse.aether.repository.Authentication
|
import org.eclipse.aether.repository.Authentication
|
||||||
import org.eclipse.aether.repository.Proxy
|
import org.eclipse.aether.repository.Proxy
|
||||||
import org.eclipse.aether.repository.RemoteRepository
|
import org.eclipse.aether.repository.RemoteRepository
|
||||||
import org.eclipse.aether.repository.RepositoryPolicy
|
import org.eclipse.aether.repository.RepositoryPolicy
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||||
import org.telegram.telegrambots.meta.ApiConstants
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@ -22,154 +22,63 @@ import kotlin.system.exitProcess
|
|||||||
|
|
||||||
private val log = KotlinLogging.logger { }
|
private val log = KotlinLogging.logger { }
|
||||||
|
|
||||||
/**
|
internal fun ProxyType.toTelegramBotsType(): DefaultBotOptions.ProxyType {
|
||||||
* 机器人帐号信息.
|
return when (this) {
|
||||||
* @property name 机器人名称, 建议与实际设定的名称相同.
|
ProxyType.NO_PROXY -> DefaultBotOptions.ProxyType.NO_PROXY
|
||||||
* @property token 机器人 API Token.
|
ProxyType.HTTP -> DefaultBotOptions.ProxyType.HTTP
|
||||||
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
ProxyType.SOCKS4 -> DefaultBotOptions.ProxyType.SOCKS4
|
||||||
*/
|
ProxyType.SOCKS5 -> DefaultBotOptions.ProxyType.SOCKS5
|
||||||
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 ProxyConfig.toAetherProxy(): Proxy? {
|
||||||
* 机器人配置.
|
return if (type == ProxyType.HTTP) {
|
||||||
* @property account 机器人帐号信息, 用于访问 API.
|
Proxy(Proxy.TYPE_HTTP, host, port)
|
||||||
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
} else {
|
||||||
* @property extensions 该机器人启用的扩展.
|
null
|
||||||
* @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 MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
|
||||||
* 代理配置.
|
val builder =
|
||||||
* @property type 代理类型.
|
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
|
||||||
* @property host 代理服务端地址.
|
if (proxy != null) {
|
||||||
* @property port 代理服务端端口.
|
builder.setProxy(proxy)
|
||||||
*/
|
} else if (proxyConfig.type == ProxyType.HTTP) {
|
||||||
internal data class ProxyConfig(
|
builder.setProxy(proxyConfig.toAetherProxy())
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
builder.setReleasePolicy(
|
||||||
|
RepositoryPolicy(
|
||||||
internal data class MetricsConfig(
|
enableReleases,
|
||||||
val enable: Boolean = false,
|
RepositoryPolicy.UPDATE_POLICY_NEVER,
|
||||||
val port: Int = 9386,
|
RepositoryPolicy.CHECKSUM_POLICY_FAIL
|
||||||
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.setSnapshotPolicy(
|
)
|
||||||
RepositoryPolicy(
|
builder.setSnapshotPolicy(
|
||||||
enableSnapshots,
|
RepositoryPolicy(
|
||||||
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
enableSnapshots,
|
||||||
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
|
||||||
)
|
RepositoryPolicy.CHECKSUM_POLICY_WARN
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return builder.build()
|
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()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
internal fun checkRepositoryLayout(layoutType: String): String {
|
||||||
* ScalaBot App 配置.
|
val type = layoutType.trim().lowercase()
|
||||||
*
|
if (type != "default" && type != "legacy") {
|
||||||
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
|
||||||
* @property proxy Telegram API 代理配置.
|
}
|
||||||
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
return type
|
||||||
* @property mavenRepositories Maven 远端仓库配置.
|
}
|
||||||
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
|
||||||
*/
|
private val repoNumberGenerator = AtomicInteger(1)
|
||||||
internal data class AppConfig(
|
|
||||||
val proxy: ProxyConfig = ProxyConfig(),
|
internal fun createDefaultRepositoryId(): String {
|
||||||
val metrics: MetricsConfig = MetricsConfig(),
|
return "Repository-${repoNumberGenerator.getAndIncrement()}"
|
||||||
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
}
|
||||||
val mavenLocalRepository: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需要用到的路径.
|
* 需要用到的路径.
|
||||||
|
@ -3,6 +3,10 @@ package net.lamgc.scalabot
|
|||||||
import io.prometheus.client.exporter.HTTPServer
|
import io.prometheus.client.exporter.HTTPServer
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import mu.KotlinLogging
|
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 net.lamgc.scalabot.util.registerShutdownHook
|
||||||
import org.eclipse.aether.repository.LocalRepository
|
import org.eclipse.aether.repository.LocalRepository
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
import org.telegram.telegrambots.bots.DefaultBotOptions
|
||||||
@ -71,9 +75,9 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
|||||||
|
|
||||||
private fun getMavenLocalRepository(): LocalRepository {
|
private fun getMavenLocalRepository(): LocalRepository {
|
||||||
val localPath =
|
val localPath =
|
||||||
if (config.mavenLocalRepository != null && config.mavenLocalRepository.isNotEmpty()) {
|
if (config.mavenLocalRepository != null && config.mavenLocalRepository!!.isNotEmpty()) {
|
||||||
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
val repoPath = AppPaths.DATA_ROOT.file.toPath()
|
||||||
.resolve(config.mavenLocalRepository)
|
.resolve(config.mavenLocalRepository!!)
|
||||||
.apply {
|
.apply {
|
||||||
if (!exists()) {
|
if (!exists()) {
|
||||||
if (!parent.isWritable() || !parent.isReadable()) {
|
if (!parent.isWritable() || !parent.isReadable()) {
|
||||||
@ -136,15 +140,15 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
|
|||||||
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
|
log.info { "正在启动机器人 `${botConfig.account.name}`..." }
|
||||||
val botOption = DefaultBotOptions().apply {
|
val botOption = DefaultBotOptions().apply {
|
||||||
val proxyConfig =
|
val proxyConfig =
|
||||||
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
if (botConfig.proxy != null && botConfig.proxy!!.type != ProxyType.NO_PROXY) {
|
||||||
botConfig.proxy
|
botConfig.proxy
|
||||||
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
|
} else if (config.proxy.type != ProxyType.NO_PROXY) {
|
||||||
config.proxy
|
config.proxy
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (proxyConfig != null) {
|
if (proxyConfig != null) {
|
||||||
proxyType = proxyConfig.type
|
proxyType = proxyConfig.type.toTelegramBotsType()
|
||||||
proxyHost = config.proxy.host
|
proxyHost = config.proxy.host
|
||||||
proxyPort = config.proxy.port
|
proxyPort = config.proxy.port
|
||||||
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
|
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
|
||||||
|
@ -2,6 +2,7 @@ package net.lamgc.scalabot
|
|||||||
|
|
||||||
import com.google.common.io.Files
|
import com.google.common.io.Files
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.BotAccount
|
||||||
import net.lamgc.scalabot.util.toHexString
|
import net.lamgc.scalabot.util.toHexString
|
||||||
import org.mapdb.DB
|
import org.mapdb.DB
|
||||||
import org.mapdb.DBException
|
import org.mapdb.DBException
|
||||||
|
@ -4,6 +4,7 @@ import io.prometheus.client.Counter
|
|||||||
import io.prometheus.client.Gauge
|
import io.prometheus.client.Gauge
|
||||||
import io.prometheus.client.Summary
|
import io.prometheus.client.Summary
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.lamgc.scalabot.config.BotConfig
|
||||||
import org.eclipse.aether.artifact.Artifact
|
import org.eclipse.aether.artifact.Artifact
|
||||||
import org.telegram.abilitybots.api.bot.AbilityBot
|
import org.telegram.abilitybots.api.bot.AbilityBot
|
||||||
import org.telegram.abilitybots.api.db.DBContext
|
import org.telegram.abilitybots.api.db.DBContext
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,50 +1,16 @@
|
|||||||
package net.lamgc.scalabot
|
package net.lamgc.scalabot
|
||||||
|
|
||||||
import com.github.stefanbirkner.systemlambda.SystemLambda
|
import com.github.stefanbirkner.systemlambda.SystemLambda
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.jupiter.api.io.TempDir
|
import org.junit.jupiter.api.io.TempDir
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
internal class BotAccountTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `id getter`() {
|
|
||||||
val accountId = abs(Random().nextInt()).toLong()
|
|
||||||
assertEquals(accountId, BotAccount("Test", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", 0).id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun deserializerTest() {
|
|
||||||
val accountId = abs(Random().nextInt()).toLong()
|
|
||||||
val creatorId = abs(Random().nextInt()).toLong()
|
|
||||||
val botAccountJsonObject = Gson().fromJson(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"name": "TestBot",
|
|
||||||
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
|
||||||
"creatorId": $creatorId
|
|
||||||
}
|
|
||||||
""".trimIndent(), JsonObject::class.java
|
|
||||||
)
|
|
||||||
val botAccount = Gson().fromJson(botAccountJsonObject, BotAccount::class.java)
|
|
||||||
assertEquals(botAccountJsonObject["name"].asString, botAccount.name)
|
|
||||||
assertEquals(botAccountJsonObject["token"].asString, botAccount.token)
|
|
||||||
assertEquals(accountId, botAccount.id, "BotAccount ID does not match expectations.")
|
|
||||||
assertEquals(creatorId, botAccount.creatorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class AppPathsTest {
|
internal class AppPathsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
30
scalabot-meta/build.gradle.kts
Normal file
30
scalabot-meta/build.gradle.kts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.6.10"
|
||||||
|
id("org.jetbrains.kotlinx.kover") version "0.5.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "net.lamgc"
|
||||||
|
version = "0.3.1"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
val aetherVersion = "1.1.0"
|
||||||
|
implementation("org.eclipse.aether:aether-api:$aetherVersion")
|
||||||
|
implementation("org.eclipse.aether:aether-util:$aetherVersion")
|
||||||
|
|
||||||
|
implementation("org.telegram:telegrambots-meta:6.0.1")
|
||||||
|
|
||||||
|
implementation("com.google.code.gson:gson:2.9.0")
|
||||||
|
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
testImplementation("io.mockk:mockk:1.12.4")
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
||||||
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.getByName<Test>("test") {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
108
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
108
scalabot-meta/src/main/kotlin/Configs.kt
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import org.eclipse.aether.artifact.Artifact
|
||||||
|
import org.eclipse.aether.repository.Authentication
|
||||||
|
import org.eclipse.aether.repository.Proxy
|
||||||
|
import org.telegram.telegrambots.meta.ApiConstants
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人帐号信息.
|
||||||
|
* @property name 机器人名称, 建议与实际设定的名称相同.
|
||||||
|
* @property token 机器人 API Token.
|
||||||
|
* @property creatorId 机器人创建者, 管理机器人需要使用该信息.
|
||||||
|
*/
|
||||||
|
data class BotAccount(
|
||||||
|
val name: String,
|
||||||
|
val token: String,
|
||||||
|
val creatorId: Long = -1
|
||||||
|
) {
|
||||||
|
|
||||||
|
val id
|
||||||
|
// 不要想着每次获取都要从 token 里取出有性能损耗.
|
||||||
|
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
|
||||||
|
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
|
||||||
|
get() = token.substringBefore(":").toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机器人配置.
|
||||||
|
* @property account 机器人帐号信息, 用于访问 API.
|
||||||
|
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
|
||||||
|
* @property extensions 该机器人启用的扩展.
|
||||||
|
* @property proxy 为该机器人单独设置的代理配置, 如无设置, 则使用 AppConfig 中的代理配置.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ProxyType {
|
||||||
|
NO_PROXY,
|
||||||
|
HTTP,
|
||||||
|
SOCKS4,
|
||||||
|
SOCKS5
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理配置.
|
||||||
|
* @property type 代理类型.
|
||||||
|
* @property host 代理服务端地址.
|
||||||
|
* @property port 代理服务端端口.
|
||||||
|
*/
|
||||||
|
data class ProxyConfig(
|
||||||
|
val type: ProxyType = ProxyType.NO_PROXY,
|
||||||
|
val host: String = "127.0.0.1",
|
||||||
|
val port: Int = 1080
|
||||||
|
)
|
||||||
|
|
||||||
|
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`.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScalaBot App 配置.
|
||||||
|
*
|
||||||
|
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
|
||||||
|
* @property proxy Telegram API 代理配置.
|
||||||
|
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
|
||||||
|
* @property mavenRepositories Maven 远端仓库配置.
|
||||||
|
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
|
||||||
|
*/
|
||||||
|
data class AppConfig(
|
||||||
|
val proxy: ProxyConfig = ProxyConfig(),
|
||||||
|
val metrics: MetricsConfig = MetricsConfig(),
|
||||||
|
val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
|
||||||
|
val mavenLocalRepository: String? = null
|
||||||
|
)
|
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
26
scalabot-meta/src/main/kotlin/UsernameAuthenticator.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.sun.net.httpserver.BasicAuthenticator
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,40 +1,41 @@
|
|||||||
package net.lamgc.scalabot.util
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import net.lamgc.scalabot.MavenRepositoryConfig
|
import net.lamgc.scalabot.config.MavenRepositoryConfig
|
||||||
|
import net.lamgc.scalabot.config.ProxyType
|
||||||
|
import net.lamgc.scalabot.config.UsernameAuthenticator
|
||||||
import org.eclipse.aether.artifact.Artifact
|
import org.eclipse.aether.artifact.Artifact
|
||||||
import org.eclipse.aether.artifact.DefaultArtifact
|
import org.eclipse.aether.artifact.DefaultArtifact
|
||||||
import org.eclipse.aether.repository.Authentication
|
import org.eclipse.aether.repository.Authentication
|
||||||
import org.eclipse.aether.repository.Proxy
|
import org.eclipse.aether.repository.Proxy
|
||||||
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
import org.eclipse.aether.util.repository.AuthenticationBuilder
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
|
object ProxyTypeSerializer : JsonDeserializer<ProxyType>,
|
||||||
JsonSerializer<DefaultBotOptions.ProxyType> {
|
JsonSerializer<ProxyType> {
|
||||||
|
|
||||||
override fun deserialize(
|
override fun deserialize(
|
||||||
json: JsonElement,
|
json: JsonElement,
|
||||||
typeOfT: Type?,
|
typeOfT: Type?,
|
||||||
context: JsonDeserializationContext?
|
context: JsonDeserializationContext?
|
||||||
): DefaultBotOptions.ProxyType {
|
): ProxyType {
|
||||||
if (json.isJsonNull) {
|
if (json.isJsonNull) {
|
||||||
return DefaultBotOptions.ProxyType.NO_PROXY
|
return ProxyType.NO_PROXY
|
||||||
}
|
}
|
||||||
if (!json.isJsonPrimitive) {
|
if (!json.isJsonPrimitive) {
|
||||||
throw JsonParseException("Wrong configuration value type.")
|
throw JsonParseException("Wrong configuration value type.")
|
||||||
}
|
}
|
||||||
val value = json.asString.trim()
|
val value = json.asString.trim()
|
||||||
try {
|
try {
|
||||||
return DefaultBotOptions.ProxyType.valueOf(value.uppercase())
|
return ProxyType.valueOf(value.uppercase())
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw JsonParseException("Invalid value: $value")
|
throw JsonParseException("Invalid value: $value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(
|
override fun serialize(
|
||||||
src: DefaultBotOptions.ProxyType,
|
src: ProxyType,
|
||||||
typeOfSrc: Type?,
|
typeOfSrc: Type?,
|
||||||
context: JsonSerializationContext?
|
context: JsonSerializationContext?
|
||||||
): JsonElement {
|
): JsonElement {
|
||||||
@ -42,7 +43,7 @@ internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyTy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
|
||||||
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||||
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
|
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
|
||||||
if (!src.extension.equals("jar")) {
|
if (!src.extension.equals("jar")) {
|
||||||
@ -63,7 +64,7 @@ internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
object AuthenticationSerializer : JsonDeserializer<Authentication> {
|
||||||
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
|
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication {
|
||||||
if (json !is JsonObject) {
|
if (json !is JsonObject) {
|
||||||
@ -90,7 +91,7 @@ private object SerializerUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object MavenRepositoryConfigSerializer
|
object MavenRepositoryConfigSerializer
|
||||||
: JsonDeserializer<MavenRepositoryConfig> {
|
: JsonDeserializer<MavenRepositoryConfig> {
|
||||||
|
|
||||||
override fun deserialize(
|
override fun deserialize(
|
||||||
@ -125,3 +126,41 @@ internal object MavenRepositoryConfigSerializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
scalabot-meta/src/test/kotlin/BotAccountTest.kt
Normal file
38
scalabot-meta/src/test/kotlin/BotAccountTest.kt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package net.lamgc.scalabot.config
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
internal class BotAccountTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `id getter`() {
|
||||||
|
val accountId = abs(Random().nextInt()).toLong()
|
||||||
|
assertEquals(accountId, BotAccount("Test", "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", 0).id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deserializerTest() {
|
||||||
|
val accountId = abs(Random().nextInt()).toLong()
|
||||||
|
val creatorId = abs(Random().nextInt()).toLong()
|
||||||
|
val botAccountJsonObject = Gson().fromJson(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"name": "TestBot",
|
||||||
|
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
|
||||||
|
"creatorId": $creatorId
|
||||||
|
}
|
||||||
|
""".trimIndent(), JsonObject::class.java
|
||||||
|
)
|
||||||
|
val botAccount = Gson().fromJson(botAccountJsonObject, BotAccount::class.java)
|
||||||
|
assertEquals(botAccountJsonObject["name"].asString, botAccount.name)
|
||||||
|
assertEquals(botAccountJsonObject["token"].asString, botAccount.token)
|
||||||
|
assertEquals(accountId, botAccount.id, "BotAccount ID does not match expectations.")
|
||||||
|
assertEquals(creatorId, botAccount.creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
@file:Suppress("PackageDirectoryMismatch")
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
package net.lamgc.scalabot.util
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParseException
|
import com.google.gson.JsonParseException
|
@ -1,19 +1,16 @@
|
|||||||
package util
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import net.lamgc.scalabot.MavenRepositoryConfig
|
import net.lamgc.scalabot.config.MavenRepositoryConfig
|
||||||
import net.lamgc.scalabot.util.AuthenticationSerializer
|
import net.lamgc.scalabot.config.ProxyType
|
||||||
import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer
|
|
||||||
import net.lamgc.scalabot.util.ProxyTypeSerializer
|
|
||||||
import org.eclipse.aether.repository.Authentication
|
import org.eclipse.aether.repository.Authentication
|
||||||
import org.eclipse.aether.repository.AuthenticationContext
|
import org.eclipse.aether.repository.AuthenticationContext
|
||||||
import org.eclipse.aether.repository.Proxy
|
import org.eclipse.aether.repository.Proxy
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.telegram.telegrambots.bots.DefaultBotOptions
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
@ -26,7 +23,7 @@ internal class SerializersKtTest {
|
|||||||
private val method: Method
|
private val method: Method
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val clazz = Class.forName("net.lamgc.scalabot.util.SerializerUtils")
|
val clazz = Class.forName("net.lamgc.scalabot.config.serializer.SerializerUtils")
|
||||||
method = clazz.getDeclaredMethod("checkJsonKey", JsonObject::class.java, String::class.java)
|
method = clazz.getDeclaredMethod("checkJsonKey", JsonObject::class.java, String::class.java)
|
||||||
method.isAccessible = true
|
method.isAccessible = true
|
||||||
instance = clazz.getDeclaredField("INSTANCE").apply {
|
instance = clazz.getDeclaredField("INSTANCE").apply {
|
||||||
@ -69,7 +66,7 @@ internal class ProxyTypeSerializerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `serialize test`() {
|
fun `serialize test`() {
|
||||||
for (type in DefaultBotOptions.ProxyType.values()) {
|
for (type in ProxyType.values()) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JsonPrimitive(type.name), ProxyTypeSerializer.serialize(type, null, null),
|
JsonPrimitive(type.name), ProxyTypeSerializer.serialize(type, null, null),
|
||||||
"ProxyType 序列化结果与预期不符."
|
"ProxyType 序列化结果与预期不符."
|
||||||
@ -91,11 +88,11 @@ internal class ProxyTypeSerializerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DefaultBotOptions.ProxyType.NO_PROXY,
|
ProxyType.NO_PROXY,
|
||||||
ProxyTypeSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
ProxyTypeSerializer.deserialize(JsonNull.INSTANCE, null, null)
|
||||||
)
|
)
|
||||||
|
|
||||||
for (type in DefaultBotOptions.ProxyType.values()) {
|
for (type in ProxyType.values()) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
type, ProxyTypeSerializer.deserialize(JsonPrimitive(type.name), null, null),
|
type, ProxyTypeSerializer.deserialize(JsonPrimitive(type.name), null, null),
|
||||||
"ProxyType 反序列化结果与预期不符."
|
"ProxyType 反序列化结果与预期不符."
|
@ -1,6 +1,7 @@
|
|||||||
package net.lamgc.scalabot.util
|
package net.lamgc.scalabot.config.serializer
|
||||||
|
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
|
import net.lamgc.scalabot.config.UsernameAuthenticator
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
@ -4,3 +4,4 @@ rootProject.name = "scalabot"
|
|||||||
include(":scalabot-app")
|
include(":scalabot-app")
|
||||||
include(":scalabot-extension")
|
include(":scalabot-extension")
|
||||||
include("scalabot-ext-example")
|
include("scalabot-ext-example")
|
||||||
|
include("scalabot-meta")
|
||||||
|
Loading…
Reference in New Issue
Block a user