11 Commits

Author SHA1 Message Date
2a08f28838 refactor(config): 使用 URL 生成缺省仓库 Id.
在使用过程中, 发现 Aether 会根据仓库 Id 保存一些元信息, 所以尝试以 URL 生成仓库 Id, 或许有利于 Aether 存取仓库元信息.
改动仍需观望.
2022-08-18 00:59:51 +08:00
580d9122e5 refactor(utils): 把日志记录器的获取方式改的优雅一点(对于测试来说).
由于 Kover 一直不把无代码高阶函数忽略掉, 所以稍微改一下这个日志记录器的获取方法, 让测试覆盖数据好看点(doge).
2022-08-17 23:01:06 +08:00
2a607f1129 fix(extension): 修复 ExtensionClassLoader 无法过滤非扩展包内 BotExtensionFactory 对象的问题.
按照 ServiceLoader 的规范, 文件应该是在 META-INF/services/{class} 这里的, 但当时忘记这个设计了, 导致直接判断 name == class, 然后失效.
修复好了.
2022-08-15 02:00:34 +08:00
2d6da7c1ae build(test): 添加 Jacoco 插件.
给 Extension 模块添加 Jacoco 插件, 以后估计会开 Codecov 来统计测试覆盖率.
2022-08-15 01:45:22 +08:00
6235c5f51a build(dependencies): 更新 Kotlin 版本(1.6.10 -> 1.7.10). 2022-08-15 01:39:08 +08:00
255a02c93c refactor(config): 重构 AppPaths 的构造方法, 应对将来 Kotlin 更新中的特性.
先前的方法是利用了初始化与调用的顺序, 来实现的 Supplier 互补(虽然在代码中, 确实存在未初始化调用的情况, 但实际运行的时候, 会先初始化, 再调用 Supplier),
但是未来 Kotlin 的更新中,编译器会把这个操作视为未初始化错误, 所以在这次改动中修复掉这个 bug 操作.
2022-08-15 01:38:08 +08:00
dce28be9c7 fix(logging): 修复日志滚动文件路径错误.
由于在滚动格式中没有使用 DATA_LOGS 占位符, 导致在日志滚动时会把日志归档文件保存在运行目录下, 而不是指定的数据目录,
该改动已修复该问题.
2022-08-13 13:19:09 +08:00
673c6d8392 build: 为项目支持可重现构建.
为确保项目的使用者(无论是开发者, 还是最终用户)可以完全重现构建, 确保安全, 故调整相关配置, 以实现"可重现构建".
有关可重现构建, 可以看这个: https://reproducible-builds.org/
2022-08-06 03:23:35 +08:00
d586ca378e fix(launch): 修复缺省的 Maven 中央库不遵循代理规则的问题.
这个属于漏网之鱼, 已修复.
2022-08-05 00:23:13 +08:00
3ba4364a07 test(config): 补全对 ProxyConfig 的单元测试.
先前加了个 ProxyConfig.toString 方法, 所以补充了一下测试项.
2022-08-04 19:13:45 +08:00
eda0e522cd docs(config): 补充关于 Gson 类型适配器的使用指导.
为 AppConfig 和 BotConfig 补充关于 Gson 类型适配器使用的信息, 以便于开发者正确使用类型适配器解析和编码 Json.
2022-07-22 21:59:14 +08:00
12 changed files with 106 additions and 21 deletions

View File

@ -1,5 +1,5 @@
plugins { plugins {
kotlin("jvm") version "1.6.10" apply false kotlin("jvm") version "1.7.10" apply false
id("org.jetbrains.kotlinx.kover") version "0.5.1" apply false id("org.jetbrains.kotlinx.kover") version "0.5.1" apply false
} }

View File

@ -56,3 +56,8 @@ application {
tasks.jar.configure { tasks.jar.configure {
exclude("**/logback-test.xml") exclude("**/logback-test.xml")
} }
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}

View File

