更新消息处理逻辑

master
pan 4 years ago
parent 6091c574f4
commit 153f38cf10
  1. 5
      README.md
  2. 2
      src/main/kotlin/com/pqh/qqbot/TestController.kt
  3. 308
      src/main/kotlin/com/pqh/qqbot/TestQQBot.kt
  4. 5
      src/main/resources/application.yaml
  5. 41
      src/main/resources/logback-spring.xml
  6. 19
      src/test/kotlin/com/pqh/qqbot/AppTest.kt
  7. 9
      src/test/kotlin/com/pqh/qqbot/QqbotApplicationTests.kt

@ -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 借助于开源社区的力量快速发展中,目前已适配数百家网站的上千项内容。
* ### 查询指令速查表:

@ -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

@ -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<String, GroupMessageEvent>()
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 {

@ -1,8 +1,3 @@
#日志配置
logging:
level:
root: info
spring:
#数据库配置
data:

@ -2,17 +2,10 @@
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="APP_NAME" value="Logback"/>
<property name="APP_NAME" value="qqbot"/>
<property name="LOG_HOME_PATH" value="logs"/>
<property name="DEBUG_LOG_FILE" value="${LOG_HOME_PATH}/debug/${APP_NAME}_debug" />
<property name="DEBUG_LOG_FILE" value="${LOG_HOME_PATH}/debug/${APP_NAME}"/>
<property name="log.charset" value="UTF-8" />
<!-- 彩色日志 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } - [%t] %class:%L - %m%n" />
@ -25,7 +18,7 @@
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${DEBUG_LOG_FILE}.%d{yyyy-MM-dd}.log</FileNamePattern>
<FileNamePattern>${DEBUG_LOG_FILE}_debug.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>60</MaxHistory>
</rollingPolicy>
@ -34,20 +27,38 @@
</filter>
</appender>
<appender name="QQBOT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder charset="${log.charset}">
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${DEBUG_LOG_FILE}_trace.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>60</MaxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>TRACE</level>
</filter>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- <withJansi>true</withJansi>-->
<!-- <withJansi>true</withJansi>-->
<encoder charset="${log.charset}">
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
<level>INFO</level>
</filter>
</appender>
<root level="DEBUG">
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="CONSOLE" />
<root level="TRACE">
<appender-ref ref="QQBOT_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

@ -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<String>) {
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()
}
}

@ -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")
}
}

Loading…
Cancel
Save