21 Commits

Author SHA1 Message Date
9c2ca5103c release: 发布 0.1.0 版本. 2022-03-15 15:30:35 +08:00
05af90a5a0 docs: 补充开发版本说明.
补充说明,要求用户升级前仔细阅读更新日志, 避免出现问题.
2022-03-15 15:30:07 +08:00
4b20b0cc59 feat(bot): 新增命令列表更新功能.
增加`命令列表更新`功能, 该功能可根据已加载的 Ability, 调用 Telegram Bot API 更新命令列表.
后续会尝试支持 BotCommandScope.
2022-03-15 15:18:04 +08:00
de83d2c3d3 refactor(example): say_hello 命令添加用户的账号信息.
补充账号信息, 方便查(为了测试).

P.S. 其实这个 Commit 应该算作 feat, 但这个是示例扩展, 所以就改成了 refactor.
2022-03-15 15:15:34 +08:00
22a824377a refactor(bot): 简化 ScalaBot 构造器的参数.
改动前的构造器参数大多是直接从 BotConfig 中传递进去, 这么做不利于添加新的参数, 因此改动后, BotConfig 将直接传递到 ScalaBot 的构造器中, 由 ScaleBot 内部按需获取参数进行初始化.
2022-03-15 14:13:53 +08:00
a0344f251f fix: 修复因某一个机器人启动失败而导致整个程序崩溃.
改动之后, 将容忍机器人的启动失败, 而不会因此异常退出程序.
2022-03-10 13:33:57 +08:00
84b67c7f89 refactor(bot): 补充一些日志信息.
补充启动过程的日志信息, 防止用户以为启动卡死 (因为在通过 Maven 仓库加载扩展包的时候会比较慢).
2022-03-10 13:31:37 +08:00
851fb0251f docs: 添加 Wiki 内容. 2022-03-03 21:25:17 +08:00
1776f07f16 refactor(extension): 修改 resolveRepositoryByUrl 中对构件的校验和检查策略.
将 Release 与 Snapshot 的 Checksum 策略设为 Fail, 可防止意外的包修改问题.
2022-02-28 08:38:21 +08:00
1f48bbae8e build(vcs): 补充对日志归档包的排除项.
补充日志归档包(.log.gz)的排除项, 排除了测试过程中产生的日志归档包.
2022-02-28 08:34:07 +08:00
00e90eabd0 fix(extension): 移除默认的 MavenRepositoryExtensionFinder.
将 MavenRepositoryExtensionFinder 的初始化过程交给 Launcher 执行.
2022-02-28 08:26:26 +08:00
285c8b04d1 fix: 修复因 config.json 不存在而引发的启动异常.
当 config.json 不存在时, 当 Launcher 类被加载时, 将导致因 config.json 加载失败而出现类初始化异常, 通过调整访问时机, 修复了这个问题.
2022-02-26 17:36:23 +08:00
a642948f45 feat: 可通过配置文件设置用于查找扩展包的 Maven 仓库.
使用 Maven 扩展包搜素器将不再限制仓库, 可通过配置文件添加其他仓库.
2022-02-26 17:30:31 +08:00
6df9f1b3c7 refactor(bot): 按照 Kotlin 官方代码规范, 修正代码格式错误.
Kotlin 官方代码规范建议把 Companion object 放在类的最后面, 确实应该如此.
2022-02-26 17:26:24 +08:00
4bc3776717 refactor(config): 更改 BotConfig 的 proxy 属性默认值.
为了让用户了解到可对机器人配置单独的代理, 而将 proxy 的默认值改为 ProxyConfig 缺省值.
2022-02-26 16:22:16 +08:00
eb34f17b06 refactor(config): 调整 BotConfig 中部分字段的默认值.
内置命令默认打开, 方便用户查询命令. baseApiUrl 默认值为 Telegram 官方 API 地址, 设置默认值可让用户了解可以对此项进行修改.
2022-02-26 16:18:32 +08:00
54b3e1cad7 fix(bot): 修复 disableBuiltInAbility 逻辑错误的问题.
设置反了, 所以 disableBuiltInAbility == true 就会打开内置命令.
2022-02-26 16:11:27 +08:00
35d092a22d refactor: 根据 Kotlin 规范, 调整类访问权.
按照 Kotlin 编码规范, 将序列化类调整为`内部`可预防意外的外部使用.
2022-02-26 15:46:08 +08:00
90434d9dbf perf(extension): 修正 FoundedPackage 统计方法.
该问题源于旧版本时方法的用途, 原本该方法用于合并所有已找到的 FoundedPackage, 但新版本已经被其他方法替代, 所以也没必要再通过合并来统计数量.
2022-02-26 15:37:50 +08:00
b3c63e5abe feat(config): 支持通过环境变量或 VM 参数指定数据目录.
主要是为 Docker 镜像支持, 这样可以在镜像中指定数据目录, 方便设置持久卷或者文件映射.
2022-02-26 12:00:16 +08:00
737ac9a08a fix: 修复了注释中的一个错字 :P
纠正了语句错误而已.
2022-02-24 02:23:53 +08:00
11 changed files with 392 additions and 106 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
# Log file # Log file
*.log *.log
*.log.gz
# BlueJ files # BlueJ files
*.ctxt *.ctxt

