Shamrock: support /send_guild_channel_msg

This commit is contained in:
白池 2024-02-02 22:47:38 +08:00
parent 137c354acc
commit db252b6b6c
13 changed files with 240 additions and 55 deletions

View File

@ -61,7 +61,7 @@ internal object MsgSvc: BaseSvc() {
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
val peerId = mapping.peerId
val contact = MessageHelper.generateContact(mapping.chatType, peerId)
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId ?: "")
val msg = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
@ -89,11 +89,11 @@ internal object MsgSvc: BaseSvc() {
suspend fun getMsgByQMsgId(
chatType: Int,
peerId: String,
qqMsgId: Long
qqMsgId: Long,
subPeerId: String = ""
): Result<MsgRecord> {
val contact = MessageHelper.generateContact(chatType, peerId)
val service = QRoute.api(IMsgService::class.java) ?:
return Result.failure(Exception("获取消息服务"))
val contact = MessageHelper.generateContact(chatType, peerId, subPeerId)
val service = QRoute.api(IMsgService::class.java)
val msg = withTimeoutOrNull(5000) {
suspendCoroutine { continuation ->
@ -152,7 +152,7 @@ internal object MsgSvc: BaseSvc() {
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return -1 to "无法找到消息映射"
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId)
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId ?: "")
return suspendCancellableCoroutine { continuation ->
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
@ -184,7 +184,7 @@ internal object MsgSvc: BaseSvc() {
}
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
result.onFailure {
LogCenter.log(it.stackTraceToString(), Level.ERROR)
LogCenter.log("sendToAio: " + it.stackTraceToString(), Level.ERROR)
return result
}
val sendResult = result.getOrThrow()
@ -192,7 +192,7 @@ internal object MsgSvc: BaseSvc() {
// 发送失败,可能网络问题出现红色感叹号,重试
// 例如 rich media transfer failed
delay(100)
MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, 3, sendResult.msgHashId)
MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId)
} else {
result
}

View File

@ -107,12 +107,7 @@ internal object MessageHelper {
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
suspendCancellableCoroutine {
GlobalScope.launch {
sendResult = sendMessageWithoutMsgId(
chatType,
peerId,
msg,
fromId
) { code, message ->
sendResult = sendMessageWithoutMsgId(chatType, peerId, msg, fromId) { code, message ->
callback.onResult(code, message)
it.resume(code to message)
}
@ -248,10 +243,17 @@ internal object MessageHelper {
}
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
val peerId = when(chatType) {
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
ContactHelper.getUidByUinAsync(id.toLong())
} else id
return Contact(chatType, peerId, subId)
}
else -> id
}
return if (chatType == MsgConstant.KCHATTYPEGUILD) {
Contact(chatType, subId, peerId)
} else {
Contact(chatType, peerId, subId)
}
}
fun obtainMessageTypeByDetailType(detailType: String): Int {
@ -342,11 +344,12 @@ internal object MessageHelper {
time: Long,
chatType: Int,
peerId: String,
subPeerId: String,
msgSeq: Int,
subChatType: Int = chatType
) {
val database = MessageDB.getInstance()
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq)
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
database.messageMappingDao().insert(mapping)
}

View File

@ -12,7 +12,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import mqq.app.MobileQQ
@Entity(tableName = "message_mapping")
@Entity(tableName = "message_mapping_v2")
data class MessageMapping (
@PrimaryKey
val msgHashId: Int,
@ -21,7 +21,8 @@ data class MessageMapping (
val subChatType: Int, // 细化各种事件消息
val peerId: String,
val time: Long,
val msgSeq: Int
val msgSeq: Int,
val subPeerId: String,
)
@Dao
@ -29,25 +30,25 @@ interface MessageMappingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(mapping: MessageMapping)
@Query("UPDATE message_mapping SET msgSeq = :msgSeq WHERE msgHashId = :hash")
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)
@Query("DELETE FROM message_mapping WHERE msgHashId = :hash")
@Query("DELETE FROM message_mapping_v2 WHERE msgHashId = :hash")
fun deleteByMsgHash(hash: Int)
@Query("SELECT * FROM message_mapping WHERE msgHashId = :msgHashId")
@Query("SELECT * FROM message_mapping_v2 WHERE msgHashId = :msgHashId")
fun queryByMsgHashId(msgHashId: Int): MessageMapping?
@Query("SELECT * FROM message_mapping WHERE qqMsgId = :qqMsgId AND chatType = :chatType")
@Query("SELECT * FROM message_mapping_v2 WHERE qqMsgId = :qqMsgId AND chatType = :chatType")
fun queryByQqMsgId(chatType: Int, qqMsgId: Long): MessageMapping?
@Query("SELECT * FROM message_mapping WHERE chatType = :chatType")
@Query("SELECT * FROM message_mapping_v2 WHERE chatType = :chatType")
fun queryByChatType(chatType: Int): List<MessageMapping>
@Query("SELECT * FROM message_mapping WHERE subChatType = :subChatType AND chatType = :chatType")
@Query("SELECT * FROM message_mapping_v2 WHERE subChatType = :subChatType AND chatType = :chatType")
fun queryBySubChatType(chatType: Int, subChatType: Int): List<MessageMapping>
@Query("SELECT * FROM message_mapping WHERE peerId = :peerId AND chatType = :chatType")
@Query("SELECT * FROM message_mapping_v2 WHERE peerId = :peerId AND chatType = :chatType")
fun queryByPeerId(chatType: Int, peerId: String): List<MessageMapping>
//@Query("SELECT * FROM message_mapping WHERE msgSeq = :msgSeq AND chatType = :chatType")
@ -55,7 +56,7 @@ interface MessageMappingDao {
// 不要调用这个seq不唯一啊老哥
// 我就说怎么这么多bug
@Query("SELECT * FROM message_mapping WHERE msgSeq = :msgSeq AND chatType = :chatType AND peerId = :peerId")
@Query("SELECT * FROM message_mapping_v2 WHERE msgSeq = :msgSeq AND chatType = :chatType AND peerId = :peerId")
fun queryByMsgSeq(chatType: Int, peerId: String, msgSeq: Int): MessageMapping?
}
@ -64,7 +65,7 @@ internal abstract class MessageDB: RoomDatabase() {
abstract fun messageMappingDao(): MessageMappingDao
companion object {
private const val DB_NAME = "message_mapping.db"
private const val DB_NAME = "message_mapping_v2.db"
@Volatile
private var instance: MessageDB? = null

View File

@ -41,7 +41,7 @@ internal object GetForwardMsg: IActionHandler() {
msg.senderUin, msg.sendNickName
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -65,7 +65,7 @@ internal object GetHistoryMsg: IActionHandler() {
msgId = msgHash,
realId = msg.msgSeq.toInt(),
sender = MessageSender(
msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid
msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()
@ -89,7 +89,7 @@ internal object GetHistoryMsg: IActionHandler() {
msg.senderUin, msg.sendNickName
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -34,7 +34,10 @@ internal object GetMsg: IActionHandler() {
msg.senderUin, msg.sendNickName
.ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid
.ifEmpty { msg.peerName }, "unknown",
0,
msg.senderUid,
msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()

View File

@ -0,0 +1,112 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonArray
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.MessageResult
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("send_guild_message", ["send_guild_msg", "send_guild_channel_msg"])
internal object SendGuildMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val guildId = session.getString("guild_id").toULong()
val channelId = session.getString("channel_id").toULong()
val retryCnt = session.getIntOrNull("retry_cnt") ?: 3
val recallDuration = session.getLongOrNull("recall_duration")
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
return invoke(guildId, channelId, message, autoEscape, retryCnt, recallDuration, echo = session.echo)
} else if (session.isArray("message")) {
val message = session.getArray("message")
return invoke(guildId, channelId, message, echo = session.echo, retryCnt = retryCnt, recallDuration = recallDuration)
} else {
val message = session.getObject("message")
invoke(guildId, channelId, listOf(message).jsonArray, session.echo, retryCnt, recallDuration = recallDuration)
}
}
suspend operator fun invoke(
guildId: ULong,
channelId: ULong,
message: String,
autoEscape: Boolean,
retryCnt: Int,
recallDuration: Long?,
echo: JsonElement = EmptyJsonString
): String {
val result = if (autoEscape) {
MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), listOf(
mapOf(
"type" to "text",
"data" to mapOf(
"text" to message
)
)
).json, fromId = channelId.toString(), retryCnt)
} else {
val msg = MessageHelper.decodeCQCode(message)
if (msg.isEmpty()) {
LogCenter.log("CQ码不合法", Level.WARN)
return logic("CQCode is illegal", echo)
} else {
MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), msg, fromId = channelId.toString(), retryCnt)
}
}
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val sendMsgResult = result.getOrThrow()
if (sendMsgResult.msgHashId <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
return ok(
MessageResult(
msgId = sendMsgResult.msgHashId,
time = (sendMsgResult.msgTime * 0.001).toLong()
), echo = echo)
}
suspend operator fun invoke(
guildId: ULong, channelId: ULong, message: JsonArray, echo: JsonElement = EmptyJsonString, retryCnt: Int, recallDuration: Long?,
): String {
val result = MsgSvc.sendToAio(MsgConstant.KCHATTYPEGUILD, guildId.toString(), message, fromId = channelId.toString(), retryCnt)
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val sendMsgResult = result.getOrThrow()
if (sendMsgResult.msgHashId <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
return ok(MessageResult(
msgId = sendMsgResult.msgHashId,
time = (sendMsgResult.msgTime * 0.001).toLong()
), echo)
}
override val requiredParams: Array<String> = arrayOf("guild_id", "channel_id", "message")
private fun autoRecall(msgHash: Int, duration: Long) {
GlobalScope.launch(Dispatchers.Default) {
delay(duration)
MsgSvc.recallMsg(msgHash)
}
}
}

View File

@ -81,9 +81,6 @@ internal object SendMessage: IActionHandler() {
recallDuration: Long?,
echo: JsonElement = EmptyJsonString
): String {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
// return logic("contact is not found", echo = echo)
//}
val result = if (autoEscape) {
MsgSvc.sendToAio(chatType, peerId, listOf(
mapOf(

View File

@ -4,16 +4,31 @@ import io.ktor.http.ContentType
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGProChannelList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberProfile
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMetaByGuest
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildServiceProfile
import moe.fuqiuluo.shamrock.remote.action.handlers.SendGuildMessage
import moe.fuqiuluo.shamrock.remote.action.handlers.SendMessage
import moe.fuqiuluo.shamrock.tools.fetchGetOrNull
import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonArray
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
import moe.fuqiuluo.shamrock.tools.fetchPostOrNull
import moe.fuqiuluo.shamrock.tools.fetchPostOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonObject
import moe.fuqiuluo.shamrock.tools.isJsonString
import moe.fuqiuluo.shamrock.tools.jsonArray
fun Routing.guildAction() {
getOrPost("/get_guild_service_profile") {
@ -47,4 +62,51 @@ fun Routing.guildAction() {
val userId = fetchOrThrow("user_id")
call.respondText(GetGuildMemberProfile(guildId.toULong(), userId.toULong()), ContentType.Application.Json)
}
route("/(send_guild_channel_msg|send_guild_message|send_guild_msg)".toRegex()) {
get {
val guildId = fetchGetOrThrow("guild_id").toULong()
val channelId = fetchGetOrThrow("channel_id").toULong()
val message = fetchGetOrThrow("message")
val autoEscape = fetchGetOrNull("auto_escape")?.toBoolean() ?: false
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
val recallDuration = fetchGetOrNull("recall_duration")?.toLong()
call.respondText(SendGuildMessage(guildId, channelId, message, autoEscape, retryCnt, recallDuration), ContentType.Application.Json)
}
post {
val retryCnt = fetchOrNull("retry_cnt")?.toInt() ?: 3
val recallDuration = fetchOrNull("recall_duration")?.toLong()
val guildId = fetchOrThrow("guild_id").toULong()
val channelId = fetchOrThrow("channel_id").toULong()
call.respondText(if (isJsonData() && !isJsonString("message")) {
if (isJsonObject("message")) {
SendGuildMessage(
guildId = guildId,
channelId = channelId,
message = listOf(fetchPostJsonObject("message")).jsonArray,
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else {
SendGuildMessage(
guildId = guildId,
channelId = channelId,
message = fetchPostJsonArray("message"),
retryCnt = retryCnt,
recallDuration = recallDuration
)
}
} else {
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
SendGuildMessage(
guildId = guildId,
channelId = channelId,
message = fetchOrThrow("message"),
autoEscape = autoEscape,
retryCnt = retryCnt,
recallDuration = recallDuration
)
}, ContentType.Application.Json)
}
}
}

View File

@ -189,6 +189,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
role = MemberRole.Member,
title = record.sendNickName,
level = record.roleId.toString(),
tinyId = record.senderUid
),
)
)

View File

@ -35,6 +35,7 @@ internal data class MessageSender(
@SerialName("sex") val sex: String,
@SerialName("age") val age: Int,
@SerialName("uid") val uid: String,
@SerialName("tiny_id") val tinyId: String,
)
@Serializable

View File

@ -97,4 +97,5 @@ internal data class Sender(
@SerialName("role") val role: MemberRole?,
@SerialName("title") val title: String,
@SerialName("level") val level: String,
@SerialName("tiny_id") val tinyId: String = "0",
)

View File

@ -48,21 +48,21 @@ internal object AioListener : IKernelMsgListener {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
MessageHelper.saveMsgMapping(
hash = msgHash,
qqMsgId = record.msgId,
chatType = record.chatType,
subChatType = record.chatType,
peerId = record.peerUin.toString(),
peerId = peerId,
msgSeq = record.msgSeq.toInt(),
time = record.msgTime
time = record.msgTime,
subPeerId = record.channelId ?: peerId
)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId)
if (rawMsg.isEmpty()) return
@ -124,7 +124,7 @@ internal object AioListener : IKernelMsgListener {
}
MsgConstant.KCHATTYPEGUILD -> {
LogCenter.log("频道消息(guildId = ${record.guildId}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
LogCenter.log("频道消息(guildId = ${record.guildId}, sender=${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
if(!GlobalEventTransmitter.MessageTransmitter
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
) {
@ -144,21 +144,25 @@ internal object AioListener : IKernelMsgListener {
}
override fun onAddSendMsg(record: MsgRecord) {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
if (record.peerUin == TicketSvc.getLongUin()) return // 发给自己的消息不处理
GlobalScope.launch {
try {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
MessageHelper.saveMsgMapping(
hash = msgHash,
qqMsgId = record.msgId,
chatType = record.chatType,
subChatType = record.chatType,
peerId = record.peerUin.toString(),
peerId = peerId,
msgSeq = record.msgSeq.toInt(),
time = record.msgTime
time = record.msgTime,
subPeerId = record.channelId ?: peerId
)
LogCenter.log("预发送消息($msgHash | ${record.msgSeq} | ${record.msgId})")
@ -180,6 +184,10 @@ internal object AioListener : IKernelMsgListener {
GlobalScope.launch {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
if (mapping == null) {
@ -188,9 +196,10 @@ internal object AioListener : IKernelMsgListener {
qqMsgId = record.msgId,
chatType = record.chatType,
subChatType = record.chatType,
peerId = record.peerUin.toString(),
peerId = peerId,
msgSeq = record.msgSeq.toInt(),
time = record.msgTime
time = record.msgTime,
subPeerId = record.channelId ?: peerId
)
} else {
LogCenter.log("Update message info from ${mapping.msgSeq} to ${record.msgSeq}", Level.INFO)
@ -203,11 +212,6 @@ internal object AioListener : IKernelMsgListener {
|| record.peerUin == TicketSvc.getLongUin()
) return@launch
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId)
if (rawMsg.isEmpty()) return@launch
LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)")