46 Commits

Author SHA1 Message Date
efbb57f1f7 release: 发布 0.3.0 版本. 2022-05-18 15:58:22 +08:00
5e18149640 feat(config): 支持限定 Maven 仓库构件类型.
增加 Maven 仓库配置, 支持限定仓库可获取的构件发布类型(发布或快照).
此改动有利于用户增加仓库约束, 防止意外使用错误的扩展包版本.
2022-05-18 15:57:49 +08:00
0a5313e94a fix(extension): 修复 Maven 仓库扩展搜索器无法从第三方仓库获取扩展的问题.
由于在加载仓库配置时, 未设置仓库 Id, 导致 Aether 将仓库排除, 进而导致无法通过第三方仓库获取插件.
改动后, 将在未配置仓库 Id 的情况下, 为其生成一个 Id 名称.
2022-05-18 15:40:33 +08:00
a0afde52ac fix(launch): 修复 Maven 本地仓库文件夹未初始化的问题.
由于 Maven 本地仓库文件夹未初始化, 将导致启动时发生错误, 现已修复该问题.
2022-05-17 19:59:04 +08:00
ef37f3b2d7 fix(bot): 修复因机器人命令列表为空而导致命令列表自动更新报错的问题.
当机器人因扩展加载失败时, 将无法正常执行命令更新操作, 故添加空命令检查以避免该问题.
2022-05-17 19:56:42 +08:00
6e59a9a5ac build(publish): 增加构件签名过程.
增加构件 GPG 签名, 可保证构件未被修改, 增加构件可信度.
2022-05-17 19:26:26 +08:00
a44732a7f6 build: 将 Maven 发布仓库从 Github Repo 改为自建 Nexus 仓库.
由于 Github 自建仓库在 SNAPSHOT 版本上存在问题, 故修改发布配置以转移到自建的 Nexus 仓库.
2022-05-17 19:03:58 +08:00
95ad251826 refactor(utils): 移除不再使用的方法.
当初开发 Maven 仓库搜索器时意外提交的, 已经可以移除了.
2022-05-07 01:43:15 +08:00
8174f2a3a2 refactor(bot): 修正提示信息错误, 移除空父类方法调用.
修正了运行指标信息中的拼写错误, 移除对无操作父类方法的调用, 这么做可以明确表明只有子类实现了操作.
2022-05-07 01:37:21 +08:00
478480014a perf(utils): 优化自动释放钩子的资源引用.
原本自动释放钩子对资源的引用, 可能会出现资源已经被关闭, 但仍然无法被 GC 回收的问题.
此次改动, 将会让钩子在关闭资源后, 将资源从列表中移除.
虽然, 自动释放钩子设计上仅会被 System.exit 动作触发, 但保险起见还是加上这个改动.
2022-05-05 16:52:29 +08:00
830f05c90a refactor(utils): 加强 getPriority 方法的优先值判断.
加强优先级判断, 有利于后续使用时防止出现意外情况的问题.
顺便补充一手单元测试.
2022-05-05 16:13:48 +08:00
8be0978783 refactor: 更改 AppConfig 的获取方式, 以便于编写测试用例.
通过 Const 单例对象获取配置信息不利于编写测试用例, 所以改为利用参数的默认值来获取 Const 的 config 对象.

Issue #5
2022-05-04 23:55:21 +08:00
ce613787f6 fix: 修正方法参数使用错误的问题.
MavenRepositoryConfig 的 toRemoteRepository 方法使用了参数默认值, 可能会导致意外使用常量的情况,
故移除 MavenRepositoryConfig.toRemoteRepository 的参数默认值.

Pull Request #6
2022-05-04 23:07:45 +08:00
2389d082f4 test(config): 优化对 defaultInitializer 方法的单元测试.
将 defaultInitializer 方法的反射获取次数减少为一次, 并在测试结束后恢复访问权设置.
2022-05-04 22:36:35 +08:00
27f54c3c36 test(config): 补充一部分 AppPaths 的单元测试项目.
补充了针对 AppPaths.defaultInitializer() 和 AppPaths.DATA_ROOT 的单元测试项.
其他的有待补充.
2022-05-04 02:00:01 +08:00
7b985ce325 refactor: 将十六进制转换代码迁移到 Kotlin.
将 ByteUtils 的实现改用 Kotlin 代码做, 移除 ByteUtils.
另外, 本次修改同时修正了方法名错误的问题(hax 改成 hex), 并补充了单元测试.
2022-05-04 00:38:30 +08:00
77b7a7cd08 feat(launch): 对配置中没有启用任何机器人的情况输出警告.
增加对没有启用任何机器人时候的一个警告信息, 以防止被误认为无响应退出.
2022-05-02 02:20:19 +08:00
e8b746b3f8 feat(config): 第一次运行将提醒用户更改配置文件.
之前忘记添加这个提醒了, 首次运行的时候, `config.json` 和 `bot.json` 是不存在的, 所以根据这两个文件的存在与否, 来判定并提醒用户更改配置文件.
2022-05-02 02:18:23 +08:00
d24572a4f3 refactor(config): 修改 AppConfig 的获取方式, 便于编写测试用例.
通过 Const 单例对象获取配置信息不利于编写测试用例, 所以改为利用参数的默认值来获取 Const 的 config 对象.

