You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
qqbot/src/main/kotlin/com/pqh/qqbot/TestQQBot.kt

303 lines
11 KiB

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