From 153f38cf10a7a1ff3c35e0c80373b17aaa07373a Mon Sep 17 00:00:00 2001 From: pan <1029559041@qq.com> Date: Fri, 28 Aug 2020 18:46:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B6=88=E6=81=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- .../kotlin/com/pqh/qqbot/TestController.kt | 2 - src/main/kotlin/com/pqh/qqbot/TestQQBot.kt | 308 ++++++++++-------- src/main/resources/application.yaml | 5 - src/main/resources/logback-spring.xml | 41 ++- src/test/kotlin/com/pqh/qqbot/AppTest.kt | 19 ++ .../com/pqh/qqbot/QqbotApplicationTests.kt | 9 +- 7 files changed, 220 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index b6f2cda..4c9c56a 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,10 @@ mirai 既可以作为项目中的 QQ 协议支持库, 也可以作为单独的 订阅指令:群用户向机器人订阅消息的请求指令。 一条合法的指令由 `>>>`+`一级指令`+`二级指令`+`占位符`+`参数`组成,比如查询pid(P站图片id)的指令是`>>>query pixiv -p 80353815` - 模块:指令集调用的处理器,应用依赖的核心逻辑单元,称之为模块。模块本身不依赖于QQ BOT运行环境,是可独立运行的应用 + 模块:指令集调用的处理器,应用依赖的核心逻辑单元,称之为模块。模块本身不依赖于QQ BOT运行环境,是可独立运行的应用。 目前计划支持`kotlin`和`java`开发的模块。模块内部**封装**了群用户查询消息的**处理逻辑**,这里的处理逻辑实际上是通过`HTTP/HTTPS`协议向服务端爬取消息,进行解析并返回的过程,所以模块暂统称为`爬虫模块`,每个爬虫模块必须提供**输入**和**输出**,输入指解析QQ BOT发送的指令,而响应给QQ BOT的处理结果就是输出。 - RSSHub:一个开源、简单易用、易于扩展的 RSS 生成器,可以给任何奇奇怪怪的内容生成 RSS 订阅源。RSSHub 借助于开源社区的力量快速发展中,目前已适配数百家网站的上千项内容 + + RSSHub:一个开源、简单易用、易于扩展的 RSS 生成器,可以给任何奇奇怪怪的内容生成 RSS 订阅源。RSSHub 借助于开源社区的力量快速发展中,目前已适配数百家网站的上千项内容。 * ### 查询指令速查表: diff --git a/src/main/kotlin/com/pqh/qqbot/TestController.kt b/src/main/kotlin/com/pqh/qqbot/TestController.kt index 91092bf..7de16db 100644 --- a/src/main/kotlin/com/pqh/qqbot/TestController.kt +++ b/src/main/kotlin/com/pqh/qqbot/TestController.kt @@ -3,8 +3,6 @@ package com.pqh.qqbot import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController diff --git a/src/main/kotlin/com/pqh/qqbot/TestQQBot.kt b/src/main/kotlin/com/pqh/qqbot/TestQQBot.kt index d8b9d34..a8dec1c 100644 --- a/src/main/kotlin/com/pqh/qqbot/TestQQBot.kt +++ b/src/main/kotlin/com/pqh/qqbot/TestQQBot.kt @@ -1,18 +1,17 @@ package com.pqh.qqbot -import io.ktor.util.KtorExperimentalAPI -import io.ktor.util.hex -import kotlinx.coroutines.* +import io.ktor.util.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import net.mamoe.mirai.Bot import net.mamoe.mirai.alsoLogin import net.mamoe.mirai.closeAndJoin import net.mamoe.mirai.event.subscribeMessages -import net.mamoe.mirai.message.GroupMessageEvent -import net.mamoe.mirai.message.MessageEvent +import net.mamoe.mirai.message.* import net.mamoe.mirai.message.data.* -import net.mamoe.mirai.message.sourceId -import net.mamoe.mirai.message.sourceTime -import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.BotConfiguration +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.MiraiLoggerPlatformBase import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -21,45 +20,48 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component import org.springframework.stereotype.Service -import org.springframework.util.ResourceUtils import java.io.File import java.net.URL -import java.time.Duration import java.util.* -import java.util.concurrent.TimeUnit -import javax.annotation.PostConstruct - +import kotlin.collections.HashMap const val mongoDBName: String = "MongoDB" -class CustomLogger(val logger: Logger,override val identity: String?) :MiraiLoggerPlatformBase(){ +class CustomLogger(val logger: Logger, override val identity: String?) : MiraiLoggerPlatformBase() { override fun debug0(message: String?, e: Throwable?) { - logger.debug(message,e) + val s = Thread.currentThread().getStackTrace() + logger.debug("(${s.get(4)})-${message}", e) } override fun error0(message: String?, e: Throwable?) { - logger.error(message,e) + val s = Thread.currentThread().getStackTrace() + logger.error("(${s.get(4)})-${message}", e) } override fun info0(message: String?, e: Throwable?) { - logger.info(message,e) + val s = Thread.currentThread().getStackTrace() + logger.info("(${s.get(4)})-${message}", e) } override fun verbose0(message: String?, e: Throwable?) { - logger.trace(message,e) + val s = Thread.currentThread().getStackTrace() + logger.trace("(${s.get(4)})-${message}", e) } override fun warning0(message: String?, e: Throwable?) { - logger.warn(message,e) + val s = Thread.currentThread().getStackTrace() + logger.warn("(${s.get(4)})-${message}", e) } } + @Service class TestQQBot { - val logger=LoggerFactory.getLogger(TestQQBot::class.java) + val logger = LoggerFactory.getLogger(TestQQBot::class.java) + /** * 程序入口 */ @@ -68,7 +70,8 @@ class TestQQBot { @Qualifier(mongoDBName) lateinit var db: DB - val maxCount = 3 + //消息缓存,引用回复 + val map = HashMap() data class Account(val qqId: Long, val adminQQId: Long, val pwd: String, val forward: Long) @@ -92,85 +95,109 @@ class TestQQBot { GlobalScope.launch { val config = BotConfiguration.Default config.fileBasedDeviceInfo("qqbot.json") - val bot = Bot(account.qqId, account.pwd, config) - bot.logger.follower=CustomLogger(logger,"Bot ${bot.id}") - bot.logger.info("日志log") - bot.alsoLogin() - - val group = bot.getGroup(account.forward) - - bot.subscribeMessages { - sentBy(account.adminQQId) { - case(AdminCommand.CLOSE.name) { - bot.logger.info("执行关闭机器人指令") - bot.closeAndJoin() + config.botLoggerSupplier = { CustomLogger(logger, "Bot ${it.id}") } + config.networkLoggerSupplier = { CustomLogger(logger, "Net ${it.id}") } + try { + val bot = Bot(account.qqId, account.pwd, config) + bot.alsoLogin() + + val group = bot.getGroup(account.forward) + val admin = bot.getFriend(account.adminQQId) + bot.subscribeMessages { + sentBy(account.adminQQId) { + case(AdminCommand.CLOSE.name) { + bot.logger.info("执行关闭机器人指令") + bot.closeAndJoin() + } } - } - always { - if(message.isContentNotEmpty()) { - db.saveToDB(this, account) - if (this is GroupMessageEvent&&sender.group!=group) { - GlobalScope.launch { - var title = sender.group.name - message.forEach { - if (it.isPlain()) { - if (it.content.length > 5) { - title = it.content.substring(0, 5) + always { + if (message.isContentNotEmpty()) { + db.saveToDB(this, account) + bot.logger.info("消息id:${message.id}:${message.internalId}") + //处理除指定群以外的群消息 + if (this is GroupMessageEvent && sender.group != group) { + val s = group.sendMessage(PlainText("转发${this.group.name}的${senderName}发送的消息\n") + message) + logger.info("转发消息id:${s.sourceId}:${s.sourceInternalId}") + map.put("${s.sourceId}:${s.sourceInternalId}", this) + } + //处理指定群消息 + else if (this is GroupMessageEvent && sender.group.id == group.id) { + val quoteReply = message.get(QuoteReply.Key) + val key = "${quoteReply?.source?.id}:${quoteReply?.source?.internalId}" + if (quoteReply != null && map.containsKey(key)) { + val messageEvent = map.get(key)!! + buildMessageChain { + message.forEach { + if (!(it is QuoteReply || it is At)) { + +it + } } - return@forEach - } - } - - var count = 0 - while (count++ < maxCount) { - try { - val msg=buildForwardMessage(displayStrategy = ForwardTitle(title)) { - sender.id named senderName at time says message + }.let { + if (it.isContentNotEmpty()) { + it.sendTo(messageEvent.sender) } - msg.sendTo(group) - break - } catch (e: IllegalStateException) { - bot.logger.error("机器人转发信息发生异常:${e}\n再尝试发送一次") - delay(Duration.ofSeconds(3).toMillis()) } } + } + //处理非管理员的私聊信息或者临时会话 + else if (this is FriendMessageEvent && this.sender.id != admin.id || this is TempMessageEvent) { + admin.sendMessage(buildMessageChain { + +PlainText("来自QQ(${sender.id})[${senderName}]发送的信息:") + }.plus(message)) + } + //处理来自管理员私聊的信息 + else if (this is FriendMessageEvent && this.sender.id == admin.id) { + val targetMsg = message.get(PlainText.Key) + val split = "@" + if (targetMsg != null && targetMsg.isContentNotEmpty() && targetMsg.content.split("\r")[0].matches(Regex("\\d+${split}\\d+"))) { + val s = targetMsg.content.split("\r")[0].split(split) + bot.getGroup(s[0].toLong()).get(s[1].toLong()).sendMessage( + buildMessageChain { + message.filterIndexed { index, i -> + index > 2 + }.forEach { + +it + } + } + ) + } } - }else if(this is GroupMessageEvent&&sender.group==group){ - bot.logger.info("不能套娃") - }else{ - bot.logger.info("${this.javaClass}类型不转发") + } else { + bot.logger.info("信息为空不转发") } - }else{ - bot.logger.info("信息为空不转发") } } + } catch (e: Exception) { + logger.error("初始化机器人发生异常信息:${e}") } } + } - } +} - /** - * 保存QQ消息图片 - */ - @Component - class SaveImg { - val logger=LoggerFactory.getLogger(SaveImg::class.java) +/** + * 保存QQ消息图片 + */ +@Component +class SaveImg { + val logger = LoggerFactory.getLogger(SaveImg::class.java) - @Value("\${qq-image}") - lateinit var staticLocations: String + @Value("\${qq-image}") + lateinit var staticLocations: String - // 截取url作为文件名 - val regexFileName = Regex("\\d+-\\d+-[0-9A-Z]+") + // 截取url作为文件名 + val regexFileName = Regex("\\d+-\\d+-[0-9A-Z]+") - //保存图片 - @KtorExperimentalAPI - fun saveImage(logger: MiraiLogger, url: String): String? { - val u = URL(url) + //保存图片 + @KtorExperimentalAPI + fun saveImage(logger: MiraiLogger, url: String): String? { + val u = URL(url) + try { val content = u.readBytes() val dir = File(staticLocations) val fileName: String? @@ -185,82 +212,86 @@ class TestQQBot { logger.debug("图片保存失败") } return fileName + } catch (e: Exception) { + logger.info("保存图片发生异常信息:${e}") + return null } - //获取图片类型 - @KtorExperimentalAPI - fun checkFileType(bytes: ByteArray): String { - - return when { - "FFD8FF" == hex(bytes.copyOfRange(0, 3)).toUpperCase() -> ".jpg" - "89504E47" == hex(bytes.copyOfRange(0, 4)).toUpperCase() -> ".png" - "47494638" == hex(bytes.copyOfRange(0, 4)).toUpperCase() -> ".gif" - else -> { - logger.info("无法识别文件头:${hex(bytes.copyOfRange(0, 10))}") - "" - } - } - } } - /** - * 存储QQ消息到数据库 - */ - interface DB { - suspend fun saveToDB(event: MessageEvent, account: Account) + //获取图片类型 + @KtorExperimentalAPI + fun checkFileType(bytes: ByteArray): String { + + return when { + "FFD8FF" == hex(bytes.copyOfRange(0, 3)).toUpperCase() -> ".jpg" + "89504E47" == hex(bytes.copyOfRange(0, 4)).toUpperCase() -> ".png" + "47494638" == hex(bytes.copyOfRange(0, 4)).toUpperCase() -> ".gif" + else -> { + logger.info("无法识别文件头:${hex(bytes.copyOfRange(0, 10))}") + "" + } + } } +} - /** - * mongoDB保存QQ消息 - */ - @Component(mongoDBName) - class MongoDB : DB { - - @Autowired - lateinit var messageRepository: MessageRepository - - @Autowired - lateinit var subRepository: MessageSubRepository - - @Autowired - lateinit var saveImg: SaveImg +/** + * 存储QQ消息到数据库 + */ +interface DB { + suspend fun saveToDB(event: MessageEvent, account: TestQQBot.Account) +} - @KtorExperimentalAPI - override suspend fun saveToDB(event: MessageEvent, account: Account) { +/** + * mongoDB保存QQ消息 + */ +@Component(mongoDBName) +class MongoDB : DB { - val m = messageRepository.save(MySqlMessage(senderId = event.sender.id, time = event.time, botId = event.bot.id)) + @Autowired + lateinit var messageRepository: MessageRepository - val logger = event.bot.logger - if (m.id != null) { - event.message.forEach { - val subMessage = when (it) { - is Image -> { - logger.debug("接收图片,查询下载链接") - val imageUrl = it.queryUrl() - logger.debug("图片下载链接:${imageUrl}") + @Autowired + lateinit var subRepository: MessageSubRepository - val path = saveImg.saveImage(event.bot.logger, imageUrl) - if (path != null) { - MySqlSubMessage(m.id, path, Image.Key.typeName) - } else { - null - } - } - is PlainText -> { - MySqlSubMessage(m.id, it.content, PlainText.Key.typeName) - } - else -> { - logger.debug("消息类型:${it.javaClass}不处理") + @Autowired + lateinit var saveImg: SaveImg + + @KtorExperimentalAPI + override suspend fun saveToDB(event: MessageEvent, account: TestQQBot.Account) { + + val m = messageRepository.save(MySqlMessage(senderId = event.sender.id, time = event.time, botId = event.bot.id)) + + val logger = event.bot.logger + if (m.id != null) { + event.message.forEach { + val subMessage = when (it) { + is Image -> { + logger.debug("接收图片,查询下载链接") + val imageUrl = it.queryUrl() + logger.debug("图片下载链接:${imageUrl}") + + val path = saveImg.saveImage(event.bot.logger, imageUrl) + if (path != null) { + MySqlSubMessage(m.id, path, Image.Key.typeName) + } else { null } } - if (subMessage != null) { - subRepository.save(subMessage) + is PlainText -> { + MySqlSubMessage(m.id, it.content, PlainText.Key.typeName) } + else -> { + logger.debug("消息类型:${it.javaClass}不处理") + null + } + } + if (subMessage != null) { + subRepository.save(subMessage) } } - } + } } @@ -269,3 +300,4 @@ class TestQQBot { + diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 59ec038..c80c1ba 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,8 +1,3 @@ -#日志配置 -logging: - level: - root: info - spring: #数据库配置 data: diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index fe5f2f9..9f68d05 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -2,17 +2,10 @@ - + - + - - - - - - - @@ -25,7 +18,7 @@ - ${DEBUG_LOG_FILE}.%d{yyyy-MM-dd}.log + ${DEBUG_LOG_FILE}_debug.%d{yyyy-MM-dd}.log 60 @@ -34,20 +27,38 @@ + + + ${FILE_LOG_PATTERN} + + + + ${DEBUG_LOG_FILE}_trace.%d{yyyy-MM-dd}.log + 60 + + + + TRACE + + + - + ${CONSOLE_LOG_PATTERN} - DEBUG + INFO - - - + + + + + + diff --git a/src/test/kotlin/com/pqh/qqbot/AppTest.kt b/src/test/kotlin/com/pqh/qqbot/AppTest.kt index 3cecba0..b7310e2 100644 --- a/src/test/kotlin/com/pqh/qqbot/AppTest.kt +++ b/src/test/kotlin/com/pqh/qqbot/AppTest.kt @@ -1,5 +1,9 @@ package com.pqh.qqbot +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.mamoe.mirai.Bot +import net.mamoe.mirai.utils.BotConfiguration import org.junit.jupiter.api.Test class AppTest { @@ -19,5 +23,20 @@ class AppTest { } } +} +suspend fun main(arg: Array) { + println("参数长度:${arg.size}") + if (arg.size == 2) { + println("请输入账号") + val id = arg[0].toLong() + println("请输入密码") + val pwd = arg[1] + println("账号:${id},密码:${pwd}") + val config = BotConfiguration.Default + config.fileBasedDeviceInfo("qqbot.json") + GlobalScope.launch { + Bot(id, pwd, config).login() + }.join() + } } \ No newline at end of file diff --git a/src/test/kotlin/com/pqh/qqbot/QqbotApplicationTests.kt b/src/test/kotlin/com/pqh/qqbot/QqbotApplicationTests.kt index c805394..2617b33 100644 --- a/src/test/kotlin/com/pqh/qqbot/QqbotApplicationTests.kt +++ b/src/test/kotlin/com/pqh/qqbot/QqbotApplicationTests.kt @@ -11,18 +11,11 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.test.web.client.getForEntity import org.springframework.http.HttpMethod -import org.springframework.http.MediaType -import org.springframework.http.RequestEntity.get -import org.springframework.http.RequestEntity.post -import org.springframework.http.ResponseEntity.status -import org.springframework.test.context.junit4.SpringRunner import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.util.ResourceUtils import java.io.File -import java.net.URI import java.nio.charset.StandardCharsets @@ -76,6 +69,8 @@ class QqbotApplicationTests(@Autowired val restTemplate: TestRestTemplate) { @Test fun testLog(){ logger.info("日志log") + logger.debug("日志log") + logger.trace("日志log") } }