mirror of
https://github.com/LamGC/ScalaBot.git
synced 2025-07-01 04:47:24 +00:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e51327ed7
|
|||
93cf5c4e2f
|
|||
e12f858690
|
|||
3e99d7d033
|
|||
8131f41313
|
|||
270e744bcf
|
|||
d84465ebd9
|
|||
18bc3a8d48
|
|||
2b88134207
|
|||
142eddfa28
|
|||
64849adfab
|
|||
29bd12a8dd
|
|||
9cdf10ccc2
|
|||
4210efef3b
|
|||
bc0f3be32c
|
|||
c5f28e395e
|
|||
1172caa8d7
|
|||
eb95436404
|
|||
804d0e3012
|
|||
1281dbcabe
|
|||
1cd26b3b25
|
|||
19162dcaef
|
|||
0748afaff5
|
|||
b20b25bc7b
|
@ -7,5 +7,5 @@ allprojects {
|
||||
|
||||
}
|
||||
group = "net.lamgc"
|
||||
version = "0.1.0"
|
||||
version = "0.2.1"
|
||||
}
|
@ -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,13 +22,14 @@ 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")
|
||||
|
@ -32,7 +32,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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人配置.
|
||||
@ -127,11 +134,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 +161,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()) {
|
||||
@ -220,9 +232,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"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ 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
|
||||
@ -12,14 +13,14 @@ 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()
|
||||
|
||||
val launcher = Launcher()
|
||||
.registerShutdownHook()
|
||||
if (Const.config.metrics.enable) {
|
||||
startMetricsServer()
|
||||
}
|
||||
@ -53,6 +54,16 @@ internal class Launcher : AutoCloseable {
|
||||
|
||||
private val botApi = TelegramBotsApi(DefaultBotSession::class.java)
|
||||
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
|
||||
fun launch(): Boolean {
|
||||
@ -106,13 +117,14 @@ internal class Launcher : AutoCloseable {
|
||||
}.toList()
|
||||
val extensionPackageFinders = setOf(
|
||||
MavenRepositoryExtensionFinder(
|
||||
localRepository = mavenLocalRepository,
|
||||
remoteRepositories = remoteRepositories,
|
||||
proxy = Const.config.proxy.toAetherProxy()
|
||||
)
|
||||
)
|
||||
|
||||
val bot = ScalaBot(
|
||||
BotDBMaker.getBotMaker(account),
|
||||
BotDBMaker.getBotDbInstance(account),
|
||||
botOption,
|
||||
extensionPackageFinders,
|
||||
botConfig
|
||||
@ -138,9 +150,16 @@ internal class Launcher : AutoCloseable {
|
||||
@Synchronized
|
||||
override fun close() {
|
||||
botSessionMap.forEach {
|
||||
log.info { "正在关闭机器人 `${it.key.botUsername}` ..." }
|
||||
it.value.stop()
|
||||
log.info { "已关闭机器人 `${it.key.botUsername}`." }
|
||||
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}` 关闭时发生异常." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,190 @@
|
||||
package net.lamgc.scalabot
|
||||
|
||||
import com.google.common.io.Files
|
||||
import mu.KotlinLogging
|
||||
import net.lamgc.scalabot.util.toHaxString
|
||||
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.toHaxString()}.db")
|
||||
})
|
@ -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})" }
|
||||
|
@ -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(
|
||||
|
@ -34,11 +34,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,10 +44,8 @@ internal fun File.deepListFiles(
|
||||
if (!onlyFile) {
|
||||
result.add(file)
|
||||
}
|
||||
val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter)
|
||||
if (subFiles != null) {
|
||||
result.addAll(subFiles)
|
||||
}
|
||||
val subFiles = file.deepListFiles(false, onlyFile, fileFilter, filenameFilter) ?: continue
|
||||
result.addAll(subFiles)
|
||||
}
|
||||
}
|
||||
return result.toTypedArray()
|
||||
|
@ -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"/>
|
||||
|
31
scalabot-app/src/test/kotlin/AppConfigTest.kt
Normal file
31
scalabot-app/src/test/kotlin/AppConfigTest.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -7,11 +7,12 @@ plugins {
|
||||
}
|
||||
|
||||
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 +24,8 @@ tasks.withType<Javadoc> {
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user