package com.pqh.qqbot 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.* import net.mamoe.mirai.message.data.* 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 import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component import org.springframework.stereotype.Service import java.io.File import java.net.URL import java.util.* import kotlin.collections.HashMap const val mongoDBName: String = "MongoDB" class CustomLogger(val logger: Logger, override val identity: String?) : MiraiLoggerPlatformBase() { override fun debug0(message: String?, e: Throwable?) { val s = Thread.currentThread().getStackTrace() logger.debug("(${s.get(4)})-${message}", e) } override fun error0(message: String?, e: Throwable?) { val s = Thread.currentThread().getStackTrace() logger.error("(${s.get(4)})-${message}", e) } override fun info0(message: String?, e: Throwable?) { val s = Thread.currentThread().getStackTrace() logger.info("(${s.get(4)})-${message}", e) } override fun verbose0(message: String?, e: Throwable?) { val s = Thread.currentThread().getStackTrace() logger.trace("(${s.get(4)})-${message}", e) } override fun warning0(message: String?, e: Throwable?) { val s = Thread.currentThread().getStackTrace() logger.warn("(${s.get(4)})-${message}", e) } } @Service class TestQQBot { val logger = LoggerFactory.getLogger(TestQQBot::class.java) /** * 程序入口 */ @Autowired @Qualifier(mongoDBName) lateinit var db: DB //消息缓存,引用回复 val map = HashMap() data class Account(val qqId: Long, val adminQQId: Long, val pwd: String, val forward: Long) //管理员操作指令 enum class AdminCommand { //关闭机器人 CLOSE } //转发信息卡片标题 class ForwardTitle(val title: String) : ForwardMessage.DisplayStrategy() { override fun generateTitle(forward: ForwardMessage): String = title } /** * 初始化QQ机器人 */ @Async fun initBot(account: Account) { GlobalScope.launch { val config = BotConfiguration.Default config.fileBasedDeviceInfo("qqbot.json") 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) 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 } } }.let { if (it.isContentNotEmpty()) { it.sendTo(messageEvent.sender) } } } } //处理非管理员的私聊信息或者临时会话 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 { bot.logger.info("信息为空不转发") } } } } catch (e: Exception) { logger.error("初始化机器人发生异常信息:${e}") } } } } /** * 保存QQ消息图片 */ @Component class SaveImg { val logger = LoggerFactory.getLogger(SaveImg::class.java) @Value("\${qq-image}") lateinit var staticLocations: String // 截取url作为文件名 val regexFileName = Regex("\\d+-\\d+-[0-9A-Z]+") //保存图片 @KtorExperimentalAPI fun saveImage(logger: MiraiLogger, url: String): String? { val u = URL(url) try { val content = u.readBytes() val dir = File(staticLocations) val fileName: String? if (dir.exists() || dir.mkdirs()) { val f = regexFileName.find(url)?.value fileName = f ?: UUID.randomUUID().toString() val filepath = File(dir, fileName).absolutePath.plus(checkFileType(content)) File(filepath).writeBytes(content) logger.debug("图片保存到本地目录${filepath}") } else { fileName = null 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: TestQQBot.Account) } /** * mongoDB保存QQ消息 */ @Component(mongoDBName) class MongoDB : DB { @Autowired lateinit var messageRepository: MessageRepository @Autowired lateinit var subRepository: MessageSubRepository @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 } } is PlainText -> { MySqlSubMessage(m.id, it.content, PlainText.Key.typeName) } else -> { logger.debug("消息类型:${it.javaClass}不处理") null } } if (subMessage != null) { subRepository.save(subMessage) } } } } }