8 Commits

Author SHA1 Message Date
db252b6b6c Shamrock: support /send_guild_channel_msg 2024-02-02 22:47:38 +08:00
137c354acc Shamrock: support receiving all guild msg 2024-02-02 22:07:56 +08:00
1c7f6bd034 Shamrock: support /get_guild_member_profile 2024-02-02 19:11:45 +08:00
649d8771ca Shamrock: fix error as clover.cpp changed 2024-02-02 18:37:47 +08:00
af7b0f732e Merge remote-tracking branch 'origin/master' 2024-02-02 18:36:08 +08:00
12738fd52c Shamrock: fix #219 2024-02-02 18:35:49 +08:00
b165e1c0c2 Merge pull request #221 from PisLuanyao/master
👻
2024-02-02 18:30:03 +08:00
29c1ad8bc9 Clover.cpp
临时解决Nox提示 "QQ 屡次停止运行" ,不清楚副作用
2024-02-02 14:39:41 +08:00
26 changed files with 665 additions and 115 deletions

View File

@ -249,6 +249,11 @@ object ShamrockConfig {
return preferences.getBoolean("enable_auto_start", false) return preferences.getBoolean("enable_auto_start", false)
} }
fun disableAutoSyncSetting(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("disable_auto_sync_setting", false)
}
fun enableAliveReply(ctx: Context): Boolean { fun enableAliveReply(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0) val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("alive_reply", false) return preferences.getBoolean("alive_reply", false)
@ -264,6 +269,11 @@ object ShamrockConfig {
preferences.edit().putBoolean("enable_auto_start", v).apply() preferences.edit().putBoolean("enable_auto_start", v).apply()
} }
fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("disable_auto_sync_setting", v).apply()
}
fun setAliveReply(ctx: Context, v: Boolean) { fun setAliveReply(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0) val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("alive_reply", v).apply() preferences.edit().putBoolean("alive_reply", v).apply()
@ -322,6 +332,7 @@ object ShamrockConfig {
"shell" to preferences.getBoolean("shell", false), "shell" to preferences.getBoolean("shell", false),
"alive_reply" to preferences.getBoolean("alive_reply", false), "alive_reply" to preferences.getBoolean("alive_reply", false),
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false), "enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
) )
} }

View File

