6 Commits

Author SHA1 Message Date
252a3527a8 Merge remote-tracking branch 'origin/master' 2024-02-25 17:32:07 +08:00
ea4cf06edf Shamrock: 修复download_file指定名称失败
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 17:31:57 +08:00
1424efd7f8 send_forward_msg(support image) 2024-02-25 14:33:59 +08:00
eb807a0332 Shamrock: support upload resource by NtKernel x3
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 12:46:40 +08:00
e9a3a82b68 Shamrock: support upload resource by NtKernel x2
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 12:40:39 +08:00
fca66f3259 Shamrock: support upload resource by NtKernel
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 11:42:42 +08:00
31 changed files with 673 additions and 225 deletions

View File

@ -39,11 +39,26 @@ data class CustomFace(
@ProtoNumber(32) var width400: UInt? = null, @ProtoNumber(32) var width400: UInt? = null,
@ProtoNumber(33) var height400: UInt? = null, @ProtoNumber(33) var height400: UInt? = null,
@ProtoNumber(34) var pbReserve: PbReserve? = null, @ProtoNumber(34) var pbReserve: PbReserve? = null,
){ ) {
companion object{ companion object {
@Serializable @Serializable
data class PbReserve( data class PbReserve(
@ProtoNumber(1) var field1: Int? = null @ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(10) var field10: Int? = null,
@ProtoNumber(21) var field21: Object1? = null,
@ProtoNumber(31) var field31: String? = null
)
@Serializable
data class Object1(
@ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(2) var field2: String? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(5) var field5: Int? = null,
@ProtoNumber(7) var md5Str: String? = null
) )
} }
} }

View File

@ -5,16 +5,16 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class NotOnlineImage( data class NotOnlineImage(
@ProtoNumber(1) val filePath: ByteArray? = null, @ProtoNumber(1) val filePath: String? = null,
@ProtoNumber(2) val fileLen: UInt? = null, @ProtoNumber(2) val fileLen: UInt? = null,
@ProtoNumber(3) val downloadPath: ByteArray? = null, @ProtoNumber(3) val downloadPath: String? = null,
@ProtoNumber(4) val oldVerSendFile: ByteArray? = null, @ProtoNumber(4) val oldVerSendFile: ByteArray? = null,
@ProtoNumber(5) val imgType: UInt? = null, @ProtoNumber(5) val imgType: UInt? = null,
@ProtoNumber(6) val previewsImage: ByteArray? = null, @ProtoNumber(6) val previewsImage: ByteArray? = null,
@ProtoNumber(7) val picMd5: ByteArray? = null, @ProtoNumber(7) val picMd5: ByteArray? = null,
@ProtoNumber(8) val picHeight: UInt? = null, @ProtoNumber(8) val picHeight: UInt? = null,
@ProtoNumber(9) val picWidth: UInt? = null, @ProtoNumber(9) val picWidth: UInt? = null,
@ProtoNumber(10) val resId: ByteArray? = null, // md5 + ".jpg" @ProtoNumber(10) val resId: String? = null, // md5 + ".jpg"
@ProtoNumber(11) val flag: ByteArray? = null, @ProtoNumber(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = null, @ProtoNumber(12) val thumbUrl: String? = null,
@ProtoNumber(13) val original: Boolean? = null, @ProtoNumber(13) val original: Boolean? = null,
@ -39,8 +39,23 @@ data class NotOnlineImage(
@Serializable @Serializable
data class PbReserve( data class PbReserve(
@ProtoNumber(1) var field1: Int? = null, @ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(8) var field8: String? = null, @ProtoNumber(8) var field8: String? = null,
@ProtoNumber(30) var url: String? = null @ProtoNumber(10) var field10: Int? = null,
@ProtoNumber(20) var field20: Object1? = null,
@ProtoNumber(30) var url: String? = null,
@ProtoNumber(31) var md5Str: String? = null
)
@Serializable
data class Object1(
@ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(2) var field2: String? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(5) var field5: Int? = null,
@ProtoNumber(7) var field7: String? = null
) )
} }
} }

View File

@ -6,7 +6,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
public interface IKernelMsgService { public interface IKernelMsgService {
void deleteMsg(Contact contact, ArrayList<Long> msgIdList, IOperateCallback callback); void deleteMsg(Contact contact, ArrayList<Long> msgIdList, IOperateCallback cb);
void fetchLongMsg(Contact contact, long msgId); void fetchLongMsg(Contact contact, long msgId);

View File

@ -9,6 +9,7 @@ import io.ktor.utils.io.core.writeFully
import io.ktor.utils.io.core.writeInt import io.ktor.utils.io.core.writeInt
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
@ -80,11 +81,11 @@ internal object PacketSvc: BaseSvc() {
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray()) fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray())
return withTimeoutOrNull(5000L) { return withTimeoutOrNull(5000L) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
AioListener.registerTemporaryMsgListener(msgSeq) { MessageTempHandler.registerTemporaryMsgListener(msgSeq) {
it.resume(this.msgId) it.resume(this.msgId)
} }
it.invokeOnCancellation { it.invokeOnCancellation {
AioListener.unregisterTemporaryMsgListener(msgSeq) MessageTempHandler.unregisterTemporaryMsgListener(msgSeq)
} }
} }
} ?: -1L } ?: -1L

View File