@ -1,6 +1,7 @@
package net.lamgc.scalabot package net.lamgc.scalabot
import ch.qos.logback.core.PropertyDefinerBase import ch.qos.logback.core.PropertyDefinerBase
import com.google.common.net.InternetDomainName
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonArray import com.google.gson.JsonArray
@ -15,9 +16,11 @@ import org.eclipse.aether.repository.RepositoryPolicy
import org.telegram.telegrambots.bots.DefaultBotOptions import org.telegram.telegrambots.bots.DefaultBotOptions
import java.io.File import java.io.File
import java.net.URL import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean 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 { } private val log = KotlinLogging.logger { }
@ -42,7 +45,7 @@ internal fun ProxyConfig.toAetherProxy(): Proxy? {
internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig? = null): RemoteRepository { internal fun MavenRepositoryConfig.toRemoteRepository(proxyConfig: ProxyConfig? = null): RemoteRepository {
val repositoryId = if (id == null) { val repositoryId = if (id == null) {
val generatedRepoId = createDefaultRepositoryId() val generatedRepoId = createDefaultRepositoryId(url)
log.debug { "仓库 Url `$url` 未设置仓库 Id, 已分配缺省 Id: $generatedRepoId" } log.debug { "仓库 Url `$url` 未设置仓库 Id, 已分配缺省 Id: $generatedRepoId" }
generatedRepoId generatedRepoId
} else { } else {
@ -90,10 +93,11 @@ private fun checkRepositoryLayout(layoutType: String): String {
return type return type
} }
private val repoNumberGenerator = AtomicInteger(1) private fun createDefaultRepositoryId(url: URL): String {
val topPrivateDomain = InternetDomainName.from(url.host).topPrivateDomain().toString()
private fun createDefaultRepositoryId(): String { return "Repository-${URLEncoder.encode(topPrivateDomain, StandardCharsets.UTF_8)}-${
return "Repository-${repoNumberGenerator.getAndIncrement()}" url.toString().hashCode().toString(16)
}"
} }
/** /**
@ -102,9 +106,9 @@ private fun createDefaultRepositoryId(): String {
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径. * 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
*/ */
internal enum class AppPaths( internal enum class AppPaths(
private val pathSupplier: () -> String = { fileSupplier.invoke().canonicalPath }, private val pathSupplier: PathSupplier,
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer, private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
private val fileSupplier: () -> File = { File(pathSupplier()) } private val fileSupplier: FileSupplier,
) { ) {
/** /**
* 数据根目录. * 数据根目录.
@ -113,7 +117,7 @@ internal enum class AppPaths(
* *
* 提示: 结尾不带 `/`. * 提示: 结尾不带 `/`.
*/ */
DATA_ROOT(fileSupplier = { DATA_ROOT(fileSupplier = FileSupplier {
File( File(
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH) System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
?: System.getProperty("user.dir") ?: "." ?: System.getProperty("user.dir") ?: "."
@ -125,7 +129,7 @@ internal enum class AppPaths(
} }
}), }),
CONFIG_APPLICATION({ "$DATA_ROOT/config.json" }, { CONFIG_APPLICATION(PathSupplier { "$DATA_ROOT/config.json" }, {
if (!file.exists()) { if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use { file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.appConfigGson.toJson( GsonConst.appConfigGson.toJson(
@ -141,7 +145,7 @@ internal enum class AppPaths(
} }
} }
}), }),
CONFIG_BOT({ "$DATA_ROOT/bot.json" }, { CONFIG_BOT(PathSupplier { "$DATA_ROOT/bot.json" }, {
if (!file.exists()) { if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use { file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.botConfigGson.toJson( GsonConst.botConfigGson.toJson(
@ -167,10 +171,25 @@ internal enum class AppPaths(
TEMP({ "$DATA_ROOT/tmp/" }) TEMP({ "$DATA_ROOT/tmp/" })
; ;
val file: File constructor(pathSupplier: PathSupplier, initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer) : this(
get() = fileSupplier.invoke() fileSupplier = FileSupplier { File(pathSupplier.path).canonicalFile },
val path: String pathSupplier = pathSupplier,
get() = pathSupplier.invoke() 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) private val initialized = AtomicBoolean(false)
@ -206,6 +225,20 @@ internal enum class AppPaths(
const val ENV_DATA_PATH = "BOT_DATA_PATH" 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()
}
} }
/** /**

View File

@ -182,7 +182,7 @@ internal class Launcher(private val config: AppConfig = Const.config) : AutoClos
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/') || it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
}) { }) {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy())) add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = proxyConfig.toAetherProxy()))
} }
}.toList() }.toList()
val extensionPackageFinders = setOf( val extensionPackageFinders = setOf(

View File

@ -413,14 +413,14 @@ internal class ExtensionClassLoader(urls: Array<URL>, dependencyLoader: ClassLoa
// 以免使用了不来自扩展包的机器人扩展. // 以免使用了不来自扩展包的机器人扩展.
override fun getResources(name: String?): Enumeration<URL> { override fun getResources(name: String?): Enumeration<URL> {
if (BotExtensionFactory::class.java.equals(name)) { if ("META-INF/services/${BotExtensionFactory::class.java.name}" == name) {
return findResources(name) return findResources(name)
} }
return super.getResources(name) return super.getResources(name)
} }
override fun getResource(name: String?): URL? { override fun getResource(name: String?): URL? {
if (BotExtensionFactory::class.java.equals(name)) { if ("META-INF/services/${BotExtensionFactory::class.java}" == name) {
return findResource(name) return findResource(name)
} }
return super.getResource(name) return super.getResource(name)

View File

@ -74,7 +74,7 @@ fun <T : AutoCloseable> T.registerShutdownHook(): T {
private object UtilsInternal { private object UtilsInternal {
val autoCloseableSet = mutableSetOf<AutoCloseable>() val autoCloseableSet = mutableSetOf<AutoCloseable>()
private val log = KotlinLogging.logger { } private val log = KotlinLogging.logger(UtilsInternal::class.java.name)
init { init {
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable")) Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))

View File

@ -24,7 +24,7 @@
<appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${DATA_LOGS}/latest.log</file> <file>${DATA_LOGS}/latest.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>data/logs/%d{yyyy-MM-dd}.log.gz</fileNamePattern> <fileNamePattern>${DATA_LOGS}/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>30</maxHistory>
</rollingPolicy> </rollingPolicy>
<encoder> <encoder>

View File

@ -21,6 +21,17 @@ import kotlin.test.*
internal class AppPathsTest { internal class AppPathsTest {
@Test
fun `Consistency check`() {
for (path in AppPaths.values()) {
assertEquals(
File(path.path).canonicalPath,
path.file.canonicalPath,
"路径 File 与 Path 不一致: ${path.name}"
)
}
}
@Test @Test
fun `Data root path priority`() { fun `Data root path priority`() {
System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, "fromSystemProperties") System.setProperty(AppPaths.PathConst.PROP_DATA_PATH, "fromSystemProperties")

View File

@ -1,6 +1,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
java java
jacoco
`maven-publish` `maven-publish`
signing signing
} }
@ -29,6 +30,16 @@ java {
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
}
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
} }
publishing { publishing {

View File

@ -36,6 +36,11 @@ java {
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
tasks.getByName<Test>("test") { tasks.getByName<Test>("test") {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@ -28,6 +28,13 @@ data class BotAccount(
/** /**
* 机器人配置. * 机器人配置.
*
* 使用 Gson 解析时, 请添加以下类型适配器:
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
* - [net.lamgc.scalabot.config.serializer.BotConfigSerializer]
* - [net.lamgc.scalabot.config.serializer.BotAccountSerializer]
* - [net.lamgc.scalabot.config.serializer.ArtifactSerializer]
*
* @property enabled 是否启用机器人. * @property enabled 是否启用机器人.
* @property account 机器人帐号信息, 用于访问 API. * @property account 机器人帐号信息, 用于访问 API.
* @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令. * @property disableBuiltInAbility 是否禁用 AbilityBot 自带命令.
@ -126,6 +133,13 @@ data class MavenRepositoryConfig(
* ScalaBot App 配置. * ScalaBot App 配置.
* *
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中. * App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
*
* 使用 Gson 解析时, 请添加以下类型适配器:
* - [net.lamgc.scalabot.config.serializer.ProxyTypeSerializer]
* - [net.lamgc.scalabot.config.serializer.MavenRepositoryConfigSerializer]
* - [net.lamgc.scalabot.config.serializer.AuthenticationSerializer]
* - [net.lamgc.scalabot.config.serializer.UsernameAuthenticatorSerializer]
*
* @property proxy Telegram API 代理配置. * @property proxy Telegram API 代理配置.
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据. * @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
* @property mavenRepositories Maven 远端仓库配置. * @property mavenRepositories Maven 远端仓库配置.

View File

@ -219,6 +219,12 @@ internal class ProxyConfigTest {
assertEquals(8080, actualConfig.port) assertEquals(8080, actualConfig.port)
assertEquals(ProxyType.HTTP, actualConfig.type) assertEquals(ProxyType.HTTP, actualConfig.type)
} }
@Test
fun `toString test`() {
assertEquals("NO_PROXY", ProxyConfig(ProxyType.NO_PROXY).toString())
assertEquals("HTTP://example.org:1008", ProxyConfig(ProxyType.HTTP, "example.org", 1008).toString())
}
} }
internal class MetricsConfigTest { internal class MetricsConfigTest {