10 Commits

Author SHA1 Message Date
fdb2486090 Shamrock: Disable lost connection detection 2024-02-10 00:41:38 +08:00
d60b2a25d1 Update SECURITY.md 2024-02-09 08:04:40 +08:00
2d8dde6951 add history msg to database 2024-02-08 23:52:21 +08:00
78fd60dade Merge pull request #228 from Mythologyli/master
feat: get group applier uin from request msg
2024-02-08 22:34:40 +08:00
80dbf6af28 feat: get group applier uin from request msg 2024-02-08 22:27:55 +08:00
1e53753b5a Shamrock: fix #227 2024-02-08 20:17:51 +08:00
e727877268 Merge pull request #225 from MrXiaoM/fix-guild-message
修复 频道消息事件不符合 go-cqhttp 规范
2024-02-08 20:16:01 +08:00
63b69df3ea fix missing guild_id and channel_id 2024-02-08 14:51:14 +08:00
b03e02675b Shamrock: add timeout #223 2024-02-05 22:16:12 +08:00
e68a1ffd37 Shamrock: fix guild sync 2024-02-05 22:12:20 +08:00
12 changed files with 89 additions and 27 deletions

View File

@ -1,11 +1,19 @@
# Security Policy # Security Policy
## Support Version ## 支持的版本
| Version | Supported | | 版本 | 支持状态 |
| ------- | ------------------ | | ------- | ------------------ |
| 9.0.15 | :white_check_mark: |
| 8.9.75 | :white_check_mark: | | 8.9.75 | :white_check_mark: |
| 8.9.73 | :white_check_mark: | | 8.9.73 | :white_check_mark: |
| 8.9.98 | :white_check_mark: | | 8.9.98 | :white_check_mark: |
| < 8.9.68| :x: | | < 8.9.68| :x: |
## 频道支持性说明
如果需要使用`频道`相关功能请升级QQ到9.0.8版本
## Riru检测问题
QQ自`9.0.8`开始将会检测riru可能作为封号因素

View File