@ -1,16 +1,9 @@
package moe.fuqiuluo.qqinterface.servlet.ark package moe.fuqiuluo.qqinterface.servlet.ark
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal object ArkMsgSvc: BaseSvc() { internal object ArkMsgSvc: BaseSvc() {
fun tryShareMusic( fun tryShareMusic(

View File

@ -6,10 +6,10 @@ import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.encodeURLQueryComponent import io.ktor.http.encodeURLQueryComponent
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.ark.data.Region
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*

View File

@ -1,4 +1,5 @@
package moe.fuqiuluo.qqinterface.servlet.ark package moe.fuqiuluo.qqinterface.servlet.ark.data
sealed class ArkAppInfo( sealed class ArkAppInfo(
val appId: Long, val appId: Long,
val version: String, val version: String,

View File

@ -1,4 +1,4 @@
package moe.fuqiuluo.qqinterface.servlet.ark package moe.fuqiuluo.qqinterface.servlet.ark.data
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@ -2,7 +2,7 @@ package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.MsgElementConverter import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
@ -56,7 +56,7 @@ internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String,
val messageData = arrayListOf<MessageSegment>() val messageData = arrayListOf<MessageSegment>()
this.forEach { msg -> this.forEach { msg ->
kotlin.runCatching { kotlin.runCatching {
val converter = MsgElementConverter[msg.elementType] val converter = NtMsgElementConverter[msg.elementType]
converter?.invoke(chatType, peerId, subPeer, msg) converter?.invoke(chatType, peerId, subPeer, msg)
?: throw UnsupportedOperationException("不支持的消息element类型${msg.elementType}") ?: throw UnsupportedOperationException("不支持的消息element类型${msg.elementType}")
}.onSuccess { }.onSuccess {

View File

@ -0,0 +1,34 @@
package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import java.util.Collections
internal object MessageTempHandler {
// 通过MSG SEQ临时监听器
private val tempMessageListenerMap = Collections.synchronizedMap(HashMap<Long, suspend MsgRecord.() -> Unit>())
fun registerTemporaryMsgListener(
msgSeq: Long,
listener: suspend MsgRecord.() -> Unit
) {
LogCenter.log({ "注册临时消息监听器: $msgSeq" }, Level.DEBUG)
tempMessageListenerMap[msgSeq] = listener
}
fun unregisterTemporaryMsgListener(msgSeq: Long) {
tempMessageListenerMap.remove(msgSeq)
}
suspend fun notify(record: MsgRecord): Boolean {
tempMessageListenerMap.firstNotNullOfOrNull {
if (it.key == record.msgSeq) it else null
}?.let {
it.value(record)
tempMessageListenerMap.remove(it.key)
return true
}
return false
}
}

View File

@ -21,23 +21,23 @@ import moe.fuqiuluo.shamrock.tools.hex2ByteArray
internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment
internal object MsgElementConverter { internal object NtMsgElementConverter {
private val convertMap = hashMapOf( private val convertMap = hashMapOf(
MsgConstant.KELEMTYPETEXT to MsgElementConverter::convertTextElem, MsgConstant.KELEMTYPETEXT to NtMsgElementConverter::convertTextElem,
MsgConstant.KELEMTYPEFACE to MsgElementConverter::convertFaceElem, MsgConstant.KELEMTYPEFACE to NtMsgElementConverter::convertFaceElem,
MsgConstant.KELEMTYPEPIC to MsgElementConverter::convertImageElem, MsgConstant.KELEMTYPEPIC to NtMsgElementConverter::convertImageElem,
MsgConstant.KELEMTYPEPTT to MsgElementConverter::convertVoiceElem, MsgConstant.KELEMTYPEPTT to NtMsgElementConverter::convertVoiceElem,
MsgConstant.KELEMTYPEVIDEO to MsgElementConverter::convertVideoElem, MsgConstant.KELEMTYPEVIDEO to NtMsgElementConverter::convertVideoElem,
MsgConstant.KELEMTYPEMARKETFACE to MsgElementConverter::convertMarketFaceElem, MsgConstant.KELEMTYPEMARKETFACE to NtMsgElementConverter::convertMarketFaceElem,
MsgConstant.KELEMTYPEARKSTRUCT to MsgElementConverter::convertStructJsonElem, MsgConstant.KELEMTYPEARKSTRUCT to NtMsgElementConverter::convertStructJsonElem,
MsgConstant.KELEMTYPEREPLY to MsgElementConverter::convertReplyElem, MsgConstant.KELEMTYPEREPLY to NtMsgElementConverter::convertReplyElem,
MsgConstant.KELEMTYPEGRAYTIP to MsgElementConverter::convertGrayTipsElem, MsgConstant.KELEMTYPEGRAYTIP to NtMsgElementConverter::convertGrayTipsElem,
MsgConstant.KELEMTYPEFILE to MsgElementConverter::convertFileElem, MsgConstant.KELEMTYPEFILE to NtMsgElementConverter::convertFileElem,
MsgConstant.KELEMTYPEMARKDOWN to MsgElementConverter::convertMarkdownElem, MsgConstant.KELEMTYPEMARKDOWN to NtMsgElementConverter::convertMarkdownElem,
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem, //MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem, //MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
MsgConstant.KELEMTYPEFACEBUBBLE to MsgElementConverter::convertBubbleFaceElem, MsgConstant.KELEMTYPEFACEBUBBLE to NtMsgElementConverter::convertBubbleFaceElem,
MsgConstant.KELEMTYPEINLINEKEYBOARD to MsgElementConverter::convertInlineKeyboardElem MsgConstant.KELEMTYPEINLINEKEYBOARD to NtMsgElementConverter::convertInlineKeyboardElem
) )
operator fun get(type: Int): IMsgElementConverter? = convertMap[type] operator fun get(type: Int): IMsgElementConverter? = convertMap[type]

View File

@ -2,7 +2,7 @@ package moe.fuqiuluo.qqinterface.servlet.msg.maker
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.tencent.qqnt.kernel.nativeinterface.* import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.qqinterface.servlet.CardSvc import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
@ -11,23 +11,12 @@ import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
import moe.fuqiuluo.qqinterface.servlet.msg.toJson import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.* import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer
import moe.fuqiuluo.qqinterface.servlet.transfile.Troop
import moe.fuqiuluo.shamrock.helper.* import moe.fuqiuluo.shamrock.helper.*
import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.message.Elem import protobuf.message.Elem
import protobuf.message.element.* import protobuf.message.element.*
@ -36,8 +25,9 @@ import java.io.File
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextULong import kotlin.random.nextULong
import kotlin.time.Duration.Companion.seconds
internal typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem> internal typealias IElemMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal object ElemMaker { internal object ElemMaker {
private val makerArray = hashMapOf( private val makerArray = hashMapOf(
@ -46,30 +36,30 @@ internal object ElemMaker {
"face" to ElemMaker::createFaceElem, "face" to ElemMaker::createFaceElem,
"pic" to ElemMaker::createImageElem, "pic" to ElemMaker::createImageElem,
"image" to ElemMaker::createImageElem, "image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem, // "voice" to ElemMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem, // "record" to ElemMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem, // "video" to ElemMaker::createVideoElem,
"markdown" to ElemMaker::createMarkdownElem, "markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem, "button" to ElemMaker::createButtonElem,
"inline_keyboard" to ElemMaker::createButtonElem, "inline_keyboard" to ElemMaker::createButtonElem,
"dice" to ElemMaker::createNewDiceElem, "dice" to ElemMaker::createNewDiceElem,
"rps" to ElemMaker::createNewRpsElem, "rps" to ElemMaker::createNewRpsElem,
"poke" to ElemMaker::createPokeElem, "poke" to ElemMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem, // "anonymous" to ElemMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem, // "share" to ElemMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem, // "contact" to ElemMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem, // "location" to ElemMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem, // "music" to ElemMaker::createMusicElem,
"reply" to ElemMaker::createReplyElem, "reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem, // "touch" to ElemMaker::createTouchElem,
"weather" to ElemMaker::createWeatherElem, "weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem, "json" to ElemMaker::createJsonElem,
// "node" to MessageMaker::createNodeElem, //"forward" to MessageMaker::createForwardElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to MessageElementMaker::createBubbleFaceElem, //"bubble_face" to ElemMaker::createBubbleFaceElem,
) )
operator fun get(type: String): IMessageElementMaker? = makerArray[type] operator fun get(type: String): IElemMaker? = makerArray[type]
private suspend fun createTextElem( private suspend fun createTextElem(
chatType: Int, chatType: Int,
@ -226,26 +216,6 @@ internal object ElemMaker {
} }
requireNotNull(file) requireNotNull(file)
val md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, md5HexStr, file.name, 1, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath
) != file.length()
) {
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, md5HexStr, file.name, 2, 720, null, "", true
)
)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath)
}
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
options.inJustDecodeBounds = true options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options) BitmapFactory.decodeFile(file.absolutePath, options)
@ -264,53 +234,77 @@ internal object ElemMaker {
picHeight = options.outWidth picHeight = options.outWidth
} }
val elem = when (chatType) { val uploadRet = NtV2RichMediaSvc.tryUploadResourceByNt(
MsgConstant.KCHATTYPEGROUP -> { chatType = chatType,
Transfer with Troop(peerId) trans PictureResource(file) elementType = MsgConstant.KELEMTYPEPIC,
Elem( resources = arrayListOf(file),
customFace = CustomFace( timeout = 30.seconds
filePath = "${md5HexStr.substring(0, 8)}-${md5HexStr.substring(8, 4)}-${ ).getOrThrow().first()
md5HexStr.substring( LogCenter.log(uploadRet.toString(), Level.DEBUG)
12,
4
)
}-${md5HexStr.substring(16, 4)}-${md5HexStr.substring(20, 12)}.${FileUtils.getFileType(file)}",
fileId = 0u,
serverIp = 0u,
serverPort = 0u,
fileType = 1001u,
useful = 1u,
md5 = md5HexStr.hex2ByteArray(),
bizType = data["subType"].asIntOrNull?.toUInt(),
imageType = FileUtils.getPicType(file).toUInt(),
width = picWidth.toUInt(),
height = picHeight.toUInt(),
size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(field1 = 0)
)
)
}
MsgConstant.KCHATTYPEC2C -> { val elem = when (chatType) {
Transfer with Private(peerId) trans PictureResource(file) MsgConstant.KCHATTYPEGROUP -> Elem(
Elem( customFace = CustomFace(
notOnlineImage = NotOnlineImage( filePath = uploadRet.fileName,
filePath = "${md5HexStr}.${FileUtils.getFileType(file)}".toByteArray(), fileId = uploadRet.uuid.toUInt(),
fileLen = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(), serverIp = 0u,
downloadPath = "".toByteArray(), serverPort = 0u,
imgType = FileUtils.getPicType(file).toUInt(), fileType = FileUtils.getPicType(file).toUInt(),
picMd5 = md5HexStr.hex2ByteArray(), useful = 1u,
picHeight = picWidth.toUInt(), md5 = uploadRet.md5.hex2ByteArray(),
picWidth = picHeight.toUInt(), bizType = data["subType"].asIntOrNull?.toUInt(),
resId = "".toByteArray(), imageType = FileUtils.getPicType(file).toUInt(),
original = isOriginal, // true width = picWidth.toUInt(),
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0) height = picHeight.toUInt(),
size = uploadRet.fileSize.toUInt(),
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(
field1 = 0,
field3 = 0,
field4 = 0,
field10 = 0,
field21 = CustomFace.Companion.Object1(
field1 = 0,
field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
md5Str = uploadRet.md5
)
) )
) )
} )
MsgConstant.KCHATTYPEC2C -> Elem(
notOnlineImage = NotOnlineImage(
filePath = uploadRet.fileName,
fileLen = uploadRet.fileSize.toUInt(),
downloadPath = uploadRet.uuid,
imgType = FileUtils.getPicType(file).toUInt(),
picMd5 = uploadRet.md5.hex2ByteArray(),
picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(),
resId = uploadRet.uuid,
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(
field1 = 0,
field3 = 0,
field4 = 0,
field10 = 0,
field20 = NotOnlineImage.Companion.Object1(
field1 = 0,
field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
field7 = "",
),
md5Str = uploadRet.md5
)
)
)
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg") else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
} }

View File

@ -3,7 +3,6 @@ package moe.fuqiuluo.qqinterface.servlet.msg.maker
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.app.QQAppInterface import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.emoticon.QQSysFaceUtil import com.tencent.mobileqq.emoticon.QQSysFaceUtil
import com.tencent.mobileqq.pb.ByteStringMicro import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.mobileqq.qroute.QRoute import com.tencent.mobileqq.qroute.QRoute
@ -17,17 +16,17 @@ import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.CardSvc import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.LbsSvc import moe.fuqiuluo.qqinterface.servlet.LbsSvc
import moe.fuqiuluo.qqinterface.servlet.ark.ArkAppInfo import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.* import moe.fuqiuluo.qqinterface.servlet.transfile.*
import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private import moe.fuqiuluo.qqinterface.servlet.transfile.data.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer
import moe.fuqiuluo.qqinterface.servlet.transfile.Troop import moe.fuqiuluo.qqinterface.servlet.transfile.data.Troop
import moe.fuqiuluo.qqinterface.servlet.transfile.VideoResource import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
import moe.fuqiuluo.qqinterface.servlet.transfile.VoiceResource import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource
import moe.fuqiuluo.shamrock.helper.ActionMsgException import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.IllegalParamsException import moe.fuqiuluo.shamrock.helper.IllegalParamsException
@ -84,7 +83,6 @@ internal object NtMsgElementMaker {
"new_dice" to NtMsgElementMaker::createNewDiceElem, "new_dice" to NtMsgElementMaker::createNewDiceElem,
"new_rps" to NtMsgElementMaker::createNewRpsElem, "new_rps" to NtMsgElementMaker::createNewRpsElem,
"basketball" to NtMsgElementMaker::createBasketballElem, "basketball" to NtMsgElementMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to NtMsgElementMaker::createBubbleFaceElem, "bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
"button" to NtMsgElementMaker::createInlineKeywordElem, "button" to NtMsgElementMaker::createInlineKeywordElem,
@ -180,17 +178,6 @@ internal object NtMsgElementMaker {
return Result.success(elem) return Result.success(elem)
} }
// private suspend fun createNodeElem(
// chatType: Int,
// msgId: Long,
// peerId: String,
// data: JsonObject
// ): Result<MsgElement> {
// data.checkAndThrow("data")
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
//
// }
private suspend fun createBasketballElem( private suspend fun createBasketballElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,

View File

@ -0,0 +1,20 @@
package moe.fuqiuluo.qqinterface.servlet.structures
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UploadResult(
@SerialName("files") val files: List<CommFileInfo>
)
@Serializable
data class CommFileInfo(
@SerialName("mode_id") val modeId: Long,
@SerialName("name") val fileName: String,
@SerialName("size") val fileSize: Long,
@SerialName("md5") val md5: String,
@SerialName("uuid") val uuid: String,
@SerialName("sub_id") val subId: String,
@SerialName("sha") val sha: String,
)

View File

@ -1,14 +1,46 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.FileElement
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.PicElement
import com.tencent.qqnt.kernel.nativeinterface.PttElement
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
import com.tencent.qqnt.msg.api.IMsgUtilApi
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.serialization.SerialName import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.Serializable import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Troop
import moe.fuqiuluo.qqinterface.servlet.transfile.data.TryUpPicData
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.AudioUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.utils.MediaType
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
import moe.fuqiuluo.symbols.decodeProtobuf import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb import protobuf.oidb.TrpcOidb
@ -31,13 +63,248 @@ import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody import protobuf.oidb.cmd0x388.Cmd0x388RspBody
import protobuf.oidb.cmd0x388.TryUpImgReq import protobuf.oidb.cmd0x388.TryUpImgReq
import java.io.File import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextUInt import kotlin.random.nextUInt
import kotlin.random.nextULong import kotlin.random.nextULong
import kotlin.time.Duration
internal object NtV2RichMediaSvc: BaseSvc() { internal object NtV2RichMediaSvc: BaseSvc() {
private const val GROUP_PIC_UPLOAD_TO = "100000000"
private val requestIdSeq = atomic(2L) private val requestIdSeq = atomic(2L)
/**
* 批量上传图片
*/
suspend fun tryUploadResourceByNt(
chatType: Int,
elementType: Int,
resources: ArrayList<File>,
timeout: Duration
): Result<MutableList<CommonFileInfo>> {
require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" }
val messages = resources.map { file ->
val elem = MsgElement()
elem.elementType = elementType
when(elementType) {
MsgConstant.KELEMTYPEPIC -> {
val pic = PicElement()
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath
) != file.length()
) {
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
)
)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath)
}
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options)
val exifInterface = ExifInterface(file.absolutePath)
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
pic.picWidth = options.outWidth
pic.picHeight = options.outHeight
} else {
pic.picWidth = options.outHeight
pic.picHeight = options.outWidth
}
pic.sourcePath = file.absolutePath
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath)
pic.original = true
pic.picType = FileUtils.getPicType(file)
elem.picElement = pic
}
MsgConstant.KELEMTYPEPTT -> {
require(resources.size == 1) // 语音只能单个上传
var pttFile = file
val ptt = PttElement()
when (AudioUtils.getMediaType(pttFile)) {
MediaType.Silk -> {
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
ptt.duration = QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(pttFile.absolutePath)
}
MediaType.Amr -> {
ptt.duration = AudioUtils.getDurationSec(pttFile)
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
}
MediaType.Pcm -> {
val result = AudioUtils.pcmToSilk(pttFile)
ptt.duration = (result.second * 0.001).roundToInt()
pttFile = result.first
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
else -> {
val result = AudioUtils.audioToSilk(pttFile)
ptt.duration = runCatching {
QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(result.second.absolutePath)
}.getOrElse {
result.first
}
pttFile = result.second
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
}
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) {
QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath)
}
if (originalPath != null) {
ptt.filePath = originalPath
} else {
ptt.filePath = pttFile.absolutePath
}
ptt.canConvert2Text = true
ptt.fileId = 0
ptt.fileUuid = ""
ptt.text = ""
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
elem.pttElement = ptt
}
MsgConstant.KELEMTYPEVIDEO -> {
require(resources.size == 1) // 视频只能单个上传
val video = VideoElement()
video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
5, 2, video.videoMd5, file.name, 1, 0, null, "", true
)
)
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
5, 1, video.videoMd5, file.name, 2, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath
) != file.length()
) {
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
}
video.fileTime = AudioUtils.getVideoTime(file)
video.fileSize = file.length()
video.fileName = file.name
video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4
video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt()
val options = BitmapFactory.Options()
BitmapFactory.decodeFile(thumbPath, options)
video.thumbWidth = options.outWidth
video.thumbHeight = options.outHeight
video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath)
video.thumbPath = hashMapOf(0 to thumbPath)
elem.videoElement = video
}
/*MsgConstant.KELEMTYPEFILE -> {
require(resources.size == 1) // 文件只能单个上传
val fileElement = FileElement()
fileElement.fileMd5 = ""
fileElement.fileName = file.name
fileElement.filePath = file.absolutePath
fileElement.fileSize = file.length()
fileElement.picWidth = 0
fileElement.picHeight = 0
fileElement.videoDuration = 0
fileElement.picThumbPath = HashMap()
fileElement.expireTime = 0L
fileElement.fileSha = ""
fileElement.fileSha3 = ""
fileElement.file10MMd5 = ""
when (TransfileHelper.getExtensionId(file.name)) {
0 -> {
val wh = QRoute.api(IMsgUtilApi::class.java)
.getPicSizeByPath(file.absolutePath)
fileElement.picWidth = wh.first
fileElement.picHeight = wh.second
fileElement.picThumbPath[750] = file.absolutePath
}
2 -> {
val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(file.absolutePath))
withContext(Dispatchers.IO) {
val fileOutputStream = FileOutputStream(thumbPic)
val retriever = MediaMetadataRetriever()
retriever.setDataSource(fileElement.filePath)
retriever.frameAtTime?.compress(Bitmap.CompressFormat.JPEG, 60, fileOutputStream)
fileOutputStream.flush()
fileOutputStream.close()
}
val options = BitmapFactory.Options()
BitmapFactory.decodeFile(thumbPic.absolutePath, options)
fileElement.picHeight = options.outHeight
fileElement.picWidth = options.outWidth
fileElement.picThumbPath = hashMapOf(750 to thumbPic.absolutePath)
}
}
elem.fileElement = fileElement
}*/
else -> throw IllegalArgumentException("unsupported elementType: $elementType")
}
return@map elem
}
if (messages.isEmpty()) {
return Result.failure(Exception("no valid image files"))
}
val contact = when(chatType) {
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin())
else -> Contact(chatType, GROUP_PIC_UPLOAD_TO, GROUP_PIC_UPLOAD_TO)
}
val result = mutableListOf<CommonFileInfo>()
withTimeoutOrNull(timeout) {
suspendCancellableCoroutine {
val uniseq = MessageHelper.generateMsgId(chatType)
RichMediaUploadHandler.registerListener(uniseq.qqMsgId) upload@{
if (uniseq.qqMsgId == msgId) {
result.add(commonFileInfo)
}
if (result.size == resources.size) {
it.resume(true)
return@upload true
}
return@upload false
}
MessageHelper.sendMessageWithMsgId(
contact = contact,
message = ArrayList(messages),
uniseq = uniseq.qqMsgId
) { _, _ -> }
it.invokeOnCancellation {
RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
}
}
}
return Result.success(result)
}
/** /**
* 获取NT图片的RKEY * 获取NT图片的RKEY
*/ */
@ -173,6 +440,9 @@ internal object NtV2RichMediaSvc: BaseSvc() {
LogCenter.log("requestUploadPic => rsp: $rsp") LogCenter.log("requestUploadPic => rsp: $rsp")
} }
/**
* 使用OldBDH获取图片上传状态以及图片上传服务器
*/
suspend fun requestUploadGroupPic( suspend fun requestUploadGroupPic(
groupId: ULong, groupId: ULong,
md5: String, md5: String,
@ -215,13 +485,5 @@ internal object NtV2RichMediaSvc: BaseSvc() {
) )
} }
} }
@Serializable
data class TryUpPicData(
@SerialName("ukey") val uKey: ByteArray,
@SerialName("exist") val exist: Boolean,
@SerialName("file_id") val fileId: ULong,
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
)
} }

