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.
303 lines
11 KiB
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|