@ -353,6 +353,21 @@ internal object MessageHelper {
database.messageMappingDao().insert(mapping) database.messageMappingDao().insert(mapping)
} }
fun saveMsgMappingNotExist(
hash: Int,
qqMsgId: Long,
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, subPeerId)
database.messageMappingDao().insertNotExist(mapping)
}
external fun createMessageUniseq(chatType: Int, time: Long): Long external fun createMessageUniseq(chatType: Int, time: Long): Long
fun decodeCQCode(code: String): JsonArray { fun decodeCQCode(code: String): JsonArray {

View File

@ -30,6 +30,9 @@ interface MessageMappingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(mapping: MessageMapping) fun insert(mapping: MessageMapping)
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertNotExist(mapping: MessageMapping)
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash") @Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int) fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)

View File

@ -1,5 +1,6 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@ -18,8 +19,10 @@ internal object GetGProChannelList: IActionHandler() {
return invoke(guildId.toULong(), refresh, echo = session.echo) return invoke(guildId.toULong(), refresh, echo = session.echo)
} }
operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String { suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
val result = GProSvc.getChannelList(guildId, refresh) val result = withTimeoutOrNull(5000) {
GProSvc.getChannelList(guildId, refresh)
} ?: return error("timeout", echo)
result.onFailure { result.onFailure {
return error(it.message ?: "unable to fetch channel list", echo) return error(it.message ?: "unable to fetch channel list", echo)
} }

View File

@ -59,6 +59,16 @@ internal object GetHistoryMsg: IActionHandler() {
val msgList = ArrayList<MessageDetail>().apply { val msgList = ArrayList<MessageDetail>().apply {
addAll(result.data!!.map { msg -> addAll(result.data!!.map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId) val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
MessageHelper.saveMsgMappingNotExist(
hash = msgHash,
qqMsgId = msg.msgId,
chatType = msg.chatType,
subChatType = msg.chatType,
peerId = peerId,
msgSeq = msg.msgSeq.toInt(),
time = msg.msgTime,
subPeerId = msg.channelId ?: peerId
)
MessageDetail( MessageDetail(
time = msg.msgTime.toInt(), time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),

View File

@ -2,6 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
@ -17,7 +18,7 @@ internal object ModifyTroopMemberName: IActionHandler() {
} }
operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String { operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String {
if (!GroupSvc.isAdmin(groupId)) { if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin()) {
return logic("you are not admin", echo) return logic("you are not admin", echo)
} }
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card)) return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))

View File

@ -5,6 +5,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController import com.tencent.mobileqq.transfile.api.ITransFileController
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.routing.post import io.ktor.server.routing.post
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.remote.structures.Status import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.fetchPost import moe.fuqiuluo.shamrock.tools.fetchPost
import moe.fuqiuluo.shamrock.tools.respond import moe.fuqiuluo.shamrock.tools.respond
@ -15,7 +16,7 @@ import kotlin.random.Random
import kotlin.random.nextLong import kotlin.random.nextLong
fun Routing.registerBDH() { fun Routing.registerBDH() {
post("/upload_group_image") { if(ShamrockConfig.isDev()) post("/upload_group_image") {
val troop = fetchPost("troop") val troop = fetchPost("troop")
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT) val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
val md5Str = MD5.getMd5Hex(picBytes) val md5Str = MD5.getMd5Hex(picBytes)
@ -46,5 +47,4 @@ fun Routing.registerBDH() {
.transferAsync(transferRequest) .transferAsync(transferRequest)
respond(isOk = true, Status.Ok, "$md5Str.jpg") respond(isOk = true, Status.Ok, "$md5Str.jpg")
} }
} }

View File

@ -172,6 +172,8 @@ internal object GlobalEventTransmitter: BaseSvc() {
postType = postType, postType = postType,
messageType = MsgType.Guild, messageType = MsgType.Guild,
subType = MsgSubType.Channel, subType = MsgSubType.Channel,
guildId = record.guildId,
channelId = record.channelId,
messageId = msgHash, messageId = msgHash,
targetId = record.peerUin, targetId = record.peerUin,
peerId = botUin, peerId = botUin,

View File

@ -41,6 +41,10 @@ internal abstract class WebSocketTransmitServlet(
private val sendLock = Mutex() private val sendLock = Mutex()
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>()) protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
init {
connectionLostTimeout = 0
}
override val address: String override val address: String
get() = "-" get() = "-"

View File

@ -53,6 +53,8 @@ internal data class MessageEvent (
@SerialName("sub_type") val subType: MsgSubType, @SerialName("sub_type") val subType: MsgSubType,
@SerialName("message_id") val messageId: Int, @SerialName("message_id") val messageId: Int,
@SerialName("group_id") val groupId: Long = 0, @SerialName("group_id") val groupId: Long = 0,
@SerialName("guild_id") val guildId: String? = null,
@SerialName("channel_id") val channelId: String? = null,
@SerialName("target_id") val targetId: Long = 0, @SerialName("target_id") val targetId: Long = 0,
@SerialName("peer_id") val peerId: Long, @SerialName("peer_id") val peerId: Long,
@SerialName("user_id") val userId: Long, @SerialName("user_id") val userId: Long,

View File

@ -89,10 +89,12 @@ internal object AioListener : IKernelMsgListener {
if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage( if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
record, record.elements, rawMsg, msgHash, postType record, record.elements, rawMsg, msgHash, postType
)) { )
) {
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN) LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
} }
} }
MsgConstant.KCHATTYPEC2C -> { MsgConstant.KCHATTYPEC2C -> {
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)") LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule -> ShamrockConfig.getPrivateRule()?.let { rule ->
@ -102,7 +104,8 @@ internal object AioListener : IKernelMsgListener {
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, postType record, record.elements, rawMsg, msgHash, postType
)) { )
) {
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN) LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
@ -117,8 +120,14 @@ internal object AioListener : IKernelMsgListener {
} }
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group, postType = postType record,
)) { record.elements,
rawMsg,
msgHash,
tempSource = MessageTempSource.Group,
postType = postType
)
) {
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
@ -174,8 +183,6 @@ internal object AioListener : IKernelMsgListener {
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) { override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
msgList?.forEach { record -> msgList?.forEach { record ->
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING || record.sendStatus == MsgConstant.KSENDSTATUSSENDING
) { ) {
@ -449,6 +456,7 @@ internal object AioListener : IKernelMsgListener {
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
} }
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
} }

View File

@ -534,12 +534,11 @@ internal object PrimitiveListener {
val groupCode = event.groupCode val groupCode = event.groupCode
val applierUid = event.applierUid val applierUid = event.applierUid
val reason = event.applyMsg ?: "" val reason = event.applyMsg ?: ""
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong() var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) { if (applier == getLongUin()) {
return return
} }
val msgSeq = contentHead.msgSeq val msgSeq = contentHead.msgSeq
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
val flag = try { val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1) var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2) val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -548,10 +547,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode
} }
val seq = req?.msg_seq?.get() ?: time val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier" "$seq;$groupCode;$applier"
} catch (err: Throwable) { } catch (err: Throwable) {
"$time;$groupCode;$applier" "$time;$groupCode;$applier"
} }
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add) .transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add)
) { ) {
@ -562,7 +565,7 @@ internal object PrimitiveListener {
val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!) val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!)
val groupCode = event.applyInfo?.groupCode ?: return val groupCode = event.applyInfo?.groupCode ?: return
val applierUid = event.applyInfo?.applierUid ?: return val applierUid = event.applyInfo?.applierUid ?: return
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong() var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) { if (applier == getLongUin()) {
return return
} }
@ -570,7 +573,6 @@ internal object PrimitiveListener {
// todo // todo
return return
} }
LogCenter.log("邀请入群申请($groupCode): $applier")
val flag = try { val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1) var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2) val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -579,10 +581,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time it.msg_time.get() == time
} }
val seq = req?.msg_seq?.get() ?: time val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier" "$seq;$groupCode;$applier"
} catch (err: Throwable) { } catch (err: Throwable) {
"$time;$groupCode;$applier" "$time;$groupCode;$applier"
} }
LogCenter.log("邀请入群申请($groupCode): $applier")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add) .transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add)
) { ) {