24 Commits

Author SHA1 Message Date
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
16 changed files with 459 additions and 74 deletions

View File

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

View File

@ -11,7 +11,7 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.7.36") implementation("org.slf4j:slf4j-api:1.7.36")
implementation("io.github.microutils:kotlin-logging:2.1.21") 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" val aetherVersion = "1.1.0"
implementation("org.eclipse.aether:aether-api:$aetherVersion") implementation("org.eclipse.aether:aether-api:$aetherVersion")
@ -22,13 +22,14 @@ dependencies {
implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion") implementation("org.eclipse.aether:aether-connector-basic:$aetherVersion")
implementation("org.apache.maven:maven-aether-provider:3.3.9") 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("com.google.code.gson:gson:2.9.0")
implementation("org.jdom:jdom2:2.0.6.1") implementation("org.jdom:jdom2:2.0.6.1")
implementation("org.telegram:telegrambots-abilities:5.6.0") implementation("org.telegram:telegrambots-abilities:6.0.1")
implementation("org.telegram:telegrambots:5.6.0") implementation("org.telegram:telegrambots:6.0.1")
implementation("io.prometheus:simpleclient:0.15.0") implementation("io.prometheus:simpleclient:0.15.0")
implementation("io.prometheus:simpleclient_httpserver:0.15.0") implementation("io.prometheus:simpleclient_httpserver:0.15.0")

View File

@ -32,7 +32,14 @@ internal data class BotAccount(
val name: String, val name: String,
val token: String, val token: String,
val creatorId: Long = -1 val creatorId: Long = -1
) ) {
val id
// 不要想着每次获取都要从 token 里取出有性能损耗.
// 由于 Gson 解析方式, 如果不这么做, 会出现 token 设置前 id 初始化完成, 就只有"0"了,
// 虽然能过单元测试, 但实际使用过程是不能正常用的.
get() = token.substringBefore(":").toLong()
}
/** /**
* 机器人配置. * 机器人配置.
@ -127,11 +134,13 @@ internal data class MavenRepositoryConfig(
* @property proxy Telegram API 代理配置. * @property proxy Telegram API 代理配置.
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据. * @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
* @property mavenRepositories Maven 远端仓库配置. * @property mavenRepositories Maven 远端仓库配置.
* @property mavenLocalRepository Maven 本地仓库路径. 相对于运行目录 (而不是 DATA_ROOT 目录)
*/ */
internal data class AppConfig( internal data class AppConfig(
val proxy: ProxyConfig = ProxyConfig(), val proxy: ProxyConfig = ProxyConfig(),
val metrics: MetricsConfig = MetricsConfig(), val metrics: MetricsConfig = MetricsConfig(),
val mavenRepositories: List<MavenRepositoryConfig> = emptyList() val mavenRepositories: List<MavenRepositoryConfig> = emptyList(),
val mavenLocalRepository: String? = null
) )
/** /**
@ -152,7 +161,10 @@ internal enum class AppPaths(
* 提示: 结尾不带 `/`. * 提示: 结尾不带 `/`.
*/ */
DATA_ROOT(fileSupplier = { 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 = { }, initializer = {
val f = file val f = file
if (!f.exists()) { if (!f.exists()) {
@ -220,9 +232,9 @@ internal enum class AppPaths(
return path return path
} }
private companion object PathConst { private object PathConst {
private const val PROP_DATA_PATH = "bot.path.data" const val PROP_DATA_PATH = "bot.path.data"
private const val ENV_DATA_PATH = "BOT_DATA_PATH" const val ENV_DATA_PATH = "BOT_DATA_PATH"
} }
} }

View File

@ -4,6 +4,7 @@ import io.prometheus.client.exporter.HTTPServer
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import net.lamgc.scalabot.util.registerShutdownHook import net.lamgc.scalabot.util.registerShutdownHook
import org.eclipse.aether.repository.LocalRepository
import org.telegram.telegrambots.bots.DefaultBotOptions import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.TelegramBotsApi import org.telegram.telegrambots.meta.TelegramBotsApi
import org.telegram.telegrambots.meta.generics.BotSession import org.telegram.telegrambots.meta.generics.BotSession
@ -12,14 +13,14 @@ import kotlin.system.exitProcess
private val log = KotlinLogging.logger { } private val log = KotlinLogging.logger { }
private val launcher = Launcher()
.registerShutdownHook()
fun main(args: Array<String>): Unit = runBlocking { fun main(args: Array<String>): Unit = runBlocking {
log.info { "ScalaBot 正在启动中..." } log.info { "ScalaBot 正在启动中..." }
log.info { "数据目录: ${AppPaths.DATA_ROOT}" } log.info { "数据目录: ${AppPaths.DATA_ROOT}" }
log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" } log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
initialFiles() initialFiles()
val launcher = Launcher()
.registerShutdownHook()
if (Const.config.metrics.enable) { if (Const.config.metrics.enable) {
startMetricsServer() startMetricsServer()
} }
@ -53,6 +54,16 @@ internal class Launcher : AutoCloseable {
private val botApi = TelegramBotsApi(DefaultBotSession::class.java) private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
private val botSessionMap = mutableMapOf<ScalaBot, BotSession>() private val botSessionMap = mutableMapOf<ScalaBot, BotSession>()
private val mavenLocalRepository =
if (Const.config.mavenLocalRepository != null && Const.config.mavenLocalRepository.isNotEmpty()) {
val repoPath = AppPaths.DATA_ROOT.file.toPath()
.resolve(Const.config.mavenLocalRepository)
.toRealPath()
.toFile()
LocalRepository(repoPath)
} else {
LocalRepository("${System.getProperty("user.home")}/.m2/repository")
}
@Synchronized @Synchronized
fun launch(): Boolean { fun launch(): Boolean {
@ -106,13 +117,14 @@ internal class Launcher : AutoCloseable {
}.toList() }.toList()
val extensionPackageFinders = setOf( val extensionPackageFinders = setOf(
MavenRepositoryExtensionFinder( MavenRepositoryExtensionFinder(
localRepository = mavenLocalRepository,
remoteRepositories = remoteRepositories, remoteRepositories = remoteRepositories,
proxy = Const.config.proxy.toAetherProxy() proxy = Const.config.proxy.toAetherProxy()
) )
) )
val bot = ScalaBot( val bot = ScalaBot(
BotDBMaker.getBotMaker(account), BotDBMaker.getBotDbInstance(account),
botOption, botOption,
extensionPackageFinders, extensionPackageFinders,
botConfig botConfig
@ -138,9 +150,16 @@ internal class Launcher : AutoCloseable {
@Synchronized @Synchronized
override fun close() { override fun close() {
botSessionMap.forEach { botSessionMap.forEach {
log.info { "正在关闭机器人 `${it.key.botUsername}` ..." } try {
it.value.stop() if (!it.value.isRunning) {
log.info { "已关闭机器人 `${it.key.botUsername}`." } 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 package net.lamgc.scalabot
import com.google.common.io.Files
import mu.KotlinLogging
import net.lamgc.scalabot.util.toHaxString import net.lamgc.scalabot.util.toHaxString
import org.mapdb.DB
import org.mapdb.DBException
import org.mapdb.DBMaker import org.mapdb.DBMaker
import org.telegram.abilitybots.api.db.DBContext import org.telegram.abilitybots.api.db.DBContext
import org.telegram.abilitybots.api.db.MapDBContext import org.telegram.abilitybots.api.db.MapDBContext
import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest 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 { internal object BotDBMaker {
fun getBotMaker(botAccount: BotAccount): DBContext { private val logger = KotlinLogging.logger { }
val digest: MessageDigest = MessageDigest.getInstance("SHA-256")
val digestBytes = digest.digest(botAccount.token.toByteArray(StandardCharsets.UTF_8)) fun getBotDbInstance(botAccount: BotAccount): DBContext {
val dbPath = AppPaths.DATA_DB.path + "${digestBytes.toHaxString()}.db" for (adapter in adapters) {
val db = DBMaker.fileDB(dbPath) 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() .closeOnJvmShutdownWeakReference()
.checksumStoreEnable() .checksumStoreEnable()
.fileChannelEnable() .fileChannelEnable()
.make() .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.toHaxString()}.db")
})

View File

@ -90,6 +90,10 @@ internal class ExtensionLoader(
try { try {
val extension = val extension =
factory.createExtensionInstance(bot, getExtensionDataFolder(extensionArtifact)) 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)) factories.add(LoadedExtensionEntry(extensionArtifact, factory::class.java, extension))
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "创建扩展时发生异常. (ExtArtifact: `$extensionArtifact`, Factory: ${factory::class.java.name})" } log.error(e) { "创建扩展时发生异常. (ExtArtifact: `$extensionArtifact`, Factory: ${factory::class.java.name})" }

View File

@ -30,6 +30,7 @@ internal class ScalaBot(
extensionFinders: Set<ExtensionPackageFinder>, extensionFinders: Set<ExtensionPackageFinder>,
botConfig: BotConfig, botConfig: BotConfig,
private val creatorId: Long = botConfig.account.creatorId, private val creatorId: Long = botConfig.account.creatorId,
val accountId: Long = botConfig.account.id,
val extensions: Set<Artifact> = botConfig.extensions val extensions: Set<Artifact> = botConfig.extensions
) : ) :
AbilityBot( AbilityBot(

View File

@ -34,11 +34,7 @@ internal fun File.deepListFiles(
this.listFiles(filenameFilter) this.listFiles(filenameFilter)
} else { } else {
this.listFiles() this.listFiles()
} } ?: return null
if (files == null) {
return null
}
val result = if (addSelf) mutableSetOf(this) else mutableSetOf() val result = if (addSelf) mutableSetOf(this) else mutableSetOf()
for (file in files) { for (file in files) {
@ -48,10 +44,8 @@ internal fun File.deepListFiles(
if (!onlyFile) { if (!onlyFile) {
result.add(file) result.add(file)
} }
val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter) val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter) ?: continue
if (subFiles != null) { result.addAll(subFiles)
result.addAll(subFiles)
}
} }
} }
return result.toTypedArray() return result.toTypedArray()

View File

@ -5,6 +5,8 @@
<logger name="org.eclipse.aether.internal.impl.DefaultTransporterProvider" level="INFO"/> <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.DefaultRepositoryConnectorProvider" level="INFO"/>
<logger name="org.eclipse.aether.internal.impl.EnhancedLocalRepositoryManager" 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"> <root level="DEBUG">
<appender-ref ref="FILE_OUT"/> <appender-ref ref="FILE_OUT"/>

View File

@ -0,0 +1,31 @@
package net.lamgc.scalabot
import com.google.gson.Gson
import java.util.*
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
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)
}
}

View File

@ -37,11 +37,16 @@ public class SayHelloExtension implements AbilityExtension {
*/ */
public Ability test() { public Ability test() {
ReplyFlow botHello = ReplyFlow.builder(bot.db()) ReplyFlow botHello = ReplyFlow.builder(bot.db())
.enableStats("say_hello")
.action((bot, upd) -> bot.silent().send("What is u name?", upd.getMessage().getChatId())) .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() .next(Reply.of((bot, upd) -> bot.silent()
.send("OK! You name is " + upd.getMessage().getText().substring("my name is ".length()), upd.getMessage().getChatId()), .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(); .build();
return Ability.builder() return Ability.builder()

View File

@ -7,11 +7,12 @@ plugins {
} }
dependencies { 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") api("org.slf4j:slf4j-api:1.7.36")
// There is nothing to test. testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
// testImplementation(kotlin("test")) testImplementation("org.mockito:mockito-core:4.4.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
} }
tasks.withType<Javadoc> { tasks.withType<Javadoc> {
@ -23,6 +24,8 @@ tasks.withType<Javadoc> {
java { java {
withJavadocJar() withJavadocJar()
withSourcesJar() withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
} }
tasks.test { tasks.test {

View File

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