View File

@ -9,13 +9,26 @@ on [rubenlagus/TelegramBots](https://github.com/rubenlagus/TelegramBots).
是按 Bot 融入应用的方式设计的, 且 AbilityExtension 对 ReplyFlow 不太支持(因为无法获取所属 AbilityBot 的 StateDB 所以我尝试提供了一个 Factory 接口,在创建 是按 Bot 融入应用的方式设计的, 且 AbilityExtension 对 ReplyFlow 不太支持(因为无法获取所属 AbilityBot 的 StateDB 所以我尝试提供了一个 Factory 接口,在创建
AbilityExtension 对象时提供扩展将要服务的 AbilityBot 对象,这样 AbilityExtension 就可以不受限的实现功能了。 AbilityExtension 对象时提供扩展将要服务的 AbilityBot 对象,这样 AbilityExtension 就可以不受限的实现功能了。
## 开发版本警告
当前应用处于开发版本状态,在 1.0.0 发布前,任何功能都可能存在不兼容更改,在升级版本前,请仔细阅读更新日志, (如果有)按照迁移指南迁移数据后方可升级;
由于不遵循迁移指南而导致的损失,本项目相关开发人员不会对此负责。
### 版本号
本项目遵循 SemVer 版本号规范但在正式版1.0.0)发布前,可能会存在次版本号更新不向下兼容的问题,请仔细阅读迁移指南进行升级!
## 使用 ## 使用
1. 首先,在 Telegram 中联系 [BotFather](https://t.me/BotFather) ,申请机器人账号。 1. (如果没有准备机器人账号)首先,在 Telegram 中联系 [BotFather](https://t.me/BotFather) ,申请机器人账号。
2. 下载 [最新版本](https://github.com/LamGC/ScalaBot/releases/latest) 的 ScalaBot 将发行包解压到某个目录中,然后准备一个用于存储 ScalaBot 运行数据的目录。 2. 运行环境需要安装好 Java 11或更高版本
3. 在作为数据存储位置的目录中,执行从分发包中解压出来的 `bin/ScalaBot` 脚本以打开 ScalaBot。 由于首次启动缺少配置文件ScalaBot 将会初始化配置文件(`config.json``bot.json` 3. 下载 [最新版本](https://github.com/LamGC/ScalaBot/releases/latest) 的 ScalaBot 发行包, 将发行包解压到某个目录中,然后准备一个用于存储 ScalaBot
4. 将配置文件配置好后,如已下载好需要使用的扩展包,将扩展包移至 `extensions` 文件夹即可。(无需下载的扩展包将由 ScalaBot 自动下载) 运行数据的目录;
5. 如果一切正常ScalaBot 正常运行,绑定好的 Telegram Bot 账号将会对消息有所反应。 4. (可选)如果有需要在非运行目录的路径上运行 ScalaBot例如以 Service 形式启动,或者使用 Docker可通过环境变量 `BOT_DATA_PATH` 指定 ScalaBot 的运行目录;
5. 在作为数据存储位置的目录中,执行从分发包中解压出来的 `bin/ScalaBot` 脚本以打开 ScalaBot。 由于首次启动缺少配置文件ScalaBot 将会初始化配置文件(`config.json``bot.json`
),可按照 [配置文件示例](https://github.com/LamGC/ScalaBot/wiki/Configuration) 进行配置。
6. 将配置文件配置好后,如已下载好需要使用的扩展包,将扩展包移至 `extensions` 文件夹即可。(无需下载的扩展包将由 ScalaBot 自动下载)
7. 如果一切正常ScalaBot 正常运行,绑定好的 Telegram Bot 账号将会对消息有所反应。
## 开发扩展包 ## 开发扩展包

View File

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

View File

@ -1,15 +1,22 @@
package net.lamgc.scalabot package net.lamgc.scalabot
import ch.qos.logback.core.PropertyDefinerBase
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import mu.KotlinLogging import mu.KotlinLogging
import net.lamgc.scalabot.util.ArtifactSerializer import net.lamgc.scalabot.util.ArtifactSerializer
import net.lamgc.scalabot.util.AuthenticationSerializer
import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer
import net.lamgc.scalabot.util.ProxyTypeSerializer import net.lamgc.scalabot.util.ProxyTypeSerializer
import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.repository.Authentication
import org.eclipse.aether.repository.Proxy import org.eclipse.aether.repository.Proxy
import org.eclipse.aether.repository.RemoteRepository
import org.telegram.telegrambots.bots.DefaultBotOptions import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.ApiConstants
import java.io.File import java.io.File
import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -37,16 +44,17 @@ internal data class BotAccount(
internal data class BotConfig( internal data class BotConfig(
val enabled: Boolean = true, val enabled: Boolean = true,
val account: BotAccount, val account: BotAccount,
val disableBuiltInAbility: Boolean = true, val disableBuiltInAbility: Boolean = false,
val autoUpdateCommandList: Boolean = false,
/* /*
* 使用构件坐标来选择机器人所使用的扩展包. * 使用构件坐标来选择机器人所使用的扩展包.
* 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id, * 这么做的原因是我暂时没找到一个合适的方法来让开发者方便地设定自己的扩展 Id,
* 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目一定会设置的, * 而构件坐标(POM Reference 或者叫 GAV 坐标)是开发者创建 Maven/Gradle 项目一定会设置的,
* 所以就直接用了. :P * 所以就直接用了. :P
*/ */
val extensions: Set<Artifact>, val extensions: Set<Artifact>,
val proxy: ProxyConfig? = null, val proxy: ProxyConfig? = ProxyConfig(),
val baseApiUrl: String? = null val baseApiUrl: String? = ApiConstants.BASE_URL
) )
/** /**
@ -74,35 +82,100 @@ internal data class ProxyConfig(
internal data class MetricsConfig( internal data class MetricsConfig(
val enable: Boolean = false, val enable: Boolean = false,
val port: Int = 9386, val port: Int = 9386,
val bindAddress: String? = null val bindAddress: String? = "0.0.0.0"
) )
/**
* Maven 远端仓库配置.
* @property url 仓库地址.
* @property proxy 访问仓库所使用的代理, 仅支持 http/https 代理.
* @property layout 仓库布局版本, Maven 2 及以上使用 `default`, Maven 1 使用 `legacy`.
*/
internal data class MavenRepositoryConfig(
val url: URL,
val proxy: Proxy? = Proxy("http", "127.0.0.1", 1080),
val layout: String = "default",
// 可能要设计个 type 来判断解析成什么类型的 Authentication.
val authentication: Authentication? = null
) {
fun toRemoteRepository(): RemoteRepository {
val builder = RemoteRepository.Builder(null, 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())
}
return builder.build()
}
private companion object {
fun checkRepositoryLayout(layoutType: String): String {
val type = layoutType.trim().lowercase()
if (type != "default" && type != "legacy") {
throw IllegalArgumentException("Invalid layout type (expecting 'default' or 'legacy')")
}
return type
}
}
}
/** /**
* ScalaBot App 配置. * ScalaBot App 配置.
* *
* App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中. * App 配置信息与 BotConfig 分开, 分别存储在各自单独的文件中.
* @property proxy Telegram API 代理配置. * @property proxy Telegram API 代理配置.
* @property metrics 运行指标数据配置. 可通过时序数据库记录运行数据.
* @property mavenRepositories Maven 远端仓库配置.
*/ */
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()
) )
/** /**
* 需要用到的路径. * 需要用到的路径.
*
* 必须提供 `pathSupplier` 或 `fileSupplier` 其中一个, 才能正常提供路径.
*/ */
internal enum class AppPaths( internal enum class AppPaths(
private val pathSupplier: () -> String, private val pathSupplier: () -> String = { fileSupplier.invoke().canonicalPath },
private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer private val initializer: AppPaths.() -> Unit = AppPaths::defaultInitializer,
private val fileSupplier: () -> File = { File(pathSupplier()) }
) { ) {
DEFAULT_CONFIG_APPLICATION({ "./config.json" }, { /**
* 数据根目录.
*
* 所有运行数据的存放位置.
*
* 提示: 结尾不带 `/`.
*/
DATA_ROOT(fileSupplier = {
File(System.getProperty(PathConst.PROP_DATA_PATH) ?: System.getenv(PathConst.ENV_DATA_PATH) ?: ".")
}, initializer = {
val f = file
if (!f.exists()) {
f.mkdirs()
}
}),
DEFAULT_CONFIG_APPLICATION({ "$DATA_ROOT/config.json" }, {
if (!file.exists()) { if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use { file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.botConfigGson.toJson(AppConfig(), it) GsonConst.botConfigGson.toJson(
AppConfig(
mavenRepositories = listOf(
MavenRepositoryConfig(
URL(MavenRepositoryExtensionFinder.MAVEN_CENTRAL_URL)
)
)
), it
)
} }
} }
}), }),
DEFAULT_CONFIG_BOT({ "./bot.json" }, { DEFAULT_CONFIG_BOT({ "$DATA_ROOT/bot.json" }, {
if (!file.exists()) { if (!file.exists()) {
file.bufferedWriter(StandardCharsets.UTF_8).use { file.bufferedWriter(StandardCharsets.UTF_8).use {
GsonConst.botConfigGson.toJson( GsonConst.botConfigGson.toJson(
@ -121,15 +194,15 @@ internal enum class AppPaths(
} }
} }
}), }),
DATA_DB({ "./data/db/" }), DATA_DB({ "$DATA_ROOT/data/db/" }),
DATA_LOGS({ "./data/logs/" }), DATA_LOGS({ "$DATA_ROOT/data/logs/" }),
EXTENSIONS({ "./extensions/" }), EXTENSIONS({ "$DATA_ROOT/extensions/" }),
DATA_EXTENSIONS({ "./data/extensions/" }), DATA_EXTENSIONS({ "$DATA_ROOT/data/extensions/" }),
TEMP({ "./tmp/" }) TEMP({ "$DATA_ROOT/tmp/" })
; ;
val file: File val file: File
get() = File(pathSupplier.invoke()) get() = fileSupplier.invoke()
val path: String val path: String
get() = pathSupplier.invoke() get() = pathSupplier.invoke()
@ -138,7 +211,7 @@ internal enum class AppPaths(
@Synchronized @Synchronized
fun initial() { fun initial() {
if (!initialized.get()) { if (!initialized.get()) {
initializer(this) initializer()
initialized.set(true) initialized.set(true)
} }
} }
@ -146,6 +219,21 @@ internal enum class AppPaths(
override fun toString(): String { override fun toString(): String {
return path return path
} }
private companion object PathConst {
private const val PROP_DATA_PATH = "bot.path.data"
private const val ENV_DATA_PATH = "BOT_DATA_PATH"
}
}
/**
* 为 LogBack 提供日志目录路径.
*/
internal class LogDirectorySupplier : PropertyDefinerBase() {
override fun getPropertyValue(): String {
return AppPaths.DATA_LOGS.path
}
} }
internal object Const { internal object Const {
@ -153,14 +241,16 @@ internal object Const {
} }
private fun AppPaths.defaultInitializer() { private fun AppPaths.defaultInitializer() {
if (!file.exists()) { val f = file
val result = if (path.endsWith("/")) { val p = path
file.mkdirs() if (!f.exists()) {
val result = if (p.endsWith("/")) {
f.mkdirs()
} else { } else {
file.createNewFile() f.createNewFile()
} }
if (!result) { if (!result) {
log.warn { "初始化文件(夹)失败: $path" } log.warn { "初始化文件(夹)失败: $p" }
} }
} }
} }
@ -179,6 +269,8 @@ private object GsonConst {
val appConfigGson: Gson = baseGson.newBuilder() val appConfigGson: Gson = baseGson.newBuilder()
.registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer) .registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer)
.registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer)
.registerTypeAdapter(Authentication::class.java, AuthenticationSerializer)
.create() .create()
val botConfigGson: Gson = baseGson.newBuilder() val botConfigGson: Gson = baseGson.newBuilder()

View File

@ -17,6 +17,7 @@ private val launcher = Launcher()
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.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" } log.debug { "启动参数: ${args.joinToString(prefix = "[", postfix = "]")}" }
initialFiles() initialFiles()
if (Const.config.metrics.enable) { if (Const.config.metrics.enable) {
@ -61,7 +62,11 @@ internal class Launcher : AutoCloseable {
return false return false
} }
for (botConfig in botConfigs) { for (botConfig in botConfigs) {
launchBot(botConfig) try {
launchBot(botConfig)
} catch (e: Exception) {
log.error(e) { "机器人 `${botConfig.account.name}` 启动时发生错误." }
}
} }
return true return true
} }
@ -93,17 +98,41 @@ internal class Launcher : AutoCloseable {
} }
} }
val account = botConfig.account val account = botConfig.account
val remoteRepositories = Const.config.mavenRepositories
.map(MavenRepositoryConfig::toRemoteRepository)
.toMutableList().apply {
add(MavenRepositoryExtensionFinder.getMavenCentralRepository(proxy = Const.config.proxy.toAetherProxy()))
}.toList()
val extensionPackageFinders = setOf(
MavenRepositoryExtensionFinder(
remoteRepositories = remoteRepositories,
proxy = Const.config.proxy.toAetherProxy()
)
)
val bot = ScalaBot( val bot = ScalaBot(
account.name,
account.token,
account.creatorId,
BotDBMaker.getBotMaker(account), BotDBMaker.getBotMaker(account),
botOption, botOption,
botConfig.extensions, extensionPackageFinders,
botConfig.disableBuiltInAbility botConfig
) )
botSessionMap[bot] = botApi.registerBot(bot) botSessionMap[bot] = botApi.registerBot(bot)
log.info { "机器人 `${bot.botUsername}` 已启动." } log.info { "机器人 `${bot.botUsername}` 已启动." }
if (botConfig.autoUpdateCommandList) {
log.debug { "[Bot ${botConfig.account.name}] 正在自动更新命令列表..." }
try {
val result = bot.updateCommandList()
if (result) {
log.info { "[Bot ${botConfig.account.name}] 已成功更新 Bot 命令列表." }
} else {
log.warn { "[Bot ${botConfig.account.name}] 自动更新 Bot 命令列表失败!" }
}
} catch (e: Exception) {
log.warn(e) { "命令列表自动更新失败." }
}
}
} }
@Synchronized @Synchronized

View File

@ -4,7 +4,6 @@ import mu.KotlinLogging
import net.lamgc.scalabot.extension.BotExtensionFactory import net.lamgc.scalabot.extension.BotExtensionFactory
import net.lamgc.scalabot.util.getPriority import net.lamgc.scalabot.util.getPriority
import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.repository.LocalRepository
import org.telegram.abilitybots.api.util.AbilityExtension import org.telegram.abilitybots.api.util.AbilityExtension
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -18,18 +17,15 @@ import java.util.concurrent.atomic.AtomicInteger
internal class ExtensionLoader( internal class ExtensionLoader(
private val bot: ScalaBot, private val bot: ScalaBot,
private val extensionsDataFolder: File = AppPaths.DATA_EXTENSIONS.file, private val extensionsDataFolder: File = AppPaths.DATA_EXTENSIONS.file,
private val extensionsPath: File = AppPaths.EXTENSIONS.file private val extensionsPath: File = AppPaths.EXTENSIONS.file,
private val extensionFinders: Set<ExtensionPackageFinder> = setOf()
) { ) {
private val log = KotlinLogging.logger { } private val log = KotlinLogging.logger { }
private val finders: Set<ExtensionPackageFinder> = setOf( private val finders: Set<ExtensionPackageFinder> = mutableSetOf(
FileNameFinder, FileNameFinder,
MavenMetaInformationFinder, MavenMetaInformationFinder
MavenRepositoryExtensionFinder( ).apply { addAll(extensionFinders) }.toSet()
LocalRepository("${System.getProperty("user.home")}/.m2/repository"),
proxy = Const.config.proxy.toAetherProxy()
)
)
fun getExtensions(): Set<LoadedExtensionEntry> { fun getExtensions(): Set<LoadedExtensionEntry> {
val extensionEntries = mutableSetOf<LoadedExtensionEntry>() val extensionEntries = mutableSetOf<LoadedExtensionEntry>()
@ -103,13 +99,11 @@ internal class ExtensionLoader(
} }
private fun allFoundedPackageNumber(filesMap: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Int { private fun allFoundedPackageNumber(filesMap: Map<ExtensionPackageFinder, Set<FoundExtensionPackage>>): Int {
val result = mutableSetOf<URL>() var number = 0
for (files in filesMap.values) { for (files in filesMap.values) {
for (file in files) { number += files.size
result.add(file.getRawUrl())
}
} }
return result.size return number
} }
private fun findExtensionPackage( private fun findExtensionPackage(

View File

@ -234,7 +234,7 @@ internal object MavenMetaInformationFinder : ExtensionPackageFinder {
*/ */
@FinderRules(priority = FinderPriority.REMOTE) @FinderRules(priority = FinderPriority.REMOTE)
internal class MavenRepositoryExtensionFinder( internal class MavenRepositoryExtensionFinder(
private val localRepository: LocalRepository, private val localRepository: LocalRepository = LocalRepository("${System.getProperty("user.home")}/.m2/repository"),
private val proxy: Proxy? = null, private val proxy: Proxy? = null,
private val remoteRepositories: List<RemoteRepository> = listOf(getMavenCentralRepository(proxy)), private val remoteRepositories: List<RemoteRepository> = listOf(getMavenCentralRepository(proxy)),
) : ExtensionPackageFinder { ) : ExtensionPackageFinder {
@ -354,14 +354,14 @@ internal class MavenRepositoryExtensionFinder(
RepositoryPolicy( RepositoryPolicy(
true, true,
RepositoryPolicy.UPDATE_POLICY_DAILY, RepositoryPolicy.UPDATE_POLICY_DAILY,
RepositoryPolicy.CHECKSUM_POLICY_WARN RepositoryPolicy.CHECKSUM_POLICY_FAIL
) )
) )
builder.setSnapshotPolicy( builder.setSnapshotPolicy(
RepositoryPolicy( RepositoryPolicy(
true, true,
RepositoryPolicy.UPDATE_POLICY_ALWAYS, RepositoryPolicy.UPDATE_POLICY_ALWAYS,
RepositoryPolicy.CHECKSUM_POLICY_WARN RepositoryPolicy.CHECKSUM_POLICY_FAIL
) )
) )
if (authentication != null) { if (authentication != null) {

View File

@ -7,32 +7,115 @@ import mu.KotlinLogging
import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.Artifact
import org.telegram.abilitybots.api.bot.AbilityBot import org.telegram.abilitybots.api.bot.AbilityBot
import org.telegram.abilitybots.api.db.DBContext import org.telegram.abilitybots.api.db.DBContext
import org.telegram.abilitybots.api.objects.Ability
import org.telegram.abilitybots.api.toggle.BareboneToggle import org.telegram.abilitybots.api.toggle.BareboneToggle
import org.telegram.abilitybots.api.toggle.DefaultToggle import org.telegram.abilitybots.api.toggle.DefaultToggle
import org.telegram.telegrambots.bots.DefaultBotOptions import org.telegram.telegrambots.bots.DefaultBotOptions
import org.telegram.telegrambots.meta.api.methods.commands.DeleteMyCommands
import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands
import org.telegram.telegrambots.meta.api.objects.Update import org.telegram.telegrambots.meta.api.objects.Update
import org.telegram.telegrambots.meta.api.objects.commands.BotCommand
/** /**
* 可扩展 Bot. * 可扩展 Bot.
* @param name 机器人名称. 建议设为机器人用户名.
* @param token 机器人 API 令牌.
* @property creatorId 机器人所有人的 Telegram 用户 Id. 可通过联系部分机器人来获取该信息. * @property creatorId 机器人所有人的 Telegram 用户 Id. 可通过联系部分机器人来获取该信息.
* (e.g. [@userinfobot](http://t.me/userinfobot)) * (e.g. [@userinfobot](http://t.me/userinfobot))
* @param db 机器人数据库对象. 用于状态机等用途. * @param db 机器人数据库对象. 用于状态机等用途.
* @param options AbilityBot 设置对象. * @param options AbilityBot 设置对象.
* @property extensions 扩展坐标集合. * @property extensions 扩展坐标集合.
* @param disableBuiltInAbility 是否禁用 [AbilityBot] 内置命令.
*/ */
internal class ScalaBot( internal class ScalaBot(
name: String,
token: String,
private val creatorId: Long,
db: DBContext, db: DBContext,
options: DefaultBotOptions, options: DefaultBotOptions,
val extensions: Set<Artifact>, extensionFinders: Set<ExtensionPackageFinder>,
disableBuiltInAbility: Boolean botConfig: BotConfig,
private val creatorId: Long = botConfig.account.creatorId,
val extensions: Set<Artifact> = botConfig.extensions
) : ) :
AbilityBot(token, name, db, if (disableBuiltInAbility) DefaultToggle() else BareboneToggle(), options) { AbilityBot(
botConfig.account.token,
botConfig.account.name,
db,
if (botConfig.disableBuiltInAbility)
BareboneToggle()
else
DefaultToggle(),
options
) {
private val extensionLoader = ExtensionLoader(
bot = this,
extensionFinders = extensionFinders
)
init {
log.info { "[Bot $botUsername] 正在加载扩展..." }
val extensionEntries = extensionLoader.getExtensions()
for (entry in extensionEntries) {
addExtension(entry.extension)
log.debug {
"[Bot $botUsername] 扩展包 `${entry.extensionArtifact}` 中的扩展 `${entry.extension::class.qualifiedName}` " +
"(由工厂类 `${entry.factoryClass.name}` 创建) 已注册."
}
}
log.info { "[Bot $botUsername] 扩展加载完成." }
}
override fun creatorId(): Long = creatorId
override fun onUpdateReceived(update: Update?) {
botUpdateCounter.labels(botUsername).inc()
botUpdateGauge.labels(botUsername).inc()
val timer = updateProcessTime.labels(botUsername).startTimer()
try {
super.onUpdateReceived(update)
} catch (e: Exception) {
exceptionHandlingCounter.labels(botUsername).inc()
throw e
} finally {
timer.observeDuration()
botUpdateGauge.labels(botUsername).dec()
}
}
/**
* 更新 Telegram Bot 的命令列表.
*
* 本方法将根据已注册的 [Ability] 更新 Telegram 中机器人的命令列表.
*
* 调用本方法前, 必须先调用一次 [registerAbilities], 否则无法获取 Ability 信息.
* @return 更新成功返回 `true`.
*/
fun updateCommandList(): Boolean {
if (abilities() == null) {
throw IllegalStateException("Abilities has not been initialized.")
}
val botCommands = abilities().values.map {
val abilityInfo = if (it.info() == null || it.info().trim().isEmpty()) {
log.warn { "[Bot $botUsername] Ability `${it.name()}` 没有说明信息." }
"(The command has no description)"
} else {
log.debug { "[Bot $botUsername] Ability `${it.name()}` info `${it.info()}`" }
it.info().trim()
}
BotCommand(it.name(), abilityInfo)
}
val setMyCommands = SetMyCommands()
setMyCommands.commands = botCommands
return execute(DeleteMyCommands()) && execute(setMyCommands)
}
override fun onRegister() {
super.onRegister()
onlineBotGauge.inc()
}
override fun onClosing() {
super.onClosing()
onlineBotGauge.dec()
}
companion object { companion object {
@JvmStatic @JvmStatic
@ -83,45 +166,4 @@ internal class ScalaBot(
.subsystem("telegrambots") .subsystem("telegrambots")
.register() .register()
} }
private val extensionLoader = ExtensionLoader(this)
init {
val extensionEntries = extensionLoader.getExtensions()
for (entry in extensionEntries) {
addExtension(entry.extension)
log.debug {
"[Bot ${botUsername}] 扩展包 `${entry.extensionArtifact}` 中的扩展 `${entry.extension::class.qualifiedName}` " +
"(由工厂类 `${entry.factoryClass.name}` 创建) 已注册."
}
}
}
override fun creatorId(): Long = creatorId
override fun onUpdateReceived(update: Update?) {
botUpdateCounter.labels(botUsername).inc()
botUpdateGauge.labels(botUsername).inc()
val timer = updateProcessTime.labels(botUsername).startTimer()
try {
super.onUpdateReceived(update)
} catch (e: Exception) {
exceptionHandlingCounter.labels(botUsername).inc()
throw e
} finally {
timer.observeDuration()
botUpdateGauge.labels(botUsername).dec()
}
}
override fun onRegister() {
super.onRegister()
onlineBotGauge.inc()
}
override fun onClosing() {
super.onClosing()
onlineBotGauge.dec()
}
} }

View File

@ -1,12 +1,18 @@
package net.lamgc.scalabot.util package net.lamgc.scalabot.util
import com.google.gson.* import com.google.gson.*
import mu.KotlinLogging
import net.lamgc.scalabot.MavenRepositoryConfig
import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.repository.Authentication
import org.eclipse.aether.repository.Proxy
import org.eclipse.aether.util.repository.AuthenticationBuilder
import org.telegram.telegrambots.bots.DefaultBotOptions import org.telegram.telegrambots.bots.DefaultBotOptions
import java.lang.reflect.Type import java.lang.reflect.Type
import java.net.URL
object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>, internal object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
JsonSerializer<DefaultBotOptions.ProxyType> { JsonSerializer<DefaultBotOptions.ProxyType> {
override fun deserialize( override fun deserialize(
@ -34,7 +40,7 @@ object ProxyTypeSerializer : JsonDeserializer<DefaultBotOptions.ProxyType>,
} }
} }
object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> { internal object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact> {
override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { override fun serialize(src: Artifact, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}") val gavBuilder = StringBuilder("${src.groupId}:${src.artifactId}")
if (!src.extension.equals("jar")) { if (!src.extension.equals("jar")) {
@ -55,3 +61,105 @@ object ArtifactSerializer : JsonSerializer<Artifact>, JsonDeserializer<Artifact>
} }
internal object AuthenticationSerializer : JsonDeserializer<Authentication> {
private val log = KotlinLogging.logger { }
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Authentication? {
val builder = AuthenticationBuilder()
when (json) {
is JsonArray -> {
for (element in json) {
if (element is JsonArray) {
builder.addCustom(jsonArrayToAuthentication(element))
} else if (element is JsonObject) {
jsonToAuthentication(element, builder)
}
}
}
is JsonObject -> {
jsonToAuthentication(json, builder)
}
else -> {
throw JsonParseException("Unsupported JSON data type: ${json::class.java}")
}
}
return builder.build()
}
private fun jsonArrayToAuthentication(jsonArray: JsonArray): Authentication {
val builder = AuthenticationBuilder()
for (element in jsonArray) {
when (element) {
is JsonObject -> jsonToAuthentication(element, builder)
is JsonArray -> builder.addCustom(jsonArrayToAuthentication(element))
else -> log.warn { "不支持的 Json 类型: ${element::class.java}" }
}
}
return builder.build()
}
private const val KEY_TYPE = "type"
private fun jsonToAuthentication(json: JsonObject, builder: AuthenticationBuilder) {
if (!json.has(KEY_TYPE)) {
log.warn { "缺少 type 字段, 无法判断 Maven 认证信息类型." }
return
} else if (!json.get(KEY_TYPE).isJsonPrimitive) {
log.warn { "type 字段类型错误(应为 Primitive 类型), 无法判断 Maven 认证信息类型.(实际类型: `${json::class.java}`)" }
return
}
when (json.get(KEY_TYPE).asString.trim().lowercase()) {
"string" -> {
builder.addString(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
}
"secret" -> {
builder.addSecret(checkJsonKey(json, "key"), checkJsonKey(json, "value"))
}
}
}
}
private fun checkJsonKey(json: JsonObject, key: String): String {
if (!json.has(key)) {
throw JsonParseException("Required field does not exist: $key")
} else if (!json.get(key).isJsonPrimitive) {
throw JsonParseException("Wrong field `$key` type: ${json.get(key)::class.java}")
}
return json.get(key).asString
}
internal object MavenRepositoryConfigSerializer
: JsonDeserializer<MavenRepositoryConfig> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): MavenRepositoryConfig {
return when (json) {
is JsonObject -> {
MavenRepositoryConfig(
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",
authentication = if (json.has("authentication") && json.get("authentication").isJsonObject)
context.deserialize<Authentication>(
json.getAsJsonObject("authentication"), Authentication::class.java
) else null
)
}
is JsonPrimitive -> {
MavenRepositoryConfig(URL(json.asString))
}
else -> {
throw JsonParseException("Unsupported Maven warehouse configuration type.")
}
}
}
}

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<included> <included>
<define name="DATA_LOGS" class="net.lamgc.scalabot.LogDirectorySupplier"/>
<appender name="STD_OUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STD_OUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>[%d{HH:mm:ss.SSS} %5level][%logger{36}][%thread]: %msg%n</pattern> <pattern>[%d{HH:mm:ss.SSS} %5level][%logger{36}][%thread]: %msg%n</pattern>
@ -20,7 +22,7 @@
</appender> </appender>
<appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE_OUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>data/logs/latest.log</file> <file>${DATA_LOGS}/latest.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>data/logs/%d{yyyy-MM-dd}.log.gz</fileNamePattern> <fileNamePattern>data/logs/%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory> <maxHistory>30</maxHistory>

View File

@ -23,7 +23,12 @@ public class SayHelloExtension implements AbilityExtension {
.info("Say hello to you.") .info("Say hello to you.")
.privacy(Privacy.PUBLIC) .privacy(Privacy.PUBLIC)
.locality(Locality.ALL) .locality(Locality.ALL)
.action(ctx -> ctx.bot().silent().send("Hello! " + ctx.user().getUserName(), ctx.chatId())) .action(ctx -> {
String msg = "Hello! " + ctx.user().getUserName() +
" ( " + ctx.user().getId() + " ) [ " + ctx.user().getLanguageCode() + " ]" + "\n" +
"Current Chat ID: " + ctx.chatId();
ctx.bot().silent().send(msg, ctx.chatId());
})
.build(); .build();
} }