Issue #5
2022-05-01 23:54:22 +08:00
f11290c73d feat: 可以覆盖 Maven 中央仓库配置.
原本设计是无论配置文件中是否带有 Maven 中央仓库, 都会添加 Maven 中央仓库进去, 这样可能会覆盖用户的仓库配置.
新改动将检查配置中是否添加了 Maven 中央仓库配置来决定是否补充 Maven 中央仓库.
2022-05-01 23:09:44 +08:00
1f2ab0f9b1 fix(extension): 修复搜索器错误日志不包括异常信息.
意外漏掉了这个错误信息, 目前已补充, 以方便寻找问题.
2022-05-01 00:09:28 +08:00
d14ef9de36 fix: 修复Maven 本地仓库文件夹未初始化的问题.
当本地仓库文件夹未初始化时, 将导致文件写入失败, 已修复该问题.
2022-04-30 21:12:36 +08:00
3e51327ed7 release: 紧急更新 0.2.1 版本. 2022-04-22 18:16:33 +08:00
93cf5c4e2f fix: 修复因文件访问比初始化早而导致的启动错误问题.
由于 Launcher 比 initialFiles 更早的执行, 调用 Const 导致更早的访问了未初始化的文件, 导致第一次启动会出现错误.
改动中将 Launcher 移入 main 方法不会有影响, 相比于在 AppPaths.<init> 中初始化会更安全.
2022-04-22 18:15:30 +08:00
e12f858690 release: 发布 0.2.0 版本. 2022-04-20 17:37:03 +08:00
3e99d7d033 fix(example): 修复 Reply 判断条件不充分的问题.
由于条件判断不充分, 导致运行时可能会出现 NPE 的问题.
2022-04-20 16:35:02 +08:00
8131f41313 feat(extension): 初步添加一些工具方法.
添加 AbilityBots 类, 向开发者提供一些"工具"方法, 该方法将有助于插件的功能开发.
2022-04-19 00:17:57 +08:00
270e744bcf build: 更新并增加 Kotlin 相关依赖项.
由于不明确的 Kotlin 运行时机制, 需直接向框架本身添加 Kotlin 标准库依赖, 以解决潜在的 Kotlin 反射支持问题.
不可能因运行时问题就继续向框架添加不相关的依赖, 所以该问题将继续调查.
2022-04-19 00:08:24 +08:00
d84465ebd9 build: 更新 TelegramBots 依赖项.
更新该依赖项以支持新版本的 Telegram API.
请注意: TelegramBots 已将弃用类移除.
2022-04-18 23:56:30 +08:00
18bc3a8d48 refactor(logging): 补充两个日志屏蔽.
屏蔽两个日志量较大的类, 日志量过多会影响 Debug.
2022-04-18 23:54:29 +08:00
2b88134207 refactor(extension): 移除 ScalaBotExtension 类.
该类的作用不大, 实现的细节可以由开发者自行实现(或许会设计地更好), 故移除该类.

DEPRECATED: 移除 ScalaBotExtension 类

由于该抽象类设计过于简单, 且并未达到其存在的预期目的(简化开发扩展的复杂性), 故移除本类.
开发者应将扩展类由继承 ScalaBotExtension 更改为实现 AbilityExtension 类, `getSender(): MessageSender` 和 `getDBContext():DBContext` 方法以及 `bot`(BaseAbilityBot) 字段应手动添加, 并根据需要通过构造方法获取并存储.
2022-04-15 00:18:59 +08:00
142eddfa28 refactor(extension): Maven 本地仓库路径将会相对于 DATA_ROOT 路径.
为防止混淆配置意义, 方便数据转移, 将 Maven 本地仓库路径调整为相对 DATA_ROOT 目录, 绝对路径不受影响.
2022-04-14 22:38:51 +08:00
64849adfab fix(config): 修复潜在的未初始化异常.
原设计中, PathConst 类会晚于 AppPaths 类中各枚举值的初始化, 进而导致 DATA_ROOT 获取 PathConst 中字段时出现未初始化异常的情况.
由于 AppPaths 设计为在运行时生成并获取路径, 所以该文件不会对 AppPaths 造成太大影响, 但 Kotlin 已决定在未来版本明确该问题为错误(Error), 所以将 PathConst 由伴生对象修改为单例对象以避免该问题.
2022-04-14 21:46:01 +08:00
29bd12a8dd feat(bot): 增加 accountId 属性.
增加该属性有利于其他组件对机器人账户进行标记.
2022-04-10 16:28:20 +08:00
9cdf10ccc2 refactor(launch): 增强关闭阶段的鲁棒性.
增加异常捕获, 防止由于部分 bot 发生异常而无法关闭其余机器人.
2022-04-10 16:27:20 +08:00
4210efef3b refactor(config): 改进了数据目录的获取方式.
补充了通过 user.dir 获取目录路径的方式.
2022-04-10 16:26:13 +08:00
bc0f3be32c docs(extension): 补充参数说明.
补充 shareDataFolder 参数说明, 便于扩展开发者根据需要调整数据存取.
2022-03-29 10:06:25 +08:00
c5f28e395e refactor(util): 优化代码.
使用 Kotlin 语法糖优化不必要的代码.
2022-03-28 23:43:33 +08:00
1172caa8d7 build(extension): 更新 TelegramBots 依赖项版本.
将依赖版本更新, 无兼容性问题.
2022-03-28 23:05:10 +08:00
eb95436404 fix(extension): 修复创建扩展对象时可能会出现的 NPE 问题.
创建对象后没有检查 NPE 问题, 修复该问题后有助于接受 Factory 不提供扩展的情况(现在这个情况是允许的了).
2022-03-28 21:38:40 +08:00
804d0e3012 build(extension): 为编译指定 Java 目标版本.
指定 Java 目标版本, 以免因环境错误编译成其他的 Java 字节码版本.
2022-03-25 18:15:00 +08:00
1281dbcabe feat(config): 支持指定本地仓库的路径.
可指定本地仓库的路径, 用于代替用户目录下的默认 Maven 本地仓库, 这么做可以在任意位置设置共享本地仓库.
2022-03-21 23:47:59 +08:00
1cd26b3b25 build: 更新依赖项版本. 2022-03-21 22:34:30 +08:00
19162dcaef feat(database): 更改数据库命名方式.
由于 BotToken 可以更换, 所以旧版命名方式将会有迁移的问题, 故更改为通过 Bot AccountId 来命名数据库.

CLOSED #3
2022-03-20 11:56:38 +08:00
0748afaff5 refactor(config): 为 BotAccount 添加 id 字段.
根据 Telegram Bot token 的组成结构, 可以取出 Bot Account Id, 故添加 id 字段.
2022-03-20 11:37:24 +08:00
b20b25bc7b docs(extension): 补充说明 shareDataFolder.
原来的说明容易产生误解, 让扩展开发者以为一个扩展一个数据目录, 所以在这块上补充了一些说明.
2022-03-20 11:04:07 +08:00
20 changed files with 828 additions and 164 deletions

View File

@ -7,5 +7,5 @@ allprojects {
}
group = "net.lamgc"
version = "0.1.0"
version = "0.3.0"
}

View File

@ -11,7 +11,7 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.7.36")
implementation("io.github.microutils:kotlin-logging:2.1.21")
implementation("ch.qos.logback:logback-classic:1.2.10")
implementation("ch.qos.logback:logback-classic:1.2.11")
val aetherVersion = "1.1.0"
implementation("org.eclipse.aether:aether-api:$aetherVersion")
@ -22,18 +22,20 @@ dependencies {
implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion")
implementation("org.apache.maven:maven-aether-provider:3.3.9")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.20")
implementation("com.google.code.gson:gson:2.9.0")
implementation("org.jdom:jdom2:2.0.6.1")
implementation("org.telegram:telegrambots-abilities:5.6.0")
implementation("org.telegram:telegrambots:5.6.0")
implementation("org.telegram:telegrambots-abilities:6.0.1")
implementation("org.telegram:telegrambots:6.0.1")
implementation("io.prometheus:simpleclient:0.15.0")
implementation("io.prometheus:simpleclient_httpserver:0.15.0")
testImplementation(kotlin("test"))
testImplementation("io.mockk:mockk:1.12.3")
}
tasks.test {

View File

@ -1,17 +0,0 @@
package net.lamgc.scalabot.util;
final class ByteUtils {
private ByteUtils() {
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte aByte : bytes) {
String hexBit = Integer.toHexString(aByte & 0xFF);
builder.append(hexBit.length() == 1 ? "0" + hexBit : hexBit);
}
return builder.toString();
}
}

