From 09974aad9e9942f510faf61800bb21c14ee95f7b Mon Sep 17 00:00:00 2001 From: dongRogen <3601778801@qq.com> Date: Thu, 4 Aug 2022 10:59:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=A2=E9=98=85=E5=A4=84?= =?UTF-8?q?=E7=90=86=20-=20=E5=AE=9E=E7=8E=B0=E8=AE=A2=E9=98=85=E5=A4=84?= =?UTF-8?q?=E7=90=86=20SubscribeHandler=20-=20=E6=B6=88=E6=81=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=B8=AD=E7=9A=84=20HTMLPattern=20=E8=AE=BE=E4=B8=BA?= =?UTF-8?q?=20public=20-=20=E8=A1=A5=E5=85=85=E7=94=A8=E6=88=B7=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E8=AE=A2=E9=98=85=E7=9A=84=E5=A4=84=E7=90=86=20-=20Pl?= =?UTF-8?q?uginMain=20=E5=90=AF=E7=94=A8=E8=AE=A2=E9=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/top/jie65535/jcf/MessageHandler.kt | 17 +- .../kotlin/top/jie65535/jcf/PluginMain.kt | 14 +- .../top/jie65535/jcf/SubscribeHandler.kt | 309 ++++++++++++++++++ 3 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt diff --git a/src/main/kotlin/top/jie65535/jcf/MessageHandler.kt b/src/main/kotlin/top/jie65535/jcf/MessageHandler.kt index 280bf47..05e7db3 100644 --- a/src/main/kotlin/top/jie65535/jcf/MessageHandler.kt +++ b/src/main/kotlin/top/jie65535/jcf/MessageHandler.kt @@ -1,7 +1,9 @@ package top.jie65535.jcf +import io.ktor.http.* import kotlinx.coroutines.* import net.mamoe.mirai.event.* +import net.mamoe.mirai.event.events.GroupMessageEvent import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.MessageSource.Key.quote @@ -125,7 +127,7 @@ class MessageHandler( subject.sendMessage( buildForwardMessage { // logo - mod.logo.thumbnailUrl?.let { + mod.logo?.thumbnailUrl?.let { if (it.isNotBlank()) bot says loadImage(it) } // basic info @@ -141,7 +143,7 @@ class MessageHandler( 主页:${mod.links.websiteUrl} """.trimIndent() }) - var msg = "$WAIT_REPLY_TIMEOUT_S 秒内回复 $SUBSCRIBE_KEYWORD 订阅模组更新(TODO)" + var msg = "$WAIT_REPLY_TIMEOUT_S 秒内回复 $SUBSCRIBE_KEYWORD 订阅模组更新" if (mod.latestFiles.isNotEmpty()) { msg += "\n回复编号查看文件详细信息\n" + "回复 $VIEW_FILES_KEYWORD 查看全部历史文件" @@ -163,9 +165,14 @@ class MessageHandler( eventChannel.nextEvent(EventPriority.MONITOR) { it.sender == sender } } val nextMessage = next.message.content + val subsHandler = PluginMain.subscribeHandler if (nextMessage.equals(SUBSCRIBE_KEYWORD, true)) { - // TODO 实现订阅模组更新功能 - subject.sendMessage("订阅更新功能暂未完成,敬请期待") + if (next is GroupMessageEvent) { + subsHandler.sub(mod.id, next.sender.id, next.group.id) + } else { + subsHandler.sub(mod.id, next.sender.id) + } + subject.sendMessage(QuoteReply(next.source) + "已添加订阅") } else if (mod.latestFiles.isNotEmpty()) { if (nextMessage.equals(VIEW_FILES_KEYWORD, true)) { // 查看所有文件 @@ -248,7 +255,7 @@ class MessageHandler( return image } - private val HTMLPattern = Pattern.compile("<[^>]+>", Pattern.CASE_INSENSITIVE) + val HTMLPattern: Pattern = Pattern.compile("<[^>]+>", Pattern.CASE_INSENSITIVE) fun MessageEvent.sendLargeMessage(message: String): Message { return buildForwardMessage { for (g in message.indices step ONE_GRP_SIZE) { diff --git a/src/main/kotlin/top/jie65535/jcf/PluginMain.kt b/src/main/kotlin/top/jie65535/jcf/PluginMain.kt index e859afa..ebe9a1f 100644 --- a/src/main/kotlin/top/jie65535/jcf/PluginMain.kt +++ b/src/main/kotlin/top/jie65535/jcf/PluginMain.kt @@ -1,5 +1,6 @@ package top.jie65535.jcf +import kotlinx.coroutines.launch import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin @@ -17,8 +18,14 @@ object PluginMain: KotlinPlugin( "https://github.com/jie65535/mirai-console-jcf-plugin") } ) { + /** + * 订阅处理类 + */ + lateinit var subscribeHandler: SubscribeHandler private set + override fun onEnable() { logger.info { "Plugin loaded" } + PluginData.reload() PluginConfig.reload() PluginCommands.register() @@ -31,7 +38,12 @@ object PluginMain: KotlinPlugin( val service = MinecraftService(PluginConfig.apiKey) val eventChannel = GlobalEventChannel.parentScope(this) val messageHandler = MessageHandler(service, eventChannel, logger) + subscribeHandler = SubscribeHandler(service, logger) messageHandler.startListen() + launch { + subscribeHandler.load(this) + } + subscribeHandler.start()// TODO 添加可切换闲置状态的命令 logger.info { "Plugin Enabled" } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt b/src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt new file mode 100644 index 0000000..f753020 --- /dev/null +++ b/src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt @@ -0,0 +1,309 @@ +package top.jie65535.jcf + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import net.mamoe.mirai.Bot +import net.mamoe.mirai.message.data.At +import net.mamoe.mirai.message.data.buildMessageChain +import net.mamoe.mirai.utils.MiraiLogger +import top.jie65535.jcf.model.mod.Mod + +/** + * 处理订阅 + * + * @param service api服务 + * @param logger 日志 + */ +class SubscribeHandler( + private val service: MinecraftService, + private val logger: MiraiLogger, +) { + + // region -- 参数 + + /** + * 清理订阅 + * + * @param mod 模组id;为null将清空所有订阅 + * @param group 群号;为null将移除指定mod下所有订阅 + */ + fun clean(mod: Int? = null, group: Long? = null) { + var updInner = false + val modSet = HashMap(PluginData.modsLastFile) + val subSet = HashMap(PluginData.subscriptionSet) + if (mod == null) { + subSet.clear() + modSet.clear() + logger.info("清理所有订阅") + } else if (group == null) { + modSet -= mod + subSet -= mod + logger.info("清理mod[${MOD_INFO_CACHE[mod]?.name}]的订阅") + } else { + subSet[mod]?.let { + logger.info("清理群/分组[$group]的订阅") + updInner = true + it -= group + } + } + + if (modSet.size != PluginData.modsLastFile.size) { + PluginData.modsLastFile = modSet + } + if (subSet.size != PluginData.subscriptionSet.size || updInner) { + PluginData.subscriptionSet = subSet + } + } + + /** + * 取消订阅 + * + * @param mod 模组id + * @param qq 个人q号或群成员q号 + * @param group 群号;为null时(默认)表示个人订阅 + */ + fun unsub(mod: Int, qq: Long, group: Long? = null) { + if (mod < 0 || qq < 0) return + + val gid = group ?: GROUP_ID_SINGLE + val subSet = HashMap(PluginData.subscriptionSet) + + val groups = subSet[mod] ?: return + val members = groups[gid] ?: return + members -= qq + PluginData.subscriptionSet = subSet + logger.info("取消订阅--{$mod:{$gid:[$qq]}}") + } + + /** + * 记录订阅 + * + * @param mod 模组id + * @param qq q号 + * @param group 群号;为null时(默认)表示个人订阅 + */ + fun sub(mod: Int, qq: Long, group: Long? = null) { + if (mod < 0 || qq < 0) return + val gid = group ?: GROUP_ID_SINGLE + val modSet = HashMap(PluginData.modsLastFile) + val subSet = HashMap(PluginData.subscriptionSet) + if (mod !in modSet) modSet[mod] = -1 + + val groupSet = subSet[mod] ?: mutableMapOf() + val qqSet = groupSet[gid] ?: mutableListOf() + var changed = gid !in groupSet + subSet[mod] = groupSet + groupSet[gid] = qqSet + if (qq !in qqSet) { + qqSet += qq + changed = true + logger.info("添加订阅--{$mod:{$gid:[$qq]}}") + } + + if (mod !in PluginData.modsLastFile) { + PluginData.modsLastFile = modSet + } + if (changed) { + PluginData.subscriptionSet = subSet + } + } + // endregion + + // region -- 流程 + + /** + * 检查更新 + * + * @param init 是否初始化 + * @return 检查到更新的{ mod : newFileId } + */ + private suspend fun checkUpdate(init: Boolean = false) = flow { + val oldSet = PluginData.modsLastFile + if (oldSet.isNotEmpty()) { + for ((mod, old) in oldSet) { + try { + val info = service.getMod(mod) + logger.info("模组更新【${info.name}】") + MOD_INFO_CACHE[mod] = info + val last = info.latestFilesIndexes[0].fileId + if (old != last || init) { + emit(mod to last) + } + } catch (e: Exception) { + logger.warning("err msg: ${e.message}") + emit(mod to -1) + continue + } + }// for + } + } + + /** + * 获取更新日志 + * + * @param mod 模组id + * @param file 文件id + * @return 更新日志 + */ + private suspend fun getChangeLogs(mod: Int, file: Int): String = try { + val changelog = service.getModFileChangelog(mod, file) + MessageHandler.HTMLPattern.matcher(changelog) + .replaceAll("") + .replace(Regex("\n+"), "\n") + } catch (e: Exception) { + logger.warning("err msg: ${e.message}") + "" + } + + /** + * 执行发送 + * + * @param sender 发送消息的bot + * @param modLogs { mod : changeLog } + */ + private suspend fun send(sender: Bot, modLogs: Pair) { + val (mod, logs) = modLogs + if (logs.isBlank()) return + + val subGroups = PluginData.subscriptionSet[mod] ?: return + val modName = MOD_INFO_CACHE[mod]?.name ?: return + val title = "你订阅的mod【$modName】更新啦!" + val context = "更新日志:\n$logs" + subGroups.forEach { (group, qqs) -> + if (group == GROUP_ID_SINGLE) { + qqs.forEach { + sender.getFriend(it)?.apply { + sendMessage(title) + sendMessage(context) + } + } + } else { + sender.getGroup(group)?.apply { + val titleChain = buildMessageChain { + qqs.forEach { +At(it) } + +"\n$title" + } + sendMessage(titleChain) + sendMessage(context) + } + }// if else + }// foreach + } + + /** + * 准备发送更新日志 + * + * @param senderQQ 指定发送消息的机器人id + * @param updMod { mod : file } + */ + private suspend fun feedback(senderQQ: Long, updMod: Pair) { + val (mod, file) = updMod + if (mod < 0) return + + Bot.instances.firstOrNull { + it.isOnline && it.id == senderQQ + }?.let { sender -> + val log = getChangeLogs(mod, file) + send(sender, mod to log) + }// let + } + + /** + * 循环执行 + */ + private fun CoroutineScope.loop() = launch { + val senderQQ = PluginConfig.subscribeSender + val interval = PluginConfig.checkInterval + if (senderQQ < 0) { + logger.warning("必须配置订阅信息推送bot(qq id)才可以进行订阅推送!") + logger.warning("插件会持续收集订阅与检查mod更新,但无法进行消息推送。") + } + logger.info("subscription listening") + while (true) { + delay(1000 * interval) + if (isIdle) continue + + val subSet = HashMap(PluginData.subscriptionSet) + val modFiles = HashMap(PluginData.modsLastFile) + checkUpdate() + .buffer() + .collect { + val (mod, file) = it + if (file < 0) { + modFiles -= mod + subSet -= mod + } else { + modFiles[mod] = file + feedback(senderQQ, it) + } + } + PluginData.subscriptionSet = subSet + PluginData.modsLastFile = modFiles + }// while + } + + // endregion + + // region -- 状态 + + /** + * 是否闲置 + */ + var isIdle = true + private set + + /** + * 取消闲置 + */ + fun start() { + isIdle = false + } + + /** + * 进入闲置 + */ + fun idle() { + isIdle = true + } + + /** + * 初始化,并开始订阅循环 + * + * @param scope 指定协程上下文 + */ + suspend fun load(scope: CoroutineScope) { + logger.info("loading plugin data...") + val subs = HashMap(PluginData.subscriptionSet) + val files = HashMap(PluginData.modsLastFile) + checkUpdate(true) + .buffer() + .collect { (mod, file) -> + if (file < 0) { + files -= mod + subs -= mod + } else { + files[mod] = file + } + } + PluginData.subscriptionSet = subs + PluginData.modsLastFile = files + scope.loop() + } + // endregion + + companion object { + + /** + * 标识个人订阅 + */ + const val GROUP_ID_SINGLE: Long = 0 + + /** + * 缓存模组信息 + */ + private val MOD_INFO_CACHE: MutableMap = mutableMapOf() + } +}