@ -91,7 +91,7 @@ fun LabFragment() {
ActionBox( ActionBox(
modifier = Modifier.padding(top = 12.dp), modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.round_logo_dev_24), painter = painterResource(id = R.drawable.round_logo_dev_24),
title = "实验功能" title = "基础设置"
) { color -> ) { color ->
Column { Column {
Divider( Divider(
@ -142,6 +142,16 @@ fun LabFragment() {
return@Function true return@Function true
} }
Function(
title = "禁止Shamrock同步设置",
desc = "禁止Shamrock同步设置防止恢复手动修改后的配置文件。",
descColor = color,
isSwitch = ShamrockConfig.disableAutoSyncSetting(ctx)
) {
ShamrockConfig.setDisableAutoSyncSetting(ctx, it)
return@Function true
}
kotlin.runCatching { kotlin.runCatching {
ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE) ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE)
}.onSuccess { }.onSuccess {

View File

@ -0,0 +1,61 @@
@file:OptIn(ExperimentalSerializationApi::class)
package moe.whitechi73.protobuf.oidb.cmd0xfc2
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class Oidb0xfc2ReqBody(
@ProtoNumber(1) var msgCmd: Int? = null,
@ProtoNumber(3) var msgBusType: Int? = null,
@ProtoNumber(4) var msgChannelInfo: Oidb0xfc2ChannelInfo? = null,
@ProtoNumber(5) var msgTerminalType: Int? = null,
//@ProtoNumber(100) var msg_apply_upload_req: Any? = null,
//@ProtoNumber(200) var msg_upload_completed_req: Any? = null,
@ProtoNumber(300) var msgApplyDownloadReq: Oidb0xfc2MsgApplyDownloadReq? = null,
//@ProtoNumber(400) var msg_apply_preview_req: Any? = null,
//@ProtoNumber(500) var msg_apply_security_strike_req: Any? = null,
)
@Serializable
data class Oidb0xfc2RspBody(
@ProtoNumber(1) var msgCmd: Int? = null,
@ProtoNumber(5) var msgBusType: Int? = null,
//@ProtoNumber(110) var msg_apply_upload_rsp: Any? = null,
//@ProtoNumber(210) var msg_upload_completed_rsp: Any? = null,
@ProtoNumber(310) var msgApplyDownloadRsp: Oidb0xfc2MsgApplyDownloadRsp? = null,
//@ProtoNumber(410) var msg_apply_preview_rsp: Any? = null,
//@ProtoNumber(510) var msg_apply_security_strike_rsp: Any? = null,
)
@Serializable
data class Oidb0xfc2MsgApplyDownloadRsp(
@ProtoNumber(1) var msgDownloadInfo: Oidb0xfc2MsgDownloadInfo? = null,
//@ProtoNumber(2) var msgFileInfo: Any? = null,
//@ProtoNumber(3) var msgChacha20Param: Any? = null,
//@ProtoNumber(4) var useEncrypt: UInt = UInt.MIN_VALUE,
)
@Serializable
data class Oidb0xfc2MsgDownloadInfo(
@ProtoNumber(1) var downloadKey: ByteArray? = null,
//@ProtoNumber(2) var msg_out_addr: Any? = null,
//@ProtoNumber(3) var msg_inner_addr: Any? = null,
//@ProtoNumber(4) var msg_out_addr_ipv6: Any? = null,
@ProtoNumber(5) var downloadDomain: String? = null,
@ProtoNumber(6) var downloadUrl: String? = null,
//@ProtoNumber(7) var str_cookie: Any? = null,
)
@Serializable
data class Oidb0xfc2MsgApplyDownloadReq(
@ProtoNumber(1) val fieldId: String,
@ProtoNumber(2) val supportEncrypt: Int,
)
@Serializable
data class Oidb0xfc2ChannelInfo(
@ProtoNumber(3) val guildId: ULong,
@ProtoNumber(4) val channelId: ULong,
)

View File

@ -0,0 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public interface IGProFetchMemberRolesCallback {
void onFetchMemberRolesCallback(int code, String reason, ArrayList<GProGuildRole> roles);
}

View File

@ -3,5 +3,5 @@ package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList; import java.util.ArrayList;
public interface IGProGetUserInfoCallback { public interface IGProGetUserInfoCallback {
void onGetUserInfo(int i2, String str, ArrayList<GProUser> arrayList, ArrayList<Long> arrayList2); void onGetUserInfo(int code, String reason, ArrayList<GProUser> userList, ArrayList<Long> tinyIdList);
} }

View File

@ -41,7 +41,11 @@ public interface IKernelGuildService {
void refreshGuildInfoOnly(long j2, boolean z, int i2); void refreshGuildInfoOnly(long j2, boolean z, int i2);
void fetchUserInfo(long j2, long j3, ArrayList<Long> tinyIdList, int i2, IGProGetUserInfoCallback iGProGetUserInfoCallback); void fetchMemberRoles(long guildId, long channelId, long tinyId, int seq, IGProFetchMemberRolesCallback cb);
void refreshGuildUserProfileInfo(long guildId, long tinyId, int seq);
void fetchUserInfo(long guildId, long channelId, ArrayList<Long> tinyIdList, int seq, IGProGetUserInfoCallback cb);
GProSimpleProfile getSimpleProfile(long guildId, long tinyId, int seq); GProSimpleProfile getSimpleProfile(long guildId, long tinyId, int seq);

View File

@ -138,6 +138,12 @@ char * __cdecl my_strstr(const char *lhs, const char *rhs) {
} }
int fake_memcmp(const void* __lhs, const void* __rhs, size_t __n) { int fake_memcmp(const void* __lhs, const void* __rhs, size_t __n) {
if (__lhs == nullptr || __rhs == nullptr) {
if (__n != 0) {
LOGI("[Shamrock] undefined behaviour in fake_memcmp");
}
return 0;
}
if (my_strstr((const char*) __rhs, "shamrock") && my_strstr((const char*) __lhs, "shamrock")) { if (my_strstr((const char*) __rhs, "shamrock") && my_strstr((const char*) __lhs, "shamrock")) {
if (backup_memcmp(__lhs, __rhs, __n) == 0) { if (backup_memcmp(__lhs, __rhs, __n) == 0) {
// 底层广播判断 // 底层广播判断

View File

@ -3,6 +3,7 @@
package moe.fuqiuluo.qqinterface.servlet package moe.fuqiuluo.qqinterface.servlet
import com.tencent.mobileqq.qqguildsdk.api.IGPSService import com.tencent.mobileqq.qqguildsdk.api.IGPSService
import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole
import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList
import com.tencent.qqnt.kernel.nativeinterface.IGProFetchMemberListWithRoleCallback import com.tencent.qqnt.kernel.nativeinterface.IGProFetchMemberListWithRoleCallback
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -108,6 +109,7 @@ internal object GProSvc: BaseSvc() {
result: ArrayList<GProRoleMemberList> = arrayListOf() result: ArrayList<GProRoleMemberList> = arrayListOf()
): Result<Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>>> { ): Result<Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>>> {
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
val fetchGuildMemberListResult: Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>> = (withTimeoutOrNull(5000) { val fetchGuildMemberListResult: Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>> = (withTimeoutOrNull(5000) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList -> kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList ->
@ -203,6 +205,21 @@ internal object GProSvc: BaseSvc() {
} }
} }
suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result<ArrayList<GProGuildRole>> {
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
if (refresh) {
kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1)
}
val result: ArrayList<GProGuildRole> = withTimeoutOrNull(5000) {
suspendCancellableCoroutine {
kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles ->
it.resume(roles)
}
}
} ?: return Result.failure(Exception("unable to fetch guild member roles"))
return Result.success(result)
}
fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList<GuildInfo> { fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList<GuildInfo> {
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
if (refresh) { if (refresh) {

View File

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

View File

@ -32,12 +32,12 @@ internal suspend fun MsgRecord.toCQCode(): String {
return MessageConvert.convertMessageRecordToCQCode(this) return MessageConvert.convertMessageRecordToCQCode(this)
} }
internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String): MessageSegmentList { internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String, subPeer: String): MessageSegmentList {
return MessageConvert.convertMessageElementsToMsgSegment(chatType, this, peerId) return MessageConvert.convertMessageElementsToMsgSegment(chatType, this, peerId, subPeer)
} }
internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String): String { internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
return MessageConvert.convertMsgElementsToCQCode(this, chatType, peerId) return MessageConvert.convertMsgElementsToCQCode(this, chatType, peerId, subPeer)
} }
@ -64,14 +64,15 @@ internal object MessageConvert {
suspend fun convertMessageElementsToMsgSegment( suspend fun convertMessageElementsToMsgSegment(
chatType: Int, chatType: Int,
elements: List<MsgElement>, elements: List<MsgElement>,
peerId: String peerId: String,
subPeer: String
): ArrayList<MessageSegment> { ): ArrayList<MessageSegment> {
val messageData = arrayListOf<MessageSegment>() val messageData = arrayListOf<MessageSegment>()
elements.forEach { msg -> elements.forEach { msg ->
kotlin.runCatching { kotlin.runCatching {
val elementId = msg.elementType val elementId = msg.elementType
val converter = convertMap[elementId] val converter = convertMap[elementId]
converter?.convert(chatType, peerId, msg) converter?.convert(chatType, peerId, subPeer, msg)
?: throw UnsupportedOperationException("不支持的消息element类型$elementId") ?: throw UnsupportedOperationException("不支持的消息element类型$elementId")
}.onSuccess { }.onSuccess {
messageData.add(it) messageData.add(it)
@ -87,35 +88,45 @@ internal object MessageConvert {
} }
suspend fun convertMessageRecordToMsgSegment(record: MsgRecord, chatType: Int = record.chatType): ArrayList<MessageSegment> { suspend fun convertMessageRecordToMsgSegment(record: MsgRecord, chatType: Int = record.chatType): ArrayList<MessageSegment> {
return convertMessageElementsToMsgSegment(chatType, record.elements, record.peerUin.toString()) val peerId = when(chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
return convertMessageElementsToMsgSegment(chatType, record.elements, peerId, record.channelId ?: peerId)
} }
suspend fun convertMsgElementsToCQCode( suspend fun convertMsgElementsToCQCode(
elements: List<MsgElement>, elements: List<MsgElement>,
chatType: Int, chatType: Int,
peerId: String peerId: String,
subPeer: String
): String { ): String {
if(elements.isEmpty()) { if(elements.isEmpty()) {
return "" return ""
} }
val msgList = convertMessageElementsToMsgSegment(chatType, elements, peerId).map { val msgList = convertMessageElementsToMsgSegment(chatType, elements, peerId, subPeer).map {
it.toJson() it.toJson()
} }
return MessageHelper.encodeCQCode(msgList) return MessageHelper.encodeCQCode(msgList)
} }
suspend fun convertMessageRecordToCQCode(record: MsgRecord, chatType: Int = record.chatType): String { suspend fun convertMessageRecordToCQCode(record: MsgRecord, chatType: Int = record.chatType): String {
val peerId = when(chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
return MessageHelper.encodeCQCode( return MessageHelper.encodeCQCode(
convertMessageElementsToMsgSegment( convertMessageElementsToMsgSegment(
chatType, chatType,
record.elements, record.elements,
record.peerUin.toString() peerId,
record.channelId ?: peerId
).map { it.toJson() } ).map { it.toJson() }
) )
} }
} }
internal fun interface IMessageConvert { internal fun interface IMessageConvert {
suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment suspend fun convert(chatType: Int, peerId: String, subPeer: String, element: MsgElement): MessageSegment
} }

View File

@ -19,7 +19,12 @@ internal sealed class MessageElemConverter: IMessageConvert {
* 文本 / 艾特 消息转换消息段 * 文本 / 艾特 消息转换消息段
*/ */
data object TextConverter: MessageElemConverter() { data object TextConverter: MessageElemConverter() {
override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment { override suspend fun convert(
chatType: Int,
peerId: String,
subPeer: String,
element: MsgElement
): MessageSegment {
val text = element.textElement val text = element.textElement
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) { return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
MessageSegment( MessageSegment(
@ -43,7 +48,12 @@ internal sealed class MessageElemConverter: IMessageConvert {
* 小表情 / 戳一戳 消息转换消息段 * 小表情 / 戳一戳 消息转换消息段
*/ */
data object FaceConverter: MessageElemConverter() { data object FaceConverter: MessageElemConverter() {
override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment { override suspend fun convert(
chatType: Int,
peerId: String,
subPeer: String,
element: MsgElement
): MessageSegment {
val face = element.faceElement val face = element.faceElement
if (face.faceType == 5) { if (face.faceType == 5) {
@ -112,6 +122,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val image = element.picElement val image = element.picElement
@ -131,6 +142,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5)
else -> unknownChatType(chatType) else -> unknownChatType(chatType)
}, },
"subType" to image.picSubType, "subType" to image.picSubType,
@ -147,6 +159,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val record = element.pttElement val record = element.pttElement
@ -162,6 +175,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid)
else -> unknownChatType(chatType) else -> unknownChatType(chatType)
} }
).also { ).also {
@ -183,6 +197,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val video = element.videoElement val video = element.videoElement
@ -195,6 +210,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl(peerId, md5, video.fileUuid)
else -> unknownChatType(chatType) else -> unknownChatType(chatType)
} }
).also { ).also {
@ -212,6 +228,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val face = element.marketFaceElement val face = element.marketFaceElement
@ -235,6 +252,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val data = element.arkElement.bytesData.asJsonObject val data = element.arkElement.bytesData.asJsonObject
@ -297,6 +315,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val reply = element.replyElement val reply = element.replyElement
@ -329,6 +348,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val tip = element.grayTipElement val tip = element.grayTipElement
@ -364,6 +384,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val fileMsg = element.fileElement val fileMsg = element.fileElement
@ -373,8 +394,11 @@ internal sealed class MessageElemConverter: IMessageConvert {
val fileId = fileMsg.fileUuid val fileId = fileMsg.fileUuid
val bizId = fileMsg.fileBizId ?: 0 val bizId = fileMsg.fileBizId ?: 0
val fileSubId = fileMsg.fileSubId ?: "" val fileSubId = fileMsg.fileSubId ?: ""
val url = if (chatType == MsgConstant.KCHATTYPEC2C) RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) val url = when (chatType) {
else RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
}
return MessageSegment( return MessageSegment(
type = "file", type = "file",
@ -398,6 +422,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val multiMsg = element.multiForwardMsgElement val multiMsg = element.multiForwardMsgElement
@ -414,6 +439,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val longMsg = element.structLongMsgElement val longMsg = element.structLongMsgElement
@ -430,6 +456,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val markdown = element.markdownElement val markdown = element.markdownElement
@ -446,6 +473,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
override suspend fun convert( override suspend fun convert(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String,
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val bubbleElement = element.faceBubbleElement val bubbleElement = element.faceBubbleElement

View File

@ -1,3 +1,4 @@
@file:OptIn(ExperimentalSerializationApi::class)
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.pb.ByteStringMicro import com.tencent.mobileqq.pb.ByteStringMicro
@ -6,6 +7,10 @@ import com.tencent.mobileqq.transfile.api.IProtoReqManager
import com.tencent.mobileqq.transfile.protohandler.RichProto import com.tencent.mobileqq.transfile.protohandler.RichProto
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
@ -14,6 +19,10 @@ import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
import mqq.app.MobileQQ import mqq.app.MobileQQ
import tencent.im.cs.cmd0x346.cmd0x346 import tencent.im.cs.cmd0x346.cmd0x346
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
@ -22,6 +31,30 @@ import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume import kotlin.coroutines.resume
internal object RichProtoSvc: BaseSvc() { internal object RichProtoSvc: BaseSvc() {
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(Oidb0xfc2ReqBody(
msgCmd = 1200,
msgBusType = 4202,
msgChannelInfo = Oidb0xfc2ChannelInfo(
guildId = peerId.toULong(),
channelId = channelId.toULong()
),
msgTerminalType = 2,
msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq(
fieldId = fileId,
supportEncrypt = 0
)
))) ?: return ""
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
ProtoBuf.decodeFromByteArray<Oidb0xfc2RspBody>(body.bytes_bodybuffer.get().toByteArray()).msgApplyDownloadRsp?.let {
it.msgDownloadInfo?.let {
return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0"
}
}
return ""
}
suspend fun getGroupFileDownUrl( suspend fun getGroupFileDownUrl(
peerId: Long, peerId: Long,
fileId: String, fileId: String,
@ -34,27 +67,23 @@ internal object RichProtoSvc: BaseSvc() {
uint32_bus_id.set(bizId) uint32_bus_id.set(bizId)
str_file_id.set(fileId) str_file_id.set(fileId)
}) })
}.toByteArray()) }.toByteArray()) ?: return ""
if (buffer == null) { val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
if (body.uint32_result.get() != 0
|| result.download_file_rsp.int32_ret_code.get() != 0) {
return "" return ""
} else {
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
if (body.uint32_result.get() != 0
|| result.download_file_rsp.int32_ret_code.get() != 0) {
return ""
}
val domain = if (!result.download_file_rsp.str_download_dns.has())
("https://" + result.download_file_rsp.str_download_ip.get())
else ("http://" + result.download_file_rsp.str_download_dns.get())
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
} }
val domain = if (!result.download_file_rsp.str_download_dns.has())
("https://" + result.download_file_rsp.str_download_ip.get())
else ("http://" + result.download_file_rsp.str_download_dns.get())
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
} }
suspend fun getC2CFileDownUrl( suspend fun getC2CFileDownUrl(
@ -83,7 +112,7 @@ internal object RichProtoSvc: BaseSvc() {
}.toByteArray()) }.toByteArray())
if (buffer == null) { if (buffer == null) {
if (retryCnt < 3) { if (retryCnt < 5) {
return getC2CFileDownUrl(fileId, subId, retryCnt + 1) return getC2CFileDownUrl(fileId, subId, retryCnt + 1)
} }
return "" return ""
@ -122,6 +151,10 @@ internal object RichProtoSvc: BaseSvc() {
return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2" return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
} }
fun getGuildPicDownUrl(md5: String): String {
return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
}
suspend fun getC2CVideoDownUrl( suspend fun getC2CVideoDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5Hex: String,
@ -287,4 +320,11 @@ internal object RichProtoSvc: BaseSvc() {
} }
} }
suspend fun getGuildPttDownUrl(
peerId: String,
md5Hex: String,
fileUUId: String
): String {
return "unsupported"
}
} }

View File

@ -90,7 +90,7 @@ internal object MessageHelper {
// ActionMsg No Care // ActionMsg No Care
if (msg.isEmpty()) { if (msg.isEmpty()) {
return Result.success(uniseq.copy(msgTime = System.currentTimeMillis(), msgHashId = 0)) return Result.success(uniseq.copy(msgTime = System.currentTimeMillis()))
} }
val totalSize = msg.filter { val totalSize = msg.filter {
@ -107,12 +107,7 @@ internal object MessageHelper {
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) { val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
GlobalScope.launch { GlobalScope.launch {
sendResult = sendMessageWithoutMsgId( sendResult = sendMessageWithoutMsgId(chatType, peerId, msg, fromId) { code, message ->
chatType,
peerId,
msg,
fromId
) { code, message ->
callback.onResult(code, message) callback.onResult(code, message)
it.resume(code to message) it.resume(code to message)
} }
@ -248,10 +243,17 @@ internal object MessageHelper {
} }
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) { val peerId = when(chatType) {
ContactHelper.getUidByUinAsync(id.toLong()) MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
} else id ContactHelper.getUidByUinAsync(id.toLong())
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 { fun obtainMessageTypeByDetailType(detailType: String): Int {
@ -308,6 +310,7 @@ internal object MessageHelper {
MsgConstant.KCHATTYPEGROUP -> "grp$msgId" MsgConstant.KCHATTYPEGROUP -> "grp$msgId"
MsgConstant.KCHATTYPEC2C -> "c2c$msgId" MsgConstant.KCHATTYPEC2C -> "c2c$msgId"
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId" MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId"
MsgConstant.KCHATTYPEGUILD -> "guild$msgId"
else -> error("不支持的消息来源类型 | generateMsgIdHash: $chatType") else -> error("不支持的消息来源类型 | generateMsgIdHash: $chatType")
} }
return abs(key.hashCode()) return abs(key.hashCode())
@ -341,11 +344,12 @@ internal object MessageHelper {
time: Long, time: Long,
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeerId: String,
msgSeq: Int, msgSeq: Int,
subChatType: Int = chatType subChatType: Int = chatType
) { ) {
val database = MessageDB.getInstance() 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) database.messageMappingDao().insert(mapping)
} }

View File

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

View File

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

View File

@ -0,0 +1,78 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GProSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("get_guild_member_profile")
internal object GetGuildMemberProfile: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val guildId = session.getString("guild_id").toULong()
val userId = session.getString("user_id").toULong()
return invoke(guildId, userId, session.echo)
}
suspend operator fun invoke(guildId: ULong, userId: ULong, echo: JsonElement = EmptyJsonString): String {
val userResult = GProSvc.getUserGuildInfo(guildId, userId).onFailure {
return error(it.message ?: "unable to fetch guild member info", echo)
}.getOrThrow()
val roles = GProSvc.fetchGuildMemberRoles(guildId, userId).onFailure {
return error(it.message ?: "unable to fetch guild member roles", echo)
}.getOrThrow()
return ok(GetGuildMemberInfo(
tinyId = userResult.memberTinyid,
nickname = userResult.nickName ?: "",
avatarUrl = userResult.url ?: "",
joinTime = userResult.joinTime,
roles = roles.map {
RoleInfo(
roleId = it.roleId.toString(),
roleName = it.name.ifNullOrEmpty(it.levelDsc.ifNullOrEmpty(it.displayTagName ?: ""))!!,
color = it.color,
permission = it.rolePermissions.permissionList.map {
Permission(
rootId = it.rootId,
childIds = it.childIds ?: emptyList()
)
},
type = it.type,
displayName = it.displayTagName ?: ""
)
}
), echo = echo)
}
override val requiredParams: Array<String> = arrayOf("guild_id", "user_id")
@Serializable
data class GetGuildMemberInfo(
@SerialName("tiny_id") val tinyId: ULong,
@SerialName("nickname") val nickname: String,
@SerialName("avatar_url") val avatarUrl: String,
@SerialName("join_time") val joinTime: ULong,
@SerialName("roles") val roles: List<RoleInfo>
)
@Serializable
data class RoleInfo(
@SerialName("role_id") val roleId: String,
@SerialName("role_name") val roleName: String,
@SerialName("color") val color: Long,
@SerialName("permission") val permission: List<Permission>,
@SerialName("type") val type: Int,
@SerialName("display_name") val displayName: String
)
@Serializable
data class Permission(
@SerialName("root_id") val rootId: Int,
@SerialName("child_ids") val childIds: List<Int>
)
}

View File

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

View File

@ -34,7 +34,10 @@ internal object GetMsg: IActionHandler() {
msg.senderUin, msg.sendNickName msg.senderUin, msg.sendNickName
.ifEmpty { msg.sendMemberName } .ifEmpty { msg.sendMemberName }
.ifEmpty { msg.sendRemarkName } .ifEmpty { msg.sendRemarkName }
.ifEmpty { msg.peerName }, "unknown", 0, msg.senderUid .ifEmpty { msg.peerName }, "unknown",
0,
msg.senderUid,
msg.senderUid
), ),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map { message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson() 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?, recallDuration: Long?,
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
// return logic("contact is not found", echo = echo)
//}
val result = if (autoEscape) { val result = if (autoEscape) {
MsgSvc.sendToAio(chatType, peerId, listOf( MsgSvc.sendToAio(chatType, peerId, listOf(
mapOf( mapOf(

View File

@ -4,15 +4,31 @@ import io.ktor.http.ContentType
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing 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.GetGProChannelList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildList import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildMemberList 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.GetGuildMetaByGuest
import moe.fuqiuluo.shamrock.remote.action.handlers.GetGuildServiceProfile 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.fetchGetOrNull
import moe.fuqiuluo.shamrock.tools.fetchGetOrThrow
import moe.fuqiuluo.shamrock.tools.fetchOrNull import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow 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.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() { fun Routing.guildAction() {
getOrPost("/get_guild_service_profile") { getOrPost("/get_guild_service_profile") {
@ -40,4 +56,57 @@ fun Routing.guildAction() {
val refresh = fetchGetOrNull("refresh") ?: fetchOrNull("no_cache") val refresh = fetchGetOrNull("refresh") ?: fetchOrNull("no_cache")
call.respondText(GetGProChannelList(guildId.toULong(), refresh?.toBoolean() ?: false), ContentType.Application.Json) call.respondText(GetGProChannelList(guildId.toULong(), refresh?.toBoolean() ?: false), ContentType.Application.Json)
} }
getOrPost("/get_guild_member_profile") {
val guildId = fetchOrThrow("guild_id")
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

@ -74,7 +74,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
peerId = uin, peerId = uin,
userId = record.senderUin, userId = record.senderUin,
message = if(ShamrockConfig.useCQ()) rawMsg.json message = if(ShamrockConfig.useCQ()) rawMsg.json
else elements.toSegments(record.chatType, record.peerUin.toString()).map { else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map {
it.toJson() it.toJson()
}.json, }.json,
rawMessage = rawMsg, rawMessage = rawMsg,
@ -129,7 +129,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
peerId = botUin, peerId = botUin,
userId = record.senderUin, userId = record.senderUin,
message = if(ShamrockConfig.useCQ()) rawMsg.json message = if(ShamrockConfig.useCQ()) rawMsg.json
else elements.toSegments(record.chatType, record.peerUin.toString()).map { else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map {
it.toJson() it.toJson()
}.json, }.json,
rawMessage = rawMsg, rawMessage = rawMsg,
@ -147,6 +147,54 @@ internal object GlobalEventTransmitter: BaseSvc() {
) )
return true return true
} }
/**
* 推送私聊消息
*/
suspend fun transGuildMessage(
record: MsgRecord,
elements: ArrayList<MsgElement>,
rawMsg: String,
msgHash: Int,
postType: PostType,
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
if (nickName.isNullOrEmpty()) {
CardSvc.getProfileCard(record.senderUin.toString()).onSuccess {
nickName = it.strNick ?: record.peerName
}
}
transMessageEvent(record,
MessageEvent(
time = record.msgTime,
selfId = botUin,
postType = postType,
messageType = MsgType.Guild,
subType = MsgSubType.Channel,
messageId = msgHash,
targetId = record.peerUin,
peerId = botUin,
userId = record.senderUid.toLong(),
message = if(ShamrockConfig.useCQ()) rawMsg.json
else elements.toSegments(record.chatType, record.guildId, record.channelId).map {
it.toJson()
}.json,
rawMessage = rawMsg,
font = 0,
sender = Sender(
userId = record.senderUid.toLong(),
nickname = nickName,
card = record.sendMemberName,
role = MemberRole.Member,
title = record.sendNickName,
level = record.roleId.toString(),
tinyId = record.senderUid
),
)
)
return true
}
} }
/** /**

View File

@ -39,19 +39,46 @@ internal object ShamrockConfig {
fun updateConfig(intent: Intent) { fun updateConfig(intent: Intent) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config") val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.apply { mmkv.apply {
putBoolean( "tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式 if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 putBoolean(
putBoolean( "ws", intent.getBooleanExtra("ws", false)) // 主动WS开关 "tablet",
putBoolean( "http", intent.getBooleanExtra("http", false)) // HTTP回调开关 intent.getBooleanExtra("tablet", false)
putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址 ) // 强制平板模式
putBoolean( "ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关 putInt("port", intent.getIntExtra("port", 5700)) // 主动HTTP端口
putBoolean( "use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码 putBoolean("ws", intent.getBooleanExtra("ws", false)) // 主动WS开关
putBoolean( "inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包 putBoolean(
putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式 "http",
intent.getBooleanExtra("http", false)
) // HTTP回调开关
putString(
"http_addr",
intent.getStringExtra("http_addr")
) // WebHook回调地址
putBoolean(
"ws_client",
intent.getBooleanExtra("ws_client", false)
) // 被动WS开关
putBoolean(
"use_cqcode",
intent.getBooleanExtra("use_cqcode", false)
) // 使用CQ码
putBoolean(
"inject_packet",
intent.getBooleanExtra("inject_packet", false)
) // 拦截无用包
putBoolean("debug", intent.getBooleanExtra("debug", false)) // 调试模式
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码
putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名
putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口
putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
}
Config.defaultToken = intent.getStringExtra("token") Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true) Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
val wsPort = intent.getIntExtra("ws_port", 5800) val wsPort = intent.getIntExtra("ws_port", 5800)
Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig( Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig(
address = "0.0.0.0", address = "0.0.0.0",
@ -59,28 +86,17 @@ internal object ShamrockConfig {
) else Config.activeWebSocket?.also { ) else Config.activeWebSocket?.also {
it.port = wsPort it.port = wsPort
} }
Config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", "")?.filter { address -> Config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", "")?.filter { address ->
address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://")) address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://"))
}?.map { }?.map {
ConnectionConfig(address = it) ConnectionConfig(address = it)
}?.toMutableList() }?.toMutableList()
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码
putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名
putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口
putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
putBoolean("isInit", true) putBoolean("isInit", true)
} }
updateConfig() if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
updateConfig()
}
} }
private val mmkv: MMKV private val mmkv: MMKV

View File

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

View File

@ -21,12 +21,15 @@ internal enum class MsgSubType {
@SerialName("group") GroupLess, @SerialName("group") GroupLess,
@SerialName("friend") Friend, @SerialName("friend") Friend,
@SerialName("other") Other, @SerialName("other") Other,
@SerialName("channel") Channel
} }
@Serializable @Serializable
internal enum class MsgType { internal enum class MsgType {
@SerialName("group") Group, @SerialName("group") Group,
@SerialName("private") Private @SerialName("private") Private,
@SerialName("guild") Guild
} }
@Serializable @Serializable
@ -94,4 +97,5 @@ internal data class Sender(
@SerialName("role") val role: MemberRole?, @SerialName("role") val role: MemberRole?,
@SerialName("title") val title: String, @SerialName("title") val title: String,
@SerialName("level") val level: String, @SerialName("level") val level: String,
@SerialName("tiny_id") val tinyId: String = "0",
) )

View File

@ -38,8 +38,6 @@ internal object AioListener : IKernelMsgListener {
private suspend fun handleMsg(record: MsgRecord) { private suspend fun handleMsg(record: MsgRecord) {
try { try {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
messageLessListenerMap.firstNotNullOfOrNull { messageLessListenerMap.firstNotNullOfOrNull {
if (it.key == record.msgSeq) it else null if (it.key == record.msgSeq) it else null
}?.let { }?.let {
@ -50,17 +48,22 @@ internal object AioListener : IKernelMsgListener {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
MessageHelper.saveMsgMapping( MessageHelper.saveMsgMapping(
hash = msgHash, hash = msgHash,
qqMsgId = record.msgId, qqMsgId = record.msgId,
chatType = record.chatType, chatType = record.chatType,
subChatType = record.chatType, subChatType = record.chatType,
peerId = record.peerUin.toString(), peerId = peerId,
msgSeq = record.msgSeq.toInt(), msgSeq = record.msgSeq.toInt(),
time = record.msgTime time = record.msgTime,
subPeerId = record.channelId ?: peerId
) )
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId)
if (rawMsg.isEmpty()) return if (rawMsg.isEmpty()) return
if (ShamrockConfig.aliveReply() && rawMsg == "ping") { if (ShamrockConfig.aliveReply() && rawMsg == "ping") {
@ -119,6 +122,16 @@ internal object AioListener : IKernelMsgListener {
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
MsgConstant.KCHATTYPEGUILD -> {
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)
) {
LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") else -> LogCenter.log("不支持PUSH事件: ${record.chatType}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -131,21 +144,25 @@ internal object AioListener : IKernelMsgListener {
} }
override fun onAddSendMsg(record: MsgRecord) { override fun onAddSendMsg(record: MsgRecord) {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
if (record.peerUin == TicketSvc.getLongUin()) return // 发给自己的消息不处理 if (record.peerUin == TicketSvc.getLongUin()) return // 发给自己的消息不处理
GlobalScope.launch { GlobalScope.launch {
try { try {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
MessageHelper.saveMsgMapping( MessageHelper.saveMsgMapping(
hash = msgHash, hash = msgHash,
qqMsgId = record.msgId, qqMsgId = record.msgId,
chatType = record.chatType, chatType = record.chatType,
subChatType = record.chatType, subChatType = record.chatType,
peerId = record.peerUin.toString(), peerId = peerId,
msgSeq = record.msgSeq.toInt(), msgSeq = record.msgSeq.toInt(),
time = record.msgTime time = record.msgTime,
subPeerId = record.channelId ?: peerId
) )
LogCenter.log("预发送消息($msgHash | ${record.msgSeq} | ${record.msgId})") LogCenter.log("预发送消息($msgHash | ${record.msgSeq} | ${record.msgId})")
@ -167,6 +184,10 @@ internal object AioListener : IKernelMsgListener {
GlobalScope.launch { GlobalScope.launch {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) 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) val mapping = MessageHelper.getMsgMappingByHash(msgHash)
if (mapping == null) { if (mapping == null) {
@ -175,9 +196,10 @@ internal object AioListener : IKernelMsgListener {
qqMsgId = record.msgId, qqMsgId = record.msgId,
chatType = record.chatType, chatType = record.chatType,
subChatType = record.chatType, subChatType = record.chatType,
peerId = record.peerUin.toString(), peerId = peerId,
msgSeq = record.msgSeq.toInt(), msgSeq = record.msgSeq.toInt(),
time = record.msgTime time = record.msgTime,
subPeerId = record.channelId ?: peerId
) )
} else { } else {
LogCenter.log("Update message info from ${mapping.msgSeq} to ${record.msgSeq}", Level.INFO) LogCenter.log("Update message info from ${mapping.msgSeq} to ${record.msgSeq}", Level.INFO)
@ -190,7 +212,7 @@ internal object AioListener : IKernelMsgListener {
|| record.peerUin == TicketSvc.getLongUin() || record.peerUin == TicketSvc.getLongUin()
) return@launch ) return@launch
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId)
if (rawMsg.isEmpty()) return@launch if (rawMsg.isEmpty()) return@launch
LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)") LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)")