Shamrock: support upload resource by NtKernel x2

Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
白池 2024-02-25 12:40:39 +08:00
parent fca66f3259
commit e9a3a82b68
2 changed files with 236 additions and 68 deletions

View File

@ -1,27 +1,44 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import androidx.exifinterface.media.ExifInterface 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.CommonFileInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback import com.tencent.qqnt.kernel.nativeinterface.FileElement
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.PicElement 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.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo 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.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull 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.TryUpPicData
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
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
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.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.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService import moe.fuqiuluo.shamrock.xposed.helper.msgService
import moe.fuqiuluo.symbols.decodeProtobuf import moe.fuqiuluo.symbols.decodeProtobuf
@ -46,7 +63,9 @@ 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.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
@ -60,98 +79,229 @@ internal object NtV2RichMediaSvc: BaseSvc() {
/** /**
* 批量上传图片 * 批量上传图片
*/ */
suspend fun tryUploadGroupPicByNt( suspend fun tryUploadResourceByNt(
imageFiles: ArrayList<File>, chatType: Int,
elementType: Int,
resources: ArrayList<File>,
timeout: Duration timeout: Duration
): Result<MutableList<CommonFileInfo>> { ): Result<MutableList<CommonFileInfo>> {
require(imageFiles.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" } require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" }
val messages = imageFiles.map { file -> val messages = resources.map { file ->
val elem = MsgElement() val elem = MsgElement()
runCatching { elem.elementType = elementType
elem.elementType = MsgConstant.KELEMTYPEPIC when(elementType) {
val pic = PicElement() MsgConstant.KELEMTYPEPIC -> {
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath) val pic = PicElement()
val msgService = NTServiceFetcher.kernelService.msgService!! pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val originalPath = msgService.getRichMediaFilePathForMobileQQSend( val msgService = NTServiceFetcher.kernelService.msgService!!
RichMediaFilePathInfo( val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
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( RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true 2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true
) )
) )
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath) 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
} }
val options = BitmapFactory.Options() MsgConstant.KELEMTYPEPTT -> {
options.inJustDecodeBounds = true require(resources.size == 1) // 语音只能单个上传
BitmapFactory.decodeFile(file.absolutePath, options) var pttFile = file
val exifInterface = ExifInterface(file.absolutePath) val ptt = PttElement()
val orientation = exifInterface.getAttributeInt( when (AudioUtils.getMediaType(pttFile)) {
ExifInterface.TAG_ORIENTATION, MediaType.Silk -> {
ExifInterface.ORIENTATION_UNDEFINED ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
) ptt.duration = QRoute.api(IAIOPttApi::class.java)
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { .getPttFileDuration(pttFile.absolutePath)
pic.picWidth = options.outWidth }
pic.picHeight = options.outHeight MediaType.Amr -> {
} else { ptt.duration = AudioUtils.getDurationSec(pttFile)
pic.picWidth = options.outHeight ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
pic.picHeight = options.outWidth }
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
} }
pic.sourcePath = file.absolutePath MsgConstant.KELEMTYPEVIDEO -> {
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath) require(resources.size == 1) // 视频只能单个上传
pic.original = true val video = VideoElement()
pic.picType = FileUtils.getPicType(file) video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
elem.picElement = pic val msgService = NTServiceFetcher.kernelService.msgService!!
}.onFailure { val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
LogCenter.log(it.stackTraceToString(), Level.WARN) RichMediaFilePathInfo(
elem.elementType = 0 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 return@map elem
}.filter {
it.elementType == MsgConstant.KELEMTYPEPIC
} }
if (messages.isEmpty()) { if (messages.isEmpty()) {
return Result.failure(Exception("no valid image files")) return Result.failure(Exception("no valid image files"))
} }
val result: MutableList<CommonFileInfo> = withTimeoutOrNull(timeout) { 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 { suspendCancellableCoroutine {
val result = mutableListOf<CommonFileInfo>() val uniseq = MessageHelper.generateMsgId(chatType)
val uniseq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
val contact = Contact(MsgConstant.KCHATTYPEGROUP, GROUP_PIC_UPLOAD_TO, GROUP_PIC_UPLOAD_TO)
RichMediaUploadHandler.registerListener(uniseq.qqMsgId) upload@{ RichMediaUploadHandler.registerListener(uniseq.qqMsgId) upload@{
if (uniseq.qqMsgId == msgId) { if (uniseq.qqMsgId == msgId) {
result.add(commonFileInfo) result.add(commonFileInfo)
} }
if (result.size == resources.size) {
it.resume(true)
return@upload true
}
return@upload false return@upload false
} }
MessageHelper.sendMessageWithMsgId( MessageHelper.sendMessageWithMsgId(
contact = contact, contact = contact,
message = ArrayList(messages), message = ArrayList(messages),
uniseq = uniseq.qqMsgId uniseq = uniseq.qqMsgId
) { code, _ -> ) { _, _ -> }
NTServiceFetcher.kernelService
.wrapperSession.msgService
.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null)
RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
if (code != 110 && code != 4) {
it.resume(null)
} else {
it.resume(result)
}
}
it.invokeOnCancellation { it.invokeOnCancellation {
RichMediaUploadHandler.removeListener(uniseq.qqMsgId) RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
} }
} }
} ?: return Result.failure(Exception("timeout")) }
return Result.success(result) return Result.success(result)
} }

View File

@ -1,5 +1,6 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.structures.CommFileInfo import moe.fuqiuluo.qqinterface.servlet.structures.CommFileInfo
import moe.fuqiuluo.qqinterface.servlet.structures.UploadResult import moe.fuqiuluo.qqinterface.servlet.structures.UploadResult
@ -12,14 +13,29 @@ import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@OneBotHandler("upload_group_image", ["upload_group_pic"]) @OneBotHandler("upload_nt_resource", ["upload_nt_res"])
internal object UploadGroupPic: IActionHandler() { internal object UploadNtResource: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val pic = session.getString("file") val pic = session.getString("file")
return invoke(pic, session.echo) 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( suspend operator fun invoke(
chatType: Int,
fileType: Int,
picture: String, picture: String,
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
@ -38,8 +54,10 @@ internal object UploadGroupPic: IActionHandler() {
if (!file.exists()) { if (!file.exists()) {
return logic("picture file is not exists", echo) return logic("picture file is not exists", echo)
} }
NtV2RichMediaSvc.tryUploadGroupPicByNt( NtV2RichMediaSvc.tryUploadResourceByNt(
imageFiles = arrayListOf(file), chatType = chatType,
elementType = fileType,
resources = arrayListOf(file),
timeout = 30.seconds timeout = 30.seconds
).onSuccess { ).onSuccess {
return ok(UploadResult(it.map { return ok(UploadResult(it.map {