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(33) var height400: UInt? = null,
@ProtoNumber(34) var pbReserve: PbReserve? = null,
){
companion object{
) {
companion object {
@Serializable
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
data class NotOnlineImage(
@ProtoNumber(1) val filePath: ByteArray? = null,
@ProtoNumber(1) val filePath: String? = 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(5) val imgType: UInt? = null,
@ProtoNumber(6) val previewsImage: ByteArray? = null,
@ProtoNumber(7) val picMd5: ByteArray? = null,
@ProtoNumber(8) val picHeight: 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(12) val thumbUrl: String? = null,
@ProtoNumber(13) val original: Boolean? = null,
@ -39,8 +39,23 @@ data class NotOnlineImage(
@Serializable
data class PbReserve(
@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(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;
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);

View File

@ -9,6 +9,7 @@ import io.ktor.utils.io.core.writeFully
import io.ktor.utils.io.core.writeInt
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
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())
return withTimeoutOrNull(5000L) {
suspendCancellableCoroutine {
AioListener.registerTemporaryMsgListener(msgSeq) {
MessageTempHandler.registerTemporaryMsgListener(msgSeq) {
it.resume(this.msgId)
}
it.invokeOnCancellation {
AioListener.unregisterTemporaryMsgListener(msgSeq)
MessageTempHandler.unregisterTemporaryMsgListener(msgSeq)
}
}
} ?: -1L

View File

@ -1,16 +1,9 @@
package moe.fuqiuluo.qqinterface.servlet.ark
import com.tencent.mobileqq.pb.ByteStringMicro
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.TicketSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal object ArkMsgSvc: BaseSvc() {
fun tryShareMusic(

View File

@ -6,10 +6,10 @@ import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.http.encodeURLQueryComponent
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
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.LogCenter
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(
val appId: Long,
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

View File

@ -2,7 +2,7 @@ package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
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.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
@ -56,7 +56,7 @@ internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String,
val messageData = arrayListOf<MessageSegment>()
this.forEach { msg ->
kotlin.runCatching {
val converter = MsgElementConverter[msg.elementType]
val converter = NtMsgElementConverter[msg.elementType]
converter?.invoke(chatType, peerId, subPeer, msg)
?: throw UnsupportedOperationException("不支持的消息element类型${msg.elementType}")
}.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 object MsgElementConverter {
internal object NtMsgElementConverter {
private val convertMap = hashMapOf(
MsgConstant.KELEMTYPETEXT to MsgElementConverter::convertTextElem,
MsgConstant.KELEMTYPEFACE to MsgElementConverter::convertFaceElem,
MsgConstant.KELEMTYPEPIC to MsgElementConverter::convertImageElem,
MsgConstant.KELEMTYPEPTT to MsgElementConverter::convertVoiceElem,
MsgConstant.KELEMTYPEVIDEO to MsgElementConverter::convertVideoElem,
MsgConstant.KELEMTYPEMARKETFACE to MsgElementConverter::convertMarketFaceElem,
MsgConstant.KELEMTYPEARKSTRUCT to MsgElementConverter::convertStructJsonElem,
MsgConstant.KELEMTYPEREPLY to MsgElementConverter::convertReplyElem,
MsgConstant.KELEMTYPEGRAYTIP to MsgElementConverter::convertGrayTipsElem,
MsgConstant.KELEMTYPEFILE to MsgElementConverter::convertFileElem,
MsgConstant.KELEMTYPEMARKDOWN to MsgElementConverter::convertMarkdownElem,
MsgConstant.KELEMTYPETEXT to NtMsgElementConverter::convertTextElem,
MsgConstant.KELEMTYPEFACE to NtMsgElementConverter::convertFaceElem,
MsgConstant.KELEMTYPEPIC to NtMsgElementConverter::convertImageElem,
MsgConstant.KELEMTYPEPTT to NtMsgElementConverter::convertVoiceElem,
MsgConstant.KELEMTYPEVIDEO to NtMsgElementConverter::convertVideoElem,
MsgConstant.KELEMTYPEMARKETFACE to NtMsgElementConverter::convertMarketFaceElem,
MsgConstant.KELEMTYPEARKSTRUCT to NtMsgElementConverter::convertStructJsonElem,
MsgConstant.KELEMTYPEREPLY to NtMsgElementConverter::convertReplyElem,
MsgConstant.KELEMTYPEGRAYTIP to NtMsgElementConverter::convertGrayTipsElem,
MsgConstant.KELEMTYPEFILE to NtMsgElementConverter::convertFileElem,
MsgConstant.KELEMTYPEMARKDOWN to NtMsgElementConverter::convertMarkdownElem,
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
MsgConstant.KELEMTYPEFACEBUBBLE to MsgElementConverter::convertBubbleFaceElem,
MsgConstant.KELEMTYPEINLINEKEYBOARD to MsgElementConverter::convertInlineKeyboardElem
MsgConstant.KELEMTYPEFACEBUBBLE to NtMsgElementConverter::convertBubbleFaceElem,
MsgConstant.KELEMTYPEINLINEKEYBOARD to NtMsgElementConverter::convertInlineKeyboardElem
)
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 androidx.exifinterface.media.ExifInterface
import com.tencent.qqnt.kernel.nativeinterface.*
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.qqinterface.servlet.CardSvc
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.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.*
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.qqinterface.servlet.transfile.NtV2RichMediaSvc
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.ParamsException
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools
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.message.Elem
import protobuf.message.element.*
@ -36,8 +25,9 @@ import java.io.File
import java.nio.ByteBuffer
import kotlin.random.Random
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 {
private val makerArray = hashMapOf(
@ -46,30 +36,30 @@ internal object ElemMaker {
"face" to ElemMaker::createFaceElem,
"pic" to ElemMaker::createImageElem,
"image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem,
// "voice" to ElemMaker::createRecordElem,
// "record" to ElemMaker::createRecordElem,
// "video" to ElemMaker::createVideoElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
"inline_keyboard" to ElemMaker::createButtonElem,
"dice" to ElemMaker::createNewDiceElem,
"rps" to ElemMaker::createNewRpsElem,
"poke" to ElemMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem,
// "anonymous" to ElemMaker::createAnonymousElem,
// "share" to ElemMaker::createShareElem,
// "contact" to ElemMaker::createContactElem,
// "location" to ElemMaker::createLocationElem,
// "music" to ElemMaker::createMusicElem,
"reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem,
// "touch" to ElemMaker::createTouchElem,
"weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem,
// "node" to MessageMaker::createNodeElem,
//"forward" to MessageMaker::createForwardElem,
//"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(
chatType: Int,
@ -226,26 +216,6 @@ internal object ElemMaker {
}
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()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options)
@ -264,53 +234,77 @@ internal object ElemMaker {
picHeight = options.outWidth
}
val elem = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> {
Transfer with Troop(peerId) trans PictureResource(file)
Elem(
customFace = CustomFace(
filePath = "${md5HexStr.substring(0, 8)}-${md5HexStr.substring(8, 4)}-${
md5HexStr.substring(
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)
)
)
}
val uploadRet = NtV2RichMediaSvc.tryUploadResourceByNt(
chatType = chatType,
elementType = MsgConstant.KELEMTYPEPIC,
resources = arrayListOf(file),
timeout = 30.seconds
).getOrThrow().first()
LogCenter.log(uploadRet.toString(), Level.DEBUG)
MsgConstant.KCHATTYPEC2C -> {
Transfer with Private(peerId) trans PictureResource(file)
Elem(
notOnlineImage = NotOnlineImage(
filePath = "${md5HexStr}.${FileUtils.getFileType(file)}".toByteArray(),
fileLen = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
downloadPath = "".toByteArray(),
imgType = FileUtils.getPicType(file).toUInt(),
picMd5 = md5HexStr.hex2ByteArray(),
picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(),
resId = "".toByteArray(),
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0)
val elem = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> Elem(
customFace = CustomFace(
filePath = uploadRet.fileName,
fileId = uploadRet.uuid.toUInt(),
serverIp = 0u,
serverPort = 0u,
fileType = FileUtils.getPicType(file).toUInt(),
useful = 1u,
md5 = uploadRet.md5.hex2ByteArray(),
bizType = data["subType"].asIntOrNull?.toUInt(),
imageType = FileUtils.getPicType(file).toUInt(),
width = picWidth.toUInt(),
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")
}

View File

@ -3,7 +3,6 @@ package moe.fuqiuluo.qqinterface.servlet.msg.maker
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.emoticon.QQSysFaceUtil
import com.tencent.mobileqq.pb.ByteStringMicro
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.GroupSvc
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.WeatherSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.*
import moe.fuqiuluo.qqinterface.servlet.transfile.FileTransfer
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.Transfer
import moe.fuqiuluo.qqinterface.servlet.transfile.Troop
import moe.fuqiuluo.qqinterface.servlet.transfile.VideoResource
import moe.fuqiuluo.qqinterface.servlet.transfile.VoiceResource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Troop
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource
import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
@ -84,7 +83,6 @@ internal object NtMsgElementMaker {
"new_dice" to NtMsgElementMaker::createNewDiceElem,
"new_rps" to NtMsgElementMaker::createNewRpsElem,
"basketball" to NtMsgElementMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
"button" to NtMsgElementMaker::createInlineKeywordElem,
@ -180,17 +178,6 @@ internal object NtMsgElementMaker {
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(
chatType: Int,
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
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.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
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.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.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 protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb
@ -31,13 +63,248 @@ import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
import protobuf.oidb.cmd0x388.TryUpImgReq
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.random.nextULong
import kotlin.time.Duration
internal object NtV2RichMediaSvc: BaseSvc() {
private const val GROUP_PIC_UPLOAD_TO = "100000000"
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
*/
@ -173,6 +440,9 @@ internal object NtV2RichMediaSvc: BaseSvc() {
LogCenter.log("requestUploadPic => rsp: $rsp")
}
/**
* 使用OldBDH获取图片上传状态以及图片上传服务器
*/
suspend fun requestUploadGroupPic(
groupId: ULong,
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

View File

@ -1,14 +1,19 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType.*
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() {
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

View File

@ -1,4 +1,4 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
package moe.fuqiuluo.qqinterface.servlet.transfile.data
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(
contact: Contact,
message: ArrayList<MsgElement>,
uniseq: Long,
callback: IOperateCallback
): SendMsgResult {
val uniseq = generateMsgId(contact.chatType)
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if (callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.msgHashId
}
service.sendMsg(
contact,
uniseq.qqMsgId,
uniseq,
message,
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(

View File

@ -3,7 +3,7 @@ package moe.fuqiuluo.shamrock.helper
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
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.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt

View File

@ -49,7 +49,7 @@ internal object DownloadFile: IActionHandler() {
headerMap[k] = v
}
}
return invoke(url, threadCnt, headerMap, echo)
return invoke(url, threadCnt, headerMap, name, echo)
} else if (base64 != null) {
return invoke(base64, name, echo)
} else {
@ -88,6 +88,7 @@ internal object DownloadFile: IActionHandler() {
url: String,
threadCnt: Int,
headers: Map<String, String>,
name: String?,
echo: JsonElement = EmptyJsonString
): String {
return kotlin.runCatching {
@ -100,7 +101,13 @@ internal object DownloadFile: IActionHandler() {
)) {
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(
file = tmp.absolutePath,
md5 = MD5.genFileMd5Hex(tmp.absolutePath)

View File

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

View File

@ -24,8 +24,7 @@ internal object SendMsgByResid : IActionHandler() {
val resId = session.getString("res_id")
val peerId = session.getString("peer_id")
val messageType = session.getString("message_type")
invoke(resId, peerId, messageType)
return ok("ok", session.echo)
return invoke(peerId, resId, messageType, session.echo)
}
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())
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
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.IActionHandler
import moe.fuqiuluo.shamrock.tools.jsonArray
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() {
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 chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val retryCnt = session.getIntOrNull("retry_cnt")
@ -19,21 +22,21 @@ internal object SendPrivateMessage : IActionHandler() {
val message = session.getString("message")
SendMessage(
chatType = chatType,
peerId = userId.toString(),
peerId = userId,
message = message,
autoEscape = autoEscape,
echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(),
fromId = groupId?.toString() ?: userId,
retryCnt = retryCnt ?: 5,
recallDuration = recallDuration
)
} else {
SendMessage(
chatType = chatType,
peerId = userId.toString(),
peerId = userId,
message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray,
echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(),
fromId = groupId?.toString() ?: userId,
retryCnt = retryCnt ?: 5,
recallDuration = recallDuration
)

View File

@ -17,12 +17,12 @@ import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.transfile.RichMediaUploadHandler
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
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.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
@ -55,6 +55,20 @@ internal object UploadGroupFile : IActionHandler() {
if (!srcFile.exists()) {
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()) {
return badParam("文件不存在", echo)
}
@ -64,6 +78,7 @@ internal object UploadGroupFile : IActionHandler() {
fileElement.fileName = name
fileElement.filePath = srcFile.absolutePath
fileElement.fileSize = srcFile.length()
fileElement.folderId = srcFile.parent ?: ""
fileElement.picWidth = 0
fileElement.picHeight = 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.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
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.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
@ -56,6 +53,21 @@ internal object UploadPrivateFile : IActionHandler() {
if (!srcFile.exists()) {
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()) {
return badParam("文件不存在", echo)
}
@ -65,6 +77,7 @@ internal object UploadPrivateFile : IActionHandler() {
fileElement.fileName = name
fileElement.filePath = srcFile.absolutePath
fileElement.fileSize = srcFile.length()
fileElement.folderId = srcFile.parent ?: ""
fileElement.picWidth = 0
fileElement.picHeight = 0
fileElement.videoDuration = 0
@ -111,8 +124,10 @@ internal object UploadPrivateFile : IActionHandler() {
msgService.sendMsgWithMsgId(
contact, msgIdPair.qqMsgId, arrayListOf(msgElement)
) { code, reason ->
LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)")
it.resume(null)
if (code != 0) {
LogCenter.log("私聊文件消息发送异常(code = $code, reason = $reason)")
it.resume(null)
}
}
RichMediaUploadHandler.registerListener(msgIdPair.qqMsgId) {
it.resume(this)

View File

@ -27,13 +27,18 @@ fun Routing.testAction() {
val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(resId, peerId, messageType))
call.respondText(SendMsgByResid(peerId, resId, messageType))
}
getOrPost("/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
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") {

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
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.transfile.RichProtoSvc
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.db.MessageDB
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.PostType
import java.util.ArrayList
@ -24,9 +25,6 @@ import java.util.Collections
import kotlin.collections.HashMap
internal object AioListener : IKernelMsgListener {
// 通过MSG SEQ临时监听器
private val tempMessageListenerMap = Collections.synchronizedMap(HashMap<Long, suspend MsgRecord.() -> Unit>())
override fun onRecvMsg(msgList: ArrayList<MsgRecord>) {
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) {
try {
tempMessageListenerMap.firstNotNullOfOrNull {
if (it.key == record.msgSeq) it else null
}?.let {
it.value(record)
tempMessageListenerMap.remove(it.key)
return
}
if (MessageTempHandler.notify(record)) return
if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
@ -432,7 +412,7 @@ internal object AioListener : IKernelMsgListener {
}
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)
}