View File

@ -1,4 +1,4 @@
package moe.fuqiuluo.shamrock.remote.service.api package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo

View File

@ -1,14 +1,19 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.data.MessageForShortVideo import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File import java.io.File
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.* import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType.*
import moe.fuqiuluo.shamrock.helper.TransfileHelper import moe.fuqiuluo.qqinterface.servlet.transfile.data.ContactType
import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Resource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType
import moe.fuqiuluo.qqinterface.servlet.transfile.data.TransTarget
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource
internal object Transfer: FileTransfer() { internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>( private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(

View File

@ -1,4 +1,4 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile.data
import com.tencent.mobileqq.data.MessageRecord import com.tencent.mobileqq.data.MessageRecord

View File

@ -1,4 +1,4 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile.data
import java.io.File import java.io.File

View File

@ -0,0 +1,13 @@
package moe.fuqiuluo.qqinterface.servlet.transfile.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TryUpPicData(
@SerialName("ukey") val uKey: ByteArray,
@SerialName("exist") val exist: Boolean,
@SerialName("file_id") val fileId: ULong,
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
)

View File

@ -205,27 +205,24 @@ internal object MessageHelper {
fun sendMessageWithMsgId( fun sendMessageWithMsgId(
contact: Contact, contact: Contact,
message: ArrayList<MsgElement>, message: ArrayList<MsgElement>,
uniseq: Long,
callback: IOperateCallback callback: IOperateCallback
): SendMsgResult { ): SendMsgResult {
val uniseq = generateMsgId(contact.chatType)
val nonMsg: Boolean = message.isEmpty() val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) { if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java) val service = QRoute.api(IMsgService::class.java)
if (callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.msgHashId
}
service.sendMsg( service.sendMsg(
contact, contact,
uniseq.qqMsgId, uniseq,
message, message,
callback callback
) )
uniseq.copy(msgTime = System.currentTimeMillis())
} else {
uniseq.copy(msgTime = 0, msgHashId = 0)
} }
return SendMsgResult(
msgTime = if (nonMsg) 0 else System.currentTimeMillis(),
msgHashId = 0,
qqMsgId = uniseq
)
} }
suspend fun sendMessageNoCb( suspend fun sendMessageNoCb(

View File

@ -3,7 +3,7 @@ package moe.fuqiuluo.shamrock.helper
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import moe.fuqiuluo.qqinterface.servlet.ark.ArkAppInfo import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.shamrock.tools.GlobalClient import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt import moe.fuqiuluo.shamrock.tools.asInt

View File

@ -49,7 +49,7 @@ internal object DownloadFile: IActionHandler() {
headerMap[k] = v headerMap[k] = v
} }
} }
return invoke(url, threadCnt, headerMap, echo) return invoke(url, threadCnt, headerMap, name, echo)
} else if (base64 != null) { } else if (base64 != null) {
return invoke(base64, name, echo) return invoke(base64, name, echo)
} else { } else {
@ -88,6 +88,7 @@ internal object DownloadFile: IActionHandler() {
url: String, url: String,
threadCnt: Int, threadCnt: Int,
headers: Map<String, String>, headers: Map<String, String>,
name: String?,
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
return kotlin.runCatching { return kotlin.runCatching {
@ -100,7 +101,13 @@ internal object DownloadFile: IActionHandler() {
)) { )) {
return error("下载失败 (0x1)", echo) return error("下载失败 (0x1)", echo)
} }
tmp = FileUtils.renameByMd5(tmp) tmp = if (name == null) {
FileUtils.renameByMd5(tmp)
} else {
val newFile = tmp.parentFile!!.resolve(name)
tmp.renameTo(newFile)
newFile
}
ok(data = DownloadResult( ok(data = DownloadResult(
file = tmp.absolutePath, file = tmp.absolutePath,
md5 = MD5.genFileMd5Hex(tmp.absolutePath) md5 = MD5.genFileMd5Hex(tmp.absolutePath)

View File

@ -146,11 +146,13 @@ internal object SendForwardMessage : IActionHandler() {
).also { ).also {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
}.map { }.map {
desc[++i] += when (it.type) { desc[i] += when (it.type) {
"text" -> it.data["text"] as String "text" -> it.data["text"] as String
"at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}" "at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> "[表情]" "face" -> "[表情]"
"voice" -> "[语音]" "pic", "image" -> "[图片]"
"voice", "record" -> "[语音]"
"video" -> "[视频]"
"node" -> "[合并转发消息]" "node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]" "markdown" -> "[Markdown消息]"
"button" -> "[Button类型]" "button" -> "[Button类型]"
@ -197,7 +199,7 @@ internal object SendForwardMessage : IActionHandler() {
body = MsgBody( body = MsgBody(
richText = RichText( richText = RichText(
elements = MessageHelper.messageArrayToMessageElements( elements = MessageHelper.messageArrayToMessageElements(
chatType = MsgConstant.KCHATTYPEGROUP, chatType = chatType,
msgId = Random.nextLong(), msgId = Random.nextLong(),
peerId = data["uin"]?.asString ?: TicketSvc.getUin(), peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
messageList = when (data["content"]) { messageList = when (data["content"]) {
@ -207,7 +209,7 @@ internal object SendForwardMessage : IActionHandler() {
}.also { }.also {
desc[++i] = desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname() )+ ": " ?: TicketSvc.getNickname()) + ": "
}.onEach { }.onEach {
val type = it.asJsonObject["type"].asString val type = it.asJsonObject["type"].asString
val itData = it.asJsonObject["data"].asJsonObject val itData = it.asJsonObject["data"].asJsonObject
@ -215,8 +217,9 @@ internal object SendForwardMessage : IActionHandler() {
"text" -> itData["text"].asString "text" -> itData["text"].asString
"at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}" "at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> "[表情]" "face" -> "[表情]"
"image" -> "[图片]" "pic", "image" -> "[图片]"
"voice" -> "[语音]" "voice", "record" -> "[语音]"
"video" -> "[视频]"
"node" -> "[合并转发消息]" "node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]" "markdown" -> "[Markdown消息]"
"button" -> "[Button类型]" "button" -> "[Button类型]"

View File

@ -24,8 +24,7 @@ internal object SendMsgByResid : IActionHandler() {
val resId = session.getString("res_id") val resId = session.getString("res_id")
val peerId = session.getString("peer_id") val peerId = session.getString("peer_id")
val messageType = session.getString("message_type") val messageType = session.getString("message_type")
invoke(resId, peerId, messageType) return invoke(peerId, resId, messageType, session.echo)
return ok("ok", session.echo)
} }
suspend operator fun invoke(peerId: String, resId: String, messageType: String, echo: JsonElement = EmptyJsonString): String { suspend operator fun invoke(peerId: String, resId: String, messageType: String, echo: JsonElement = EmptyJsonString): String {
@ -55,4 +54,6 @@ internal object SendMsgByResid : IActionHandler() {
BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, req.toByteArray()) BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, req.toByteArray())
return ok("ok", echo) return ok("ok", echo)
} }
override val requiredParams: Array<String> = arrayOf("res_id", "peer_id", "message_type")
} }

View File

@ -1,15 +1,18 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
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.jsonArray import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("send_private_msg", ["send_private_message"]) @OneBotHandler("send_private_msg", ["send_private_message", "send_friend_msg"])
internal object SendPrivateMessage : IActionHandler() { internal object SendPrivateMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getLong("user_id") val userId = session.getString("user_id").let {
if (it == "self") TicketSvc.getUin() else it
}
val groupId = session.getLongOrNull("group_id") val groupId = session.getLongOrNull("group_id")
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val retryCnt = session.getIntOrNull("retry_cnt") val retryCnt = session.getIntOrNull("retry_cnt")
@ -19,21 +22,21 @@ internal object SendPrivateMessage : IActionHandler() {
val message = session.getString("message") val message = session.getString("message")
SendMessage( SendMessage(
chatType = chatType, chatType = chatType,
peerId = userId.toString(), peerId = userId,
message = message, message = message,
autoEscape = autoEscape, autoEscape = autoEscape,
echo = session.echo, echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(), fromId = groupId?.toString() ?: userId,
retryCnt = retryCnt ?: 5, retryCnt = retryCnt ?: 5,
recallDuration = recallDuration recallDuration = recallDuration
) )
} else { } else {
SendMessage( SendMessage(
chatType = chatType, chatType = chatType,
peerId = userId.toString(), peerId = userId,
message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray, message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray,
echo = session.echo, echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(), fromId = groupId?.toString() ?: userId,
retryCnt = retryCnt ?: 5, retryCnt = retryCnt ?: 5,
recallDuration = recallDuration recallDuration = recallDuration
) )

View File

@ -17,12 +17,12 @@ 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
import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper import moe.fuqiuluo.shamrock.helper.TransfileHelper
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.remote.service.api.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
@ -55,6 +55,20 @@ internal object UploadGroupFile : IActionHandler() {
if (!srcFile.exists()) { if (!srcFile.exists()) {
srcFile = FileUtils.getFile(file) srcFile = FileUtils.getFile(file)
} }
if (!srcFile.exists()) {
srcFile = file.let {
val md5 = it.replace(
regex = "[{}\\-]".toRegex(),
replacement = ""
).split(".")[0].lowercase()
if (md5.length == 32) {
FileUtils.getFileByMd5(it)
} else {
FileUtils.parseAndSave(it)
}
}
}
if (!srcFile.exists()) { if (!srcFile.exists()) {
return badParam("文件不存在", echo) return badParam("文件不存在", echo)
} }
@ -64,6 +78,7 @@ internal object UploadGroupFile : IActionHandler() {
fileElement.fileName = name fileElement.fileName = name
fileElement.filePath = srcFile.absolutePath fileElement.filePath = srcFile.absolutePath
fileElement.fileSize = srcFile.length() fileElement.fileSize = srcFile.length()
fileElement.folderId = srcFile.parent ?: ""
fileElement.picWidth = 0 fileElement.picWidth = 0
fileElement.picHeight = 0 fileElement.picHeight = 0
fileElement.videoDuration = 0 fileElement.videoDuration = 0

View File

@ -0,0 +1,82 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.structures.CommFileInfo
import moe.fuqiuluo.qqinterface.servlet.structures.UploadResult
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.symbols.OneBotHandler
import kotlin.time.Duration.Companion.seconds
@OneBotHandler("upload_nt_resource", ["upload_nt_res"])
internal object UploadNtResource: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val pic = session.getString("file")
val chatType = when(session.getStringOrNull("message_type")) {
"group" -> MsgConstant.KCHATTYPEGROUP
"guild" -> MsgConstant.KCHATTYPEGUILD
"private" -> MsgConstant.KCHATTYPEC2C
else -> MsgConstant.KCHATTYPEGROUP
}
val fileType = when(session.getStringOrNull("file_type")) {
"file" -> MsgConstant.KELEMTYPEFILE
"image", "pic" -> MsgConstant.KELEMTYPEPIC
"video" -> MsgConstant.KELEMTYPEVIDEO
"audio", "voice", "record" -> MsgConstant.KELEMTYPEPTT
else -> MsgConstant.KELEMTYPEFILE
}
return invoke(chatType, fileType, pic, session.echo)
}
suspend operator fun invoke(
chatType: Int,
fileType: Int,
picture: String,
echo: JsonElement = EmptyJsonString
): String {
if (ShamrockConfig.isDev()) {
val file = picture.let {
val md5 = it.replace(
regex = "[{}\\-]".toRegex(),
replacement = ""
).split(".")[0].lowercase()
if (md5.length == 32) {
FileUtils.getFileByMd5(it)
} else {
FileUtils.parseAndSave(it)
}
}
if (!file.exists()) {
return logic("picture file is not exists", echo)
}
NtV2RichMediaSvc.tryUploadResourceByNt(
chatType = chatType,
elementType = fileType,
resources = arrayListOf(file),
timeout = 30.seconds
).onSuccess {
return ok(UploadResult(it.map {
CommFileInfo(
modeId = it.fileModelId,
fileName = it.fileName,
fileSize = it.fileSize,
md5 = it.md5,
uuid = it.uuid,
subId = it.subId,
sha = it.sha ?: ""
)
}), echo)
}.onFailure {
return logic("upload failed: ${it.message ?: it.toString()}", echo)
}
}
return logic("upload failed", echo)
}
override val requiredParams: Array<String> = arrayOf("file")
}

View File

@ -14,16 +14,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper import moe.fuqiuluo.shamrock.helper.TransfileHelper
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.remote.service.api.RichMediaUploadHandler import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
@ -56,6 +53,21 @@ internal object UploadPrivateFile : IActionHandler() {
if (!srcFile.exists()) { if (!srcFile.exists()) {
srcFile = FileUtils.getFile(file) srcFile = FileUtils.getFile(file)
} }
if (!srcFile.exists()) {
srcFile = file.let {
val md5 = it.replace(
regex = "[{}\\-]".toRegex(),
replacement = ""
).split(".")[0].lowercase()
if (md5.length == 32) {
FileUtils.getFileByMd5(it)
} else {
FileUtils.parseAndSave(it)
}
}
}
if (!srcFile.exists()) { if (!srcFile.exists()) {
return badParam("文件不存在", echo) return badParam("文件不存在", echo)
} }
@ -65,6 +77,7 @@ internal object UploadPrivateFile : IActionHandler() {
fileElement.fileName = name fileElement.fileName = name
fileElement.filePath = srcFile.absolutePath fileElement.filePath = srcFile.absolutePath
fileElement.fileSize = srcFile.length() fileElement.fileSize = srcFile.length()
fileElement.folderId = srcFile.parent ?: ""
fileElement.picWidth = 0 fileElement.picWidth = 0
fileElement.picHeight = 0 fileElement.picHeight = 0
fileElement.videoDuration = 0 fileElement.videoDuration = 0
@ -111,8 +124,10 @@ internal object UploadPrivateFile : IActionHandler() {
msgService.sendMsgWithMsgId( msgService.sendMsgWithMsgId(
contact, msgIdPair.qqMsgId, arrayListOf(msgElement) contact, msgIdPair.qqMsgId, arrayListOf(msgElement)
) { code, reason -> ) { code, reason ->
LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)") if (code != 0) {
it.resume(null) LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)")
it.resume(null)
}
} }
RichMediaUploadHandler.registerListener(msgIdPair.qqMsgId) { RichMediaUploadHandler.registerListener(msgIdPair.qqMsgId) {
it.resume(this) it.resume(this)

View File

@ -27,13 +27,18 @@ fun Routing.testAction() {
val resId = fetchOrThrow("res_id") val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id") val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type") val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(resId, peerId, messageType)) call.respondText(SendMsgByResid(peerId, resId, messageType))
} }
getOrPost("/createUidFromTinyId") { getOrPost("/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong() val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId") val peerId = fetchOrThrow("peerId")
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId.toLong())) call.respondText(
NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(
selfId,
peerId.toLong()
)
)
} }
getOrPost("/addSendMsg") { getOrPost("/addSendMsg") {

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
import moe.fuqiuluo.qqinterface.servlet.msg.toCQCode import moe.fuqiuluo.qqinterface.servlet.msg.toCQCode
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
@ -16,7 +17,7 @@ import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.remote.service.api.RichMediaUploadHandler import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource
import moe.fuqiuluo.shamrock.remote.service.data.push.PostType import moe.fuqiuluo.shamrock.remote.service.data.push.PostType
import java.util.ArrayList import java.util.ArrayList
@ -24,9 +25,6 @@ import java.util.Collections
import kotlin.collections.HashMap import kotlin.collections.HashMap
internal object AioListener : IKernelMsgListener { internal object AioListener : IKernelMsgListener {
// 通过MSG SEQ临时监听器
private val tempMessageListenerMap = Collections.synchronizedMap(HashMap<Long, suspend MsgRecord.() -> Unit>())
override fun onRecvMsg(msgList: ArrayList<MsgRecord>) { override fun onRecvMsg(msgList: ArrayList<MsgRecord>) {
if (msgList.isEmpty()) return if (msgList.isEmpty()) return
@ -37,27 +35,9 @@ internal object AioListener : IKernelMsgListener {
} }
} }
fun registerTemporaryMsgListener(
msgSeq: Long,
listener: suspend MsgRecord.() -> Unit
) {
LogCenter.log({ "注册临时消息监听器: $msgSeq" }, Level.DEBUG)
tempMessageListenerMap[msgSeq] = listener
}
fun unregisterTemporaryMsgListener(msgSeq: Long) {
tempMessageListenerMap.remove(msgSeq)
}
private suspend fun handleMsg(record: MsgRecord) { private suspend fun handleMsg(record: MsgRecord) {
try { try {
tempMessageListenerMap.firstNotNullOfOrNull { if (MessageTempHandler.notify(record)) return
if (it.key == record.msgSeq) it else null
}?.let {
it.value(record)
tempMessageListenerMap.remove(it.key)
return
}
if (record.msgSeq < 0) return if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
@ -432,7 +412,7 @@ internal object AioListener : IKernelMsgListener {
} }
override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) { override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) {
LogCenter.log("onRichMediaUploadComplete($notifyInfo)", Level.DEBUG) LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG)
RichMediaUploadHandler.notify(notifyInfo) RichMediaUploadHandler.notify(notifyInfo)
} }