View File

@ -13,12 +13,15 @@ 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
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.exitProcess
private val log = KotlinLogging.logger { }
@ -32,7 +35,14 @@ 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()
}
/**
* 机器人配置.
@ -92,20 +102,40 @@ internal data class MetricsConfig(
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
*/
internal data class MavenRepositoryConfig(
val id: String? = null,
val url: URL,
val proxy: Proxy? = Proxy("http", "127.0.0.1", 1080),
val layout: String = "default",
val enableReleases: Boolean = true,
val enableSnapshots: Boolean = true,
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
val authentication: Authentication? = null
) {
fun toRemoteRepository(): RemoteRepository {
val builder = RemoteRepository.Builder(null, checkRepositoryLayout(layout), url.toString())
fun toRemoteRepository(proxyConfig: ProxyConfig): RemoteRepository {
val builder =
RemoteRepository.Builder(id ?: createDefaultRepositoryId(), checkRepositoryLayout(layout), url.toString())
if (proxy != null) {
builder.setProxy(proxy)
} else if (Const.config.proxy.type == DefaultBotOptions.ProxyType.HTTP) {
builder.setProxy(Const.config.proxy.toAetherProxy())
} 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(
enableSnapshots,
RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN
)
)
return builder.build()
}
@ -117,6 +147,13 @@ internal data class MavenRepositoryConfig(
}
return type
}
private val repoNumber = AtomicInteger(1)
fun createDefaultRepositoryId(): String {
return "Repository-${repoNumber.getAndIncrement()}"
}
}
}
@ -127,11 +164,13 @@ internal data class MavenRepositoryConfig(
* @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 mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
val mavenLocalRepository: String? = null
)
/**
@ -152,7 +191,10 @@ internal enum class AppPaths(
* 提示: 结尾不带 `/`.
*/
DATA_ROOT(fileSupplier = {
File(System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH) ?: ".")
File(
System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH)
?: System.getProperty("user.dir") ?: "."
)
}, initializer = {
val f = file
if (!f.exists()) {
@ -167,7 +209,8 @@ internal enum class AppPaths(
AppConfig(
mavenRepositories = listOf(
MavenRepositoryConfig(
URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
id = "central",
url = URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
)
)
), it
@ -220,9 +263,9 @@ internal enum class AppPaths(
return path
}
private companion object PathConst {
private const val PROP_DATA_PATH = "bot.path.data"
private const val ENV_DATA_PATH = "BOT_DATA_PATH"
private object PathConst {
const val PROP_DATA_PATH = "bot.path.data"
const val ENV_DATA_PATH = "BOT_DATA_PATH"
}
}
@ -256,9 +299,17 @@ private fun AppPaths.defaultInitializer() {
}
internal fun initialFiles() {
val configFilesNotInitialized = !AppPaths.DEFAULT_CONFIG_APPLICATION.file.exists()
&& !AppPaths.DEFAULT_CONFIG_BOT.file.exists()
for (path in AppPaths.values()) {
path.initial()
}
if (configFilesNotInitialized) {
log.warn { "配置文件已初始化, 请根据需要修改配置文件后重新启动本程序." }
exitProcess(1)
}
}
private object GsonConst {

View File

@ -4,25 +4,32 @@ import io.prometheus.client.exporter.HTTPServer
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import net.lamgc.scalabot.util.registerShutdownHook
import org.eclipse.aether.repository.LocalRepository
import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.TelegramBotsApi
import org.telegram.telegrambots.meta.generics.BotSession
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
import java.io.File
import java.io.IOException
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 { }
private val launcher = Launcher()
.registerShutdownHook()
fun main(args: Array<String>): Unit = runBlocking {
log.info { "ScalaBot 正在启动中..." }
log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
initialFiles()
if (Const.config.metrics.enable) {
val launcher = Launcher()
.registerShutdownHook()
startMetricsServer()
}
if (!launcher.launch()) {
exitProcess(1)
}
@ -32,11 +39,16 @@ fun main(args: Array<String>): Unit = runBlocking {
* 启动运行指标服务器.
* 使用 Prometheus 指标格式.
*/
fun startMetricsServer() {
internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) {
if (!config.enable) {
log.debug { "运行指标服务器已禁用." }
return
}
val builder = HTTPServer.Builder()
.withDaemonThreads(true)
.withPort(Const.config.metrics.port)
.withHostname(Const.config.metrics.bindAddress)
.withPort(config.port)
.withHostname(config.bindAddress)
val httpServer = builder
.build()
@ -44,7 +56,7 @@ fun startMetricsServer() {
log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" }
}
internal class Launcher : AutoCloseable {
internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable {
companion object {
@JvmStatic
@ -53,6 +65,46 @@ internal class Launcher : AutoCloseable {
private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
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 = AppPaths.DATA_ROOT.file.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 {
createDirectories(
PosixFilePermissions.asFileAttribute(
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_WRITE,
PosixFilePermission.OTHERS_READ,
)
)
)
}
}
}
.toRealPath()
.toFile()
repoPath
} else {
File("${System.getProperty("user.home")}/.m2/repository")
}
if (!localPath.exists()) {
localPath.mkdirs()
}
return LocalRepository(localPath)
}
@Synchronized
fun launch(): Boolean {
@ -60,6 +112,9 @@ internal class Launcher : AutoCloseable {
if (botConfigs.isEmpty()) {
log.warn { "尚未配置任何机器人, 请先配置机器人后再启动本程序." }
return false
} else if (botConfigs.none { it.enabled }) {
log.warn { "配置文件中没有已启用的机器人, 请至少启用一个机器人." }
return false
}
for (botConfig in botConfigs) {
try {
@ -81,15 +136,15 @@ internal class Launcher : AutoCloseable {
val proxyConfig =
if (botConfig.proxy != null && botConfig.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
botConfig.proxy
} else if (Const.config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
Const.config.proxy
} else if (config.proxy.type != DefaultBotOptions.ProxyType.NO_PROXY) {
config.proxy
} else {
null
}
if (proxyConfig != null) {
proxyType = proxyConfig.type
proxyHost = Const.config.proxy.host
proxyPort = Const.config.proxy.port
proxyHost = config.proxy.host
proxyPort = config.proxy.port
log.debug { "机器人 `${botConfig.account.name}` 已启用代理配置: $proxyConfig" }
}
@ -99,20 +154,26 @@ internal class Launcher : AutoCloseable {
}
val account = botConfig.account
val remoteRepositories = Const.config.mavenRepositories
.map(MavenRepositoryConfig::toRemoteRepository)
val remoteRepositories = config.mavenRepositories
.map { it.toRemoteRepository(config.proxy) }
.toMutableList().apply {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = Const.config.proxy.toAetherProxy()))
if (this.none {
it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL
|| it.url == MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL.trimEnd('/')
}) {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = config.proxy.toAetherProxy()))
}
}.toList()
val extensionPackageFinders = setOf(
MavenRepositoryExtensionFinder(
localRepository = mavenLocalRepository,
remoteRepositories = remoteRepositories,
proxy = Const.config.proxy.toAetherProxy()
proxy = config.proxy.toAetherProxy()
)
)
val bot = ScalaBot(
BotDBMaker.getBotMaker(account),
BotDBMaker.getBotDbInstance(account),
botOption,
extensionPackageFinders,
botConfig
@ -138,9 +199,16 @@ internal class Launcher : AutoCloseable {
@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}` 关闭时发生异常." }
}
}
}

View File

@ -1,23 +1,190 @@
package net.lamgc.scalabot
import net.lamgc.scalabot.util.toHaxString
import com.google.common.io.Files
import mu.KotlinLogging
import net.lamgc.scalabot.util.toHexString
import org.mapdb.DB
import org.mapdb.DBException
import org.mapdb.DBMaker
import org.telegram.abilitybots.api.db.DBContext
import org.telegram.abilitybots.api.db.MapDBContext
import java.io.File
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
/**
* 数据库适配器.
*
* 应按照新到旧的顺序放置, 新的适配器应该在上面.
*/
private val adapters = arrayListOf<DbAdapter>(
BotAccountIdDbAdapter, // since [v0.2.0 ~ latest)
BotTokenDbAdapter // since [v0.0.1 ~ v0.2.0)
)
private const val FIELD_DB_VERSION = "::DB_VERSION"
internal object BotDBMaker {
fun getBotMaker(botAccount: BotAccount): DBContext {
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
val dbPath = AppPaths.DATA_DB.path + "${digestBytes.toHaxString()}.db"
val db = DBMaker.fileDB(dbPath)
private val logger = KotlinLogging.logger { }
fun getBotDbInstance(botAccount: BotAccount): DBContext {
for (adapter in adapters) {
val botDb = try {
adapter.getBotDb(botAccount, create = false) ?: continue
} catch (e: Exception) {
logger.error(e) { "适配器 ${adapter::class.java} 打开数据库时发生异常." }
continue
}
if (!adapter.dbVersionMatches(botDb)) {
logger.warn {
"数据库版本号与适配器不符. " +
"(Adapter: ${adapter::class.java};(${adapter.dbVersion})," +
" DatabaseVer: ${adapter.getDbVersion(botDb)})"
}
botDb.close()
continue
} else {
if (adapter != adapters[0]) {
logger.debug {
"数据库适配器不是最新的, 正在升级数据库... " +
"(Old: ${adapter::class.java}; New: ${adapters[0]::class.java})"
}
val db = try {
botDb.close()
val newDb = adapters[0].migrateDb(botAccount, adapter)
logger.debug { "数据库版本升级完成." }
newDb
} catch (e: Exception) {
logger.warn(e) { "Bot 数据库版本升级失败, 将继续使用旧版数据库." }
adapter.getBotDb(botAccount, create = false) ?: continue
}
return MapDBContext(db)
}
return MapDBContext(botDb)
}
}
logger.debug { "没有适配器成功打开数据库, 使用最新的适配器创建数据库. (Adapter: ${adapters[0]::class.java})" }
val newDb = adapters[0].getBotDb(botAccount, create = true)
?: throw IllegalStateException("No adapter is available to get the database.")
adapters[0].setDbVersion(newDb, adapters[0].dbVersion)
return MapDBContext(newDb)
}
}
/**
* 数据库适配器.
*
* 用于解决数据库格式更新带来的问题, 通过迁移机制, 将数据库从旧版本迁移到新版本, 或者只通过旧版本适配器访问而不迁移.
* @param dbVersion 数据库格式版本. 格式为: `{格式标识}_{最后使用的版本号}`, 如果为最新版适配器, 则不需要填写最后使用的版本号.
*/
private abstract class DbAdapter(val dbVersion: String) {
/**
* 获取 Bot 专有的 [DBContext].
* @param botAccount Bot 账号信息.
*/
abstract fun getBotDb(botAccount: BotAccount, create: Boolean = false): DB?
/**
* 通过 Bot 账号信息获取数据库文件.
*/
abstract fun getBotDbFile(botAccount: BotAccount): File
/**
* 将旧版数据库迁移到当前版本.
*
* 实现时请注意不要直接修改原数据库, 以防升级过程出错导致无法回退到旧版本.
*/
abstract fun migrateDb(botAccount: BotAccount, oldDbAdapter: DbAdapter): DB
/**
* 数据库版本是否匹配.
*/
open fun dbVersionMatches(db: DB): Boolean {
return getDbVersion(db) == dbVersion
}
fun getDbVersion(db: DB): String? {
if (!db.exists(FIELD_DB_VERSION)) {
return null
}
val dbVersionField = try {
db.atomicString(FIELD_DB_VERSION).open()
} catch (e: DBException.WrongConfiguration) {
return null
}
return dbVersionField.get()
}
fun setDbVersion(db: DB, version: String) {
db.atomicString(FIELD_DB_VERSION).createOrOpen().set(version)
}
}
/**
* 抽象文件数据库适配器.
*
* 只有文件有变化的适配器.
*/
private abstract class FileDbAdapter(
dbVersion: String,
private val fileProvider: (BotAccount) -> File
) : DbAdapter(dbVersion) {
@Suppress("unused")
constructor(dbVersion: String) : this(dbVersion,
{ throw NotImplementedError("When using this constructor, the \"getBotDbFile\" method must be implemented") })
override fun getBotDb(botAccount: BotAccount, create: Boolean): DB? {
val dbFile = getBotDbFile(botAccount)
if (!dbFile.exists() && !create) {
return null
}
return DBMaker.fileDB(dbFile)
.closeOnJvmShutdownWeakReference()
.checksumStoreEnable()
.fileChannelEnable()
.make()
return MapDBContext(db)
}
override fun getBotDbFile(botAccount: BotAccount): File = fileProvider(botAccount)
override fun migrateDb(botAccount: BotAccount, oldDbAdapter: DbAdapter): DB {
val oldFile = oldDbAdapter.getBotDbFile(botAccount)
val newFile = getBotDbFile(botAccount)
try {
@Suppress("UnstableApiUsage")
Files.copy(oldFile, newFile)
} catch (e: Exception) {
if (newFile.exists()) {
// 删除新文件以防止异常退出后直接读取新文件.
newFile.delete()
}
throw e
}
oldFile.delete()
return getBotDb(botAccount)!!.apply {
setDbVersion(this, this@FileDbAdapter.dbVersion)
}
}
}
/**
* 使用 Bot Token 中的 Account Id 命名数据库文件名.
*/
private object BotAccountIdDbAdapter : FileDbAdapter("BotAccountId", { botAccount ->
File(AppPaths.DATA_DB.file, "${botAccount.id}.db")
})
/**
* 使用 Bot Token, 经过 Sha256 加密后得到文件名.
*
* **已弃用**: 由于 Token 可以重新生成, 当 Token 改变后数据库文件名也会改变, 故弃用该方法.
*/
private object BotTokenDbAdapter : FileDbAdapter("BotToken_v0.1.0", { botAccount ->
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8))
File(AppPaths.DATA_DB.file, "${digestBytes.toHexString()}.db")
})

View File

@ -90,6 +90,10 @@ internal class ExtensionLoader(
try {
val extension =
factory.createExtensionInstance(bot, getExtensionDataFolder(extensionArtifact))
if (extension == null) {
log.debug { "Factory ${factory::class.java} 创建插件时返回了 null, 已跳过. (BotName: ${bot.botUsername})" }
continue
}
factories.add(LoadedExtensionEntry(extensionArtifact, factory::class.java, extension))
} catch (e: Exception) {
log.error(e) { "创建扩展时发生异常. (ExtArtifact: `$extensionArtifact`, Factory: ${factory::class.java.name})" }
@ -128,7 +132,7 @@ internal class ExtensionLoader(
result[finder] = artifacts
}
} catch (e: Exception) {
log.error { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误:" }
log.error(e) { "搜索器 ${finder::class.java.name} 在搜索扩展 `$extensionArtifact` 时发生错误." }
}
}
return result

View File

@ -257,9 +257,21 @@ internal class MavenRepositoryExtensionFinder(
}
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
log.debug {
StringBuilder().apply {
append("构件 $extensionArtifact 将在以下仓库拉取: \n")
remoteRepositories.forEach {
append("\t- ${it}\n")
}
}
}
val extensionArtifactResult = repositorySystem.resolveArtifact(
repoSystemSession,
ArtifactRequest(extensionArtifact, remoteRepositories, null)
ArtifactRequest(
extensionArtifact,
repositorySystem.newResolutionRepositories(repoSystemSession, remoteRepositories),
null
)
)
val extResolvedArtifact = extensionArtifactResult.artifact
if (!extensionArtifactResult.isResolved) {

View File

@ -30,6 +30,7 @@ internal class ScalaBot(
extensionFinders: Set<ExtensionPackageFinder>,
botConfig: BotConfig,
private val creatorId: Long = botConfig.account.creatorId,
val accountId: Long = botConfig.account.id,
val extensions: Set<Artifact> = botConfig.extensions
) :
AbilityBot(
@ -102,6 +103,12 @@ internal class ScalaBot(
}
BotCommand(it.name(), abilityInfo)
}
if (botCommands.isEmpty()) {
log.info { "Bot 没有任何命令, 命令列表更新已跳过." }
return true
}
val setMyCommands = SetMyCommands()
setMyCommands.commands = botCommands
return execute(DeleteMyCommands()) && execute(setMyCommands)
@ -113,7 +120,6 @@ internal class ScalaBot(
}
override fun onClosing() {
super.onClosing()
onlineBotGauge.dec()
}
@ -150,7 +156,7 @@ internal class ScalaBot(
private val updateProcessTime = Summary.build()
.name("update_process_duration_seconds")
.help(
"Time to process update. (This indicator includes the pre-processing of update by TelegrammBots, " +
"Time to process update. (This indicator includes the pre-processing of update by TelegramBots, " +
"so it may be different from the actual execution time of ability. " +
"It is not recommended to use it as the accurate execution time of ability)"
)

View File

@ -142,12 +142,15 @@ internal object MavenRepositoryConfigSerializer
return when (json) {
is JsonObject -> {
MavenRepositoryConfig(
id = json.get("id")?.asString,
url = URL(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
@ -155,7 +158,7 @@ internal object MavenRepositoryConfigSerializer
)
}
is JsonPrimitive -> {
MavenRepositoryConfig(URL(json.asString))
MavenRepositoryConfig(url = URL(json.asString))
}
else -> {
throw JsonParseException("Unsupported Maven warehouse configuration type.")

View File

@ -7,9 +7,8 @@ import org.eclipse.aether.artifact.Artifact
import java.io.File
import java.io.FileFilter
import java.io.FilenameFilter
import java.net.URL
internal fun ByteArray.toHaxString(): String = ByteUtils.bytesToHexString(this)
internal fun ByteArray.toHexString(): String = joinToString("") { it.toString(16) }
internal fun Artifact.equalsArtifact(that: Artifact): Boolean =
this.groupId.equals(that.groupId) &&
@ -34,11 +33,7 @@ internal fun File.deepListFiles(
this.listFiles(filenameFilter)
} else {
this.listFiles()
}
if (files == null) {
return null
}
} ?: return null
val result = if (addSelf) mutableSetOf(this) else mutableSetOf()
for (file in files) {
@ -48,12 +43,10 @@ internal fun File.deepListFiles(
if (!onlyFile) {
result.add(file)
}
val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter)
if (subFiles != null) {
val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter) ?: continue
result.addAll(subFiles)
}
}
}
return result.toTypedArray()
}
@ -62,9 +55,14 @@ internal fun File.deepListFiles(
* @return 获取 Finder 的优先级.
* @throws NoSuchFieldException 如果 Finder 没有添加 [FinderRules] 注解时抛出该异常.
*/
internal fun ExtensionPackageFinder.getPriority() =
this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
internal fun ExtensionPackageFinder.getPriority(): Int {
val value = this::class.java.getDeclaredAnnotation(FinderRules::class.java)?.priority
?: throw NoSuchFieldException("Finder did not add `FinderRules` annotation")
if (value < 0) {
throw IllegalArgumentException("Priority cannot be lower than 0. (Class: ${this::class.java})")
}
return value
}
/**
* 为 [AutoCloseable] 对象注册 Jvm Shutdown 钩子.
@ -82,30 +80,19 @@ private object UtilsInternal {
val autoCloseableSet = mutableSetOf<AutoCloseable>()
init {
Runtime.getRuntime().addShutdownHook(Thread({
Runtime.getRuntime().addShutdownHook(Thread(this::doCloseResources, "Shutdown-AutoCloseable"))
}
fun doCloseResources() {
log.debug { "Closing registered hook resources..." }
autoCloseableSet.forEach {
autoCloseableSet.removeIf {
try {
it.close()
} catch (e: Exception) {
log.error(e) { "An exception occurred while closing the resource. (Resource: `$it`)" }
}
true
}
log.debug { "All registered hook resources have been closed." }
}, "Shutdown-AutoCloseable"))
}
}
fun URL.resolveToFile(canonical: Boolean = true): File {
if ("file" != protocol) {
throw ClassCastException("Only the URL of the `file` protocol can be converted into a File object.")
}
val urlString = toString().substringAfter(':')
val file = File(urlString)
return if (canonical) {
file.canonicalFile
} else {
file
}
}

View File

@ -5,6 +5,8 @@
<logger name="org.eclipse.aether.internal.impl.DefaultTransporterProvider" level="INFO"/>
<logger name="org.eclipse.aether.internal.impl.DefaultRepositoryConnectorProvider" level="INFO"/>
<logger name="org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager" level="INFO"/>
<logger name="org.telegram.telegrambots.facilities.proxysocketfactorys" level="INFO"/>
<logger name="org.eclipse.aether.internal.impl.DefaultUpdateCheckManager" level="INFO"/>
<root level="DEBUG">
<appender-ref ref="FILE_OUT"/>

View File

@ -0,0 +1,122 @@
package net.lamgc.scalabot
import com.google.gson.Gson
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.util.*
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
internal class BotAccountTest {
@Test
fun deserializerTest() {
val accountId = abs(Random().nextInt()).toLong()
val creatorId = abs(Random().nextInt()).toLong()
val botAccount = Gson().fromJson(
"""
{
"name": "TestBot",
"token": "${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10",
"creatorId": $creatorId
}
""".trimIndent(), BotAccount::class.java
)
assertEquals("TestBot", botAccount.name)
assertEquals("${accountId}:AAHErDroUTznQsOd_oZPJ6cQEj4Z5mGHO10", botAccount.token)
assertEquals(accountId, botAccount.id, "Botaccount ID does not match expectations.")
assertEquals(creatorId, botAccount.creatorId)
}
}
internal class AppPathsTest {
@Test
fun `Data root path priority`() {
System.setProperty("bot.path.data", "A")
assertEquals("A", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有优先返回 Property 的值.")
System.getProperties().remove("bot.path.data")
if (System.getenv("BOT_DATA_PATH") != null) {
assertEquals(
System.getenv("BOT_DATA_PATH"), AppPaths.DATA_ROOT.file.path,
"`DATA_ROOT`没有返回 env 的值."
)
} else {
assertEquals(
System.getProperty("user.dir"), AppPaths.DATA_ROOT.file.path,
"`DATA_ROOT`没有返回 `user.dir` 的值."
)
val userDir = System.getProperty("user.dir")
System.getProperties().remove("user.dir")
assertEquals(".", AppPaths.DATA_ROOT.file.path, "`DATA_ROOT`没有返回 `.`(当前目录).")
System.setProperty("user.dir", userDir)
assertNotNull(System.getProperty("user.dir"), "环境还原失败!")
}
}
@Test
fun `default initializer`(@TempDir testDir: File) {
val defaultInitializerMethod = Class.forName("net.lamgc.scalabot.AppConfigsKt")
.getDeclaredMethod("defaultInitializer", AppPaths::class.java)
.apply { isAccessible = true }
val dirPath = "${testDir.canonicalPath}/directory/"
val dirFile = File(dirPath)
mockk<AppPaths> {
every { file }.returns(File(dirPath))
every { path }.returns(dirPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
assertTrue(dirFile.exists() && dirFile.isDirectory, "默认初始器未正常初始化【文件夹】.")
File(testDir, "test.txt").apply {
mockk<AppPaths> {
every { file }.returns(this@apply)
every { path }.returns(this@apply.canonicalPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
assertTrue(this@apply.exists() && this@apply.isFile, "默认初始器未正常初始化【文件】.")
}
val alreadyExistsFile = File("${testDir.canonicalPath}/alreadyExists.txt").apply {
if (!exists()) {
createNewFile()
}
}
assertTrue(alreadyExistsFile.exists(), "文件状态与预期不符.")
mockk<File> {
every { exists() }.returns(true)
every { canonicalPath }.answers { alreadyExistsFile.canonicalPath }
every { createNewFile() }.answers { alreadyExistsFile.createNewFile() }
every { mkdirs() }.answers { alreadyExistsFile.mkdirs() }
every { mkdir() }.answers { alreadyExistsFile.mkdir() }
}.apply {
mockk<AppPaths> {
every { file }.returns(this@apply)
every { path }.returns(this@apply.canonicalPath)
every { initial() }.answers {
defaultInitializerMethod.invoke(null, this@mockk)
}
}.initial()
verify(exactly = 0) { createNewFile() }
verify(exactly = 0) { mkdir() }
verify(exactly = 0) { mkdirs() }
}
defaultInitializerMethod.isAccessible = false
}
}

View File

@ -1,9 +1,21 @@
package net.lamgc.scalabot.util
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.verify
import net.lamgc.scalabot.ExtensionPackageFinder
import net.lamgc.scalabot.FinderPriority
import net.lamgc.scalabot.FinderRules
import net.lamgc.scalabot.FoundExtensionPackage
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.nio.charset.StandardCharsets
import kotlin.test.*
internal class UtilsKtTest {
@ -16,4 +28,92 @@ internal class UtilsKtTest {
.equalsArtifact(DefaultArtifact("com.example:demo-2:1.0.0-SNAPSHOT"))
)
}
@Test
fun `bytes to hex`() {
assertEquals("48656c6c6f20576f726c64", "Hello World".toByteArray(StandardCharsets.UTF_8).toHexString())
}
@Test
fun `ExtensionPackageFinder - getPriority`() {
open class BaseTestFinder : ExtensionPackageFinder {
override fun findByArtifact(extensionArtifact: Artifact, extensionsPath: File): Set<FoundExtensionPackage> {
throw IllegalStateException("Calling this class is not allowed.")
}
}
@FinderRules(FinderPriority.ALTERNATE)
class StandardTestFinder : BaseTestFinder()
assertEquals(
FinderPriority.ALTERNATE, StandardTestFinder().getPriority(),
"获取到的优先值与预期不符"
)
@FinderRules(-1)
class OutOfRangePriorityFinder : BaseTestFinder()
assertThrows<IllegalArgumentException>("getPriority 方法没有对超出范围的优先值抛出异常.") {
OutOfRangePriorityFinder().getPriority()
}
class NoAnnotationFinder : BaseTestFinder()
assertThrows<NoSuchFieldException> {
NoAnnotationFinder().getPriority()
}
}
@Test
fun `AutoCloseable shutdown hook`() {
val utilsInternalClass = Class.forName("net.lamgc.scalabot.util.UtilsInternal")
val utilsInternalObject = utilsInternalClass.getDeclaredField("INSTANCE").get(null)
?: fail("无法获取 UtilsInternal 对象.")
val doCloseResourcesMethod = utilsInternalClass.getDeclaredMethod("doCloseResources")
.apply {
isAccessible = true
}
// 正常的运行过程.
val mockResource = mockk<AutoCloseable> {
justRun { close() }
}.registerShutdownHook()
doCloseResourcesMethod.invoke(utilsInternalObject)
verify { mockResource.close() }
// 异常捕获检查.
val exceptionMockResource = mockk<AutoCloseable> {
every { close() } throws RuntimeException("Expected exception.")
}.registerShutdownHook()
assertDoesNotThrow("在关闭资源时出现未捕获异常.") {
doCloseResourcesMethod.invoke(utilsInternalObject)
}
verify { exceptionMockResource.close() }
// 错误抛出检查.
val errorMockResource = mockk<AutoCloseable> {
every { close() } throws Error("Expected error.")
}.registerShutdownHook()
assertThrows<Error>("关闭资源时捕获了不该捕获的 Error.") {
try {
doCloseResourcesMethod.invoke(utilsInternalObject)
} catch (e: InvocationTargetException) {
throw e.targetException
}
}
verify { errorMockResource.close() }
@Suppress("UNCHECKED_CAST")
val resourceSet = utilsInternalClass.getDeclaredMethod("getAutoCloseableSet").invoke(utilsInternalObject)
as MutableSet<AutoCloseable>
resourceSet.clear()
val closeRef = mockk<AutoCloseable> {
justRun { close() }
}
resourceSet.add(closeRef)
assertTrue(resourceSet.contains(closeRef), "测试用资源虚引用添加失败.")
doCloseResourcesMethod.invoke(utilsInternalObject)
assertFalse(resourceSet.contains(closeRef), "资源虚引用未从列表中删除.")
resourceSet.clear()
}
}

View File

@ -37,11 +37,16 @@ public class SayHelloExtension implements AbilityExtension {
*/
public Ability test() {
ReplyFlow botHello = ReplyFlow.builder(bot.db())
.enableStats("say_hello")
.action((bot, upd) -> bot.silent().send("What is u name?", upd.getMessage().getChatId()))
.onlyIf(update -> "hello".equalsIgnoreCase(update.getMessage().getText()))
.onlyIf(update -> update.hasMessage()
&& update.getMessage().hasText()
&& "hello".equalsIgnoreCase(update.getMessage().getText()))
.next(Reply.of((bot, upd) -> bot.silent()
.send("OK! You name is " + upd.getMessage().getText().substring("my name is ".length()), upd.getMessage().getChatId()),
upd -> upd.getMessage().getText().startsWith("my name is ")))
upd -> upd.hasMessage()
&& upd.getMessage().hasText()
&& upd.getMessage().getText().startsWith("my name is ")))
.build();
return Ability.builder()

View File

@ -4,14 +4,16 @@ plugins {
kotlin("jvm") version "1.6.10"
java
`maven-publish`
signing
}
dependencies {
api("org.telegram:telegrambots-abilities:5.6.0")
api("org.telegram:telegrambots-abilities:6.0.1")
api("org.slf4j:slf4j-api:1.7.36")
// There is nothing to test.
// testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.mockito:mockito-core:4.4.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
tasks.withType<Javadoc> {
@ -23,6 +25,8 @@ tasks.withType<Javadoc> {
java {
withJavadocJar()
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks.test {
@ -35,24 +39,21 @@ tasks.withType<KotlinCompile> {
publishing {
repositories {
val repoRootKey = "maven.repo.local.root"
val snapshot = project.version.toString().endsWith("-SNAPSHOT")
val repoRoot = System.getProperty(repoRootKey)?.trim()
if (repoRoot == null || repoRoot.isEmpty()) {
logger.warn(
"\"$repoRootKey\" configuration item is not specified, " +
"please add start parameter \"-D$repoRootKey {localPublishRepo}\"" +
" (if you are not currently executing the publish task, " +
"you can ignore this information)"
)
return@repositories
if (project.version.toString().endsWith("-SNAPSHOT")) {
maven("https://repo.lamgc.moe/repository/maven-snapshots/") {
credentials {
username = project.properties["repo.credentials.private.username"].toString()
password = project.properties["repo.credentials.private.password"].toString()
}
}
val repoUri = if (snapshot) {
uri("$repoRoot/snapshots")
} else {
uri("$repoRoot/releases")
maven("https://repo.lamgc.moe/repository/maven-releases/") {
credentials {
username = project.properties["repo.credentials.private.username"].toString()
password = project.properties["repo.credentials.private.password"].toString()
}
}
}
maven(repoUri)
}
publications {
@ -94,3 +95,8 @@ publishing {
}
}
signing {
useGpgCmd()
sign(publishing.publications["maven"])
}

View File

@ -13,6 +13,7 @@ import java.io.File;
* 所以将通过该接口工厂来创建扩展对象.
*
* @author LamGC
* @since 0.0.1
*/
public interface BotExtensionFactory {
@ -21,11 +22,15 @@ public interface BotExtensionFactory {
*
* <p> 如扩展无使用 {@link org.telegram.abilitybots.api.db.DBContext} 的话,
* 也可以返回扩展单例, 因为 AbilityBot 本身并不禁止多个机器人共用一个扩展对象
* (因为 AbilityBot 只是调用了扩展中的方法来创建了功能对象).
* (AbilityBot 只是调用了扩展中的方法来创建 Ability 对象).
*
* @param bot 机器人对象.
* @param shareDataFolder ScalaBot App 为扩展提供的数据目录, 建议存储在数据目录中, 便于数据的存储管理.
* @return 返回为该 Bot 对象创建的扩展对象.
* @param shareDataFolder ScalaBot App 为扩展提供的共享数据目录.
* <p>路径格式为:
* <pre> $DATA_ROOT/data/extensions/{GroupId}/{ArtifactId}</pre>
* <b>同一个扩展包的 Factory</b> 接收到的共享数据目录<b>都是一样的</b>,
* 建议将数据存储在数据目录中, 便于数据的存储管理.
* @return 返回为该 Bot 对象创建的扩展对象, 如果不希望为该机器人提供扩展, 可返回 {@code null}.
*/
AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder);

View File

@ -1,32 +0,0 @@
package net.lamgc.scalabot.extension;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.abilitybots.api.db.DBContext;
import org.telegram.abilitybots.api.sender.MessageSender;
import org.telegram.abilitybots.api.util.AbilityExtension;
/**
*
*/
public abstract class ScalaBotExtension implements AbilityExtension {
/**
* 扩展所属的机器人对象.
*
* <p> 不要给该属性添加 Getter, 会被当成 Ability 添加, 导致出现异常.
*/
protected final BaseAbilityBot bot;
public ScalaBotExtension(BaseAbilityBot bot) {
this.bot = bot;
}
protected MessageSender getSender() {
return bot.sender();
}
protected DBContext getDBContext() {
return bot.db();
}
}

View File

@ -0,0 +1,52 @@
package net.lamgc.scalabot.extension.util;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AbilityBots {
private final static Pattern botTokenPattern = Pattern.compile("([1-9]\\d+):([A-Za-z\\d_-]{35,})");
private AbilityBots() {
}
/**
* 获取 AbilityBot 的账户 Id.
*
* <p> 账户 Id 来自于 botToken 中, token 的格式为 "[AccountId]:[Secret]".
* <p> 账户 Id 的真实性与 botToken 的有效性有关, 本方法并不会确保 botToken 的有效性, 一般情况下也无需考虑 Id 的有效性,
* 如果有需要, 可尝试通过调用 {@link org.telegram.telegrambots.meta.api.methods.GetMe} 来确保 botToken 的有效性.
*
* @param bot 要获取账户 Id 的 AbilityBot 对象.
* @return 返回 AbilityBot 的账户 Id.
* @throws IllegalArgumentException 当 AbilityBot 的 botToken 格式错误时抛出该异常.
*/
public static long getBotAccountId(BaseAbilityBot bot) {
String botToken = bot.getBotToken();
Matcher matcher = botTokenPattern.matcher(botToken);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid token format.");
}
return Long.parseLong(matcher.group(1));
}
/**
* 取消某一对话的状态机.
*
* @param bot AbilityBot 实例.
* @param chatId 要删除状态机的聊天 Id.
* @return 如果状态机存在, 则删除后返回 true, 不存在(未开启任何状态机, 即没有触发任何 Reply)则返回 false.
*/
public static boolean cancelReplyState(BaseAbilityBot bot, long chatId) {
Map<Long, Integer> stateMap = bot.db().getMap("user_state_replies");
if (!stateMap.containsKey(chatId)) {
return false;
}
stateMap.remove(chatId);
return true;
}
}

View File

@ -0,0 +1,121 @@
package net.lamgc.scalabot.extension.util;
import org.junit.jupiter.api.Test;
import org.mapdb.DBMaker;
import org.telegram.abilitybots.api.bot.AbilityBot;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.abilitybots.api.db.MapDBContext;
import org.telegram.abilitybots.api.objects.*;
import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.User;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class AbilityBotsTest {
public static final User USER = new User(1L, "first", false, "last", "username", null, false, false, false);
public static final User CREATOR = new User(1337L, "creatorFirst", false, "creatorLast", "creatorUsername", null, false, false, false);
static Update mockFullUpdate(BaseAbilityBot bot, User user, String args) {
bot.users().put(USER.getId(), USER);
bot.users().put(CREATOR.getId(), CREATOR);
bot.userIds().put(CREATOR.getUserName(), CREATOR.getId());
bot.userIds().put(USER.getUserName(), USER.getId());
bot.admins().add(CREATOR.getId());
Update update = mock(Update.class);
when(update.hasMessage()).thenReturn(true);
Message message = mock(Message.class);
when(message.getFrom()).thenReturn(user);
when(message.getText()).thenReturn(args);
when(message.hasText()).thenReturn(true);
when(message.isUserMessage()).thenReturn(true);
when(message.getChatId()).thenReturn(user.getId());
when(update.getMessage()).thenReturn(message);
return update;
}
@Test
void getBotAccountIdTest() {
String expectToken = "1234567890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
long actual = AbilityBots.getBotAccountId(new TestingAbilityBot(expectToken, "test"));
assertEquals(1234567890, actual);
String badTokenA = "12c34d56a7890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
assertThrows(IllegalArgumentException.class, () ->
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenA, "test")));
String badTokenB = "12c34d56a7890AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
assertThrows(IllegalArgumentException.class, () ->
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenB, "test")));
}
@Test
void cancelReplyStateTest() {
User userA = new User(10001L, "first", false, "last", "username", null, false, false, false);
User userB = new User(10101L, "first", false, "last", "username", null, false, false, false);
SilentSender silent = mock(SilentSender.class);
BaseAbilityBot bot = new TestingAbilityBot("", "", silent);
bot.onRegister();
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
verify(silent, times(1)).send("Reply set!", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
verify(silent, times(1)).send("Reply 01", userA.getId());
assertTrue(AbilityBots.cancelReplyState(bot, userA.getId()));
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
verify(silent, never()).send("Reply 02", userA.getId());
assertFalse(AbilityBots.cancelReplyState(bot, userB.getId()));
silent = mock(SilentSender.class);
bot = new TestingAbilityBot("", "", silent);
bot.onRegister();
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
verify(silent, times(1)).send("Reply set!", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
verify(silent, times(1)).send("Reply 01", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
verify(silent, times(1)).send("Reply 02", userA.getId());
}
public static class TestingAbilityBot extends AbilityBot {
public TestingAbilityBot(String botToken, String botUsername) {
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
}
public TestingAbilityBot(String botToken, String botUsername, SilentSender silentSender) {
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
this.silent = silentSender;
}
public Ability setReply() {
return Ability.builder()
.name("set_reply")
.enableStats()
.locality(Locality.ALL)
.privacy(Privacy.PUBLIC)
.action(ctx -> ctx.bot().silent().send("Reply set!", ctx.chatId()))
.reply(ReplyFlow.builder(db())
.action((bot, upd) -> bot.silent().send("Reply 01", upd.getMessage().getChatId()))
.onlyIf(upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_01"))
.next(Reply.of((bot, upd) ->
bot.silent().send("Reply 02", upd.getMessage().getChatId()),
upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_02")))
.build()
)
.build();
}
@Override
public long creatorId() {
return 0;
}
}
}