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