mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
7 Commits
12d594697d
...
661680e60b
Author | SHA1 | Date | |
---|---|---|---|
661680e60b | |||
54b7eb95a8 | |||
265fff3cd2 | |||
8ca0a3815a | |||
da6d34c53e | |||
61ffb37951 | |||
593f461ffe |
@ -162,6 +162,17 @@ object ShamrockConfig {
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getUploadResourceGroup(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("up_res_group", "100000000")!!
|
||||
}
|
||||
|
||||
fun setUploadResourceGroup(ctx: Context, v: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("up_res_group", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getHttpPort(ctx: Context): Int {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getInt("port", 5700)
|
||||
@ -354,6 +365,7 @@ object ShamrockConfig {
|
||||
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
|
||||
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false),
|
||||
"enable_old_bdh" to preferences.getBoolean("enable_old_bdh", false),
|
||||
"up_res_group" to preferences.getString("up_res_group", ""),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -356,6 +356,36 @@ private fun FunctionCard(
|
||||
return@Function true
|
||||
}
|
||||
|
||||
run {
|
||||
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(2.dp),
|
||||
text = "用来上传资源的群聊,错误的资源上传终点可能导致封禁,请自建一个群聊并填写在下方。",
|
||||
color = Color.Red,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
}
|
||||
TextItem(
|
||||
title = "接受资源群聊",
|
||||
desc = "用来上传资源的群聊,请自建一个群聊并填写在下方。",
|
||||
text = uploadResourceGroup,
|
||||
hint = "请输入群号",
|
||||
error = "群号不合法",
|
||||
checker = {
|
||||
it.isNotBlank() && it.toULongOrNull() != null
|
||||
},
|
||||
confirm = {
|
||||
val groupId = uploadResourceGroup.value
|
||||
ShamrockConfig.setUploadResourceGroup(ctx, groupId)
|
||||
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Function(
|
||||
title = "专业级接口",
|
||||
@ -445,9 +475,7 @@ private fun InfoItem(
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(onDoubleClick = {
|
||||
doubleClick?.invoke(content)
|
||||
}) {
|
||||
true
|
||||
}
|
||||
}) { true }
|
||||
,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
@ -32,6 +32,11 @@ data class AdaptShareInfoReq(
|
||||
|
||||
@Serializable
|
||||
data class Template(
|
||||
@ProtoNumber(1) var templateId: UInt? = null,
|
||||
@ProtoNumber(1) var templateId: ULong? = null,
|
||||
@ProtoNumber(2) var templateData: ByteArray? = null,
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AdaptShareInfoResp(
|
||||
@ProtoNumber(2) var json: String? = null,
|
||||
): Protobuf<AdaptShareInfoResp>
|
@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import moe.fuqiuluo.symbols.Protobuf
|
||||
|
||||
const val DEFAULT_DEVICE_INFO = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0"
|
||||
|
||||
@Serializable
|
||||
data class QWebReq(
|
||||
@ProtoNumber(1) val seq: Int = 0,
|
||||
|
@ -37,6 +37,7 @@ import protobuf.oidb.cmx0xf57.Oidb0xf57Req
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U1
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U2
|
||||
import protobuf.qweb.DEFAULT_DEVICE_INFO
|
||||
import protobuf.qweb.QWebExtInfo
|
||||
import protobuf.qweb.QWebReq
|
||||
import protobuf.qweb.QWebRsp
|
||||
@ -73,7 +74,7 @@ internal object GProSvc: BaseSvc() {
|
||||
val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
|
||||
seq = 10,
|
||||
qua = PlatformUtils.getQUA(),
|
||||
deviceInfo = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0",
|
||||
deviceInfo = DEFAULT_DEVICE_INFO,
|
||||
buffer = GetGuildFeedsReq(
|
||||
count = 12,
|
||||
from = startIndex,
|
||||
|
@ -338,13 +338,13 @@ internal object MsgSvc : BaseSvc() {
|
||||
else -> MessageHelper.decodeCQCode(data["content"].asString)
|
||||
}.onEach { element ->
|
||||
val elementData = element.asJsonObject["data"].asJsonObject
|
||||
if (element.asJsonObject["type"].asString == "forward")
|
||||
if (element.asJsonObject["type"].asString == "forward") {
|
||||
forwardMsg[elementData["filename"].asString] =
|
||||
elementData["id"].asString
|
||||
}
|
||||
}
|
||||
).getOrElse { throw Exception("消息合成失败: $it") }.let {
|
||||
desc[++i] =
|
||||
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
|
||||
).getOrElse { error("消息合成失败: $it") }.let {
|
||||
desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
|
||||
?: TicketSvc.getNickname()) + ": " + it.first
|
||||
it.second
|
||||
}
|
||||
@ -353,8 +353,9 @@ internal object MsgSvc : BaseSvc() {
|
||||
} else {
|
||||
error("消息节点缺少id或content字段")
|
||||
}
|
||||
}.onFailure {
|
||||
LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
|
||||
}.getOrElse {
|
||||
LogCenter.log("消息节点解析失败:$it", Level.WARN)
|
||||
null
|
||||
}
|
||||
}.ifEmpty { return Result.failure(Exception("消息节点为空")) }
|
||||
|
@ -1,9 +1,45 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||
|
||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.lightapp.AdaptShareInfoReq
|
||||
import protobuf.lightapp.AdaptShareInfoResp
|
||||
import protobuf.qweb.DEFAULT_DEVICE_INFO
|
||||
import protobuf.qweb.QWebReq
|
||||
import protobuf.qweb.QWebRsp
|
||||
|
||||
internal object LightAppSvc: BaseSvc() {
|
||||
suspend fun adaptShare() {
|
||||
|
||||
suspend fun adaptShareJumpUrl(
|
||||
arkAppInfo: ArkAppInfo,
|
||||
coverUrl: String,
|
||||
desc: String,
|
||||
url: String
|
||||
): Result<String> {
|
||||
val rsp = sendBufferAW("LightAppSvc.mini_app_share.AdaptShareInfo", true, QWebReq(
|
||||
seq = 10,
|
||||
qua = PlatformUtils.getQUA(),
|
||||
deviceInfo = DEFAULT_DEVICE_INFO,
|
||||
buffer = AdaptShareInfoReq(
|
||||
appid = arkAppInfo.miniAppId.toString(),
|
||||
title = arkAppInfo.appName,
|
||||
desc = desc,
|
||||
time = (System.currentTimeMillis() * 0.001).toULong(),
|
||||
scene = 3u,
|
||||
templetType = 1u,
|
||||
businessType = 0u,
|
||||
picUrl = coverUrl,
|
||||
jumpUrl = "pages",
|
||||
verType = 3u,
|
||||
withShareTicket = 0u,
|
||||
webURL = url,
|
||||
).toByteArray(),
|
||||
traceId = app.account + "_0_0",
|
||||
).toByteArray())?.decodeProtobuf<QWebRsp>()?.buffer?.decodeProtobuf<AdaptShareInfoResp>()
|
||||
if (rsp == null || rsp.json.isNullOrEmpty())
|
||||
return Result.failure(Exception("unable to adapt ShareInfo"))
|
||||
return Result.success(rsp.json!!)
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ sealed class ArkAppInfo(
|
||||
val version: String,
|
||||
val packageName: String,
|
||||
val signature: String,
|
||||
val miniAppId: Long = 0
|
||||
val miniAppId: Long = 0,
|
||||
val appName: String = ""
|
||||
) {
|
||||
data object QQMusic: ArkAppInfo(
|
||||
appId = 100497308,
|
||||
@ -25,7 +26,8 @@ sealed class ArkAppInfo(
|
||||
version = "0.0.0",
|
||||
packageName = "tv.danmaku.bili",
|
||||
signature = "7194d531cbe7960a22007b9f6bdaa38b",
|
||||
miniAppId = 1109937557
|
||||
miniAppId = 1109937557,
|
||||
appName = "哔哩哔哩"
|
||||
)
|
||||
|
||||
data object Docs: ArkAppInfo(
|
||||
|
@ -261,7 +261,7 @@ internal class ElemMaker {
|
||||
resources = arrayListOf(file),
|
||||
timeout = 30.seconds
|
||||
).getOrThrow().first()
|
||||
LogCenter.log(uploadRet.toString(), Level.DEBUG)
|
||||
LogCenter.log({ uploadRet.toString() }, Level.DEBUG)
|
||||
|
||||
val elem = when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Elem(
|
||||
|
@ -1,14 +1,11 @@
|
||||
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
|
||||
@ -16,28 +13,19 @@ 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.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
|
||||
@ -63,7 +51,6 @@ 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
|
||||
@ -72,14 +59,30 @@ 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)
|
||||
|
||||
private fun fetchGroupResUploadTo(): String {
|
||||
return ShamrockConfig.getUpResGroup().ifEmpty { "100000000" }
|
||||
}
|
||||
|
||||
suspend fun tryUploadResourceByNt(
|
||||
chatType: Int,
|
||||
elementType: Int,
|
||||
resources: ArrayList<File>,
|
||||
timeout: Duration,
|
||||
retryCnt: Int = 5
|
||||
): Result<MutableList<CommonFileInfo>> {
|
||||
return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure {
|
||||
if (retryCnt > 0) {
|
||||
return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传图片
|
||||
*/
|
||||
suspend fun tryUploadResourceByNt(
|
||||
private suspend fun internalTryUploadResourceByNt(
|
||||
chatType: Int,
|
||||
elementType: Int,
|
||||
resources: ArrayList<File>,
|
||||
@ -276,8 +279,9 @@ internal object NtV2RichMediaSvc: BaseSvc() {
|
||||
}
|
||||
val contact = when(chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin())
|
||||
else -> Contact(chatType, GROUP_PIC_UPLOAD_TO, GROUP_PIC_UPLOAD_TO)
|
||||
else -> Contact(chatType, fetchGroupResUploadTo(), null)
|
||||
}
|
||||
LogCenter.log(contact.toString())
|
||||
val result = mutableListOf<CommonFileInfo>()
|
||||
withTimeoutOrNull(timeout) {
|
||||
suspendCancellableCoroutine {
|
||||
@ -297,10 +301,12 @@ internal object NtV2RichMediaSvc: BaseSvc() {
|
||||
message = ArrayList(messages),
|
||||
uniseq = uniseq.qqMsgId
|
||||
) { _, _ ->
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
msgService.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null)
|
||||
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
msgService.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null)
|
||||
}
|
||||
}
|
||||
it.invokeOnCancellation {
|
||||
RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
|
||||
|
@ -0,0 +1,44 @@
|
||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.LightAppSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||
import moe.fuqiuluo.symbols.OneBotHandler
|
||||
|
||||
@OneBotHandler("adapt_share_json")
|
||||
internal object AdaptShareJson: IActionHandler() {
|
||||
override suspend fun internalHandle(session: ActionSession): String {
|
||||
//val json = if(session.isString("json")) session.getString("json")
|
||||
//else session.getJsonElement("json").toString()
|
||||
val cover = session.getString("cover")
|
||||
val desc = session.getString("desc")
|
||||
val url = session.getStringOrNull("url") ?: ""
|
||||
return invoke(cover, desc, url, session.echo)
|
||||
}
|
||||
|
||||
suspend operator fun invoke(cover: String, desc: String, url: String, echo: JsonElement = EmptyJsonString): String {
|
||||
/*
|
||||
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
|
||||
return ok(SignArkMessageResult(it), echo = echo)
|
||||
}.onFailure {
|
||||
return error(it.message ?: it.toString(), echo)
|
||||
}*/
|
||||
LightAppSvc.adaptShareJumpUrl(ArkAppInfo.DanMaKu, cover, desc, url).onSuccess {
|
||||
return ok(AdaptShareInfo(it), echo = echo)
|
||||
}.onFailure {
|
||||
return error(it.message ?: it.toString(), echo)
|
||||
}
|
||||
return logic("logic error", echo)
|
||||
}
|
||||
|
||||
override val requiredParams: Array<String> = arrayOf("cover", "desc")
|
||||
|
||||
@Serializable
|
||||
data class AdaptShareInfo(
|
||||
val result: String
|
||||
)
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
|
||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
||||
import moe.fuqiuluo.symbols.OneBotHandler
|
||||
|
||||
@OneBotHandler("sign_ark_message")
|
||||
internal object SignArkMessage: IActionHandler() {
|
||||
override suspend fun internalHandle(session: ActionSession): String {
|
||||
val json = if(session.isString("json")) session.getString("json")
|
||||
else session.getJsonElement("json").toString()
|
||||
return invoke(json, session.echo)
|
||||
}
|
||||
|
||||
suspend operator fun invoke(json: String, echo: JsonElement = EmptyJsonString): String {
|
||||
/*
|
||||
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
|
||||
return ok(SignArkMessageResult(it), echo = echo)
|
||||
}.onFailure {
|
||||
return error(it.message ?: it.toString(), echo)
|
||||
}*/
|
||||
return logic("logic error", echo)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SignArkMessageResult(
|
||||
val result: String
|
||||
)
|
||||
}
|
@ -31,16 +31,21 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.respond
|
||||
|
||||
fun Routing.messageAction() {
|
||||
route("/sign_ark_message") {
|
||||
route("/adapt_share_json") {
|
||||
get {
|
||||
val json = fetchGetOrThrow("json")
|
||||
call.respondText(SignArkMessage(json), ContentType.Application.Json)
|
||||
val cover = fetchGetOrThrow("cover")
|
||||
val desc = fetchGetOrThrow("desc")
|
||||
val url = fetchGetOrNull("url") ?: ""
|
||||
call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json)
|
||||
}
|
||||
post {
|
||||
val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json")))
|
||||
fetchPostJsonElement("json").toString()
|
||||
else fetchPostOrThrow("json")
|
||||
call.respondText(SignArkMessage(json), ContentType.Application.Json)
|
||||
//val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json")))
|
||||
// fetchPostJsonElement("json").toString()
|
||||
//else fetchPostOrThrow("json")
|
||||
val cover = fetchPostOrThrow("cover")
|
||||
val desc = fetchPostOrThrow("desc")
|
||||
val url = fetchPostOrNull("url") ?: ""
|
||||
call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +102,13 @@ fun Routing.messageAction() {
|
||||
call.respondText(GetMsg(msgHash), ContentType.Application.Json)
|
||||
}
|
||||
|
||||
getOrPost("/send_msg_by_resid") {
|
||||
val resId = fetchOrThrow("res_id")
|
||||
val peerId = fetchOrThrow("peer_Id")
|
||||
val messageType = fetchOrThrow("message_type")
|
||||
call.respondText(SendMsgByResid(peerId, resId, messageType))
|
||||
}
|
||||
|
||||
route("/(send_msg|send_message)".toRegex()) {
|
||||
get {
|
||||
val msgType = fetchGetOrThrow("message_type")
|
||||
|
@ -24,13 +24,6 @@ fun Routing.testAction() {
|
||||
return
|
||||
}
|
||||
|
||||
getOrPost("/send_msg_by_resid") {
|
||||
val resId = fetchOrThrow("res_id")
|
||||
val peerId = fetchOrThrow("peer_Id")
|
||||
val messageType = fetchOrThrow("message_type")
|
||||
call.respondText(SendMsgByResid(peerId, resId, messageType))
|
||||
}
|
||||
|
||||
getOrPost("/createUidFromTinyId") {
|
||||
val selfId = fetchOrThrow("selfId").toLong()
|
||||
val peerId = fetchOrThrow("peerId")
|
||||
|
@ -2,8 +2,10 @@ package moe.fuqiuluo.shamrock.remote.service.config
|
||||
|
||||
import android.content.Intent
|
||||
import com.tencent.mmkv.MMKV
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalJson5
|
||||
@ -17,7 +19,7 @@ internal object ShamrockConfig {
|
||||
if (it.exists()) it.delete()
|
||||
it.mkdirs()
|
||||
}
|
||||
private val Config = kotlin.runCatching {
|
||||
private val config = kotlin.runCatching {
|
||||
GlobalJson5.decodeFromString<ServiceConfig>(ConfigDir.resolve("config.json").also {
|
||||
if (!it.exists()) it.writeText("{}")
|
||||
}.readText())
|
||||
@ -32,40 +34,21 @@ internal object ShamrockConfig {
|
||||
return mmkv.getBoolean("isInit", false)
|
||||
}
|
||||
|
||||
private fun updateConfig(config: ServiceConfig = Config) {
|
||||
private fun updateConfig(config: ServiceConfig = this.config) {
|
||||
ConfigDir.resolve("config.json").writeText(GlobalJson5.encodeToString(config))
|
||||
}
|
||||
|
||||
fun updateConfig(intent: Intent) {
|
||||
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
|
||||
mmkv.apply {
|
||||
if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
|
||||
putBoolean(
|
||||
"tablet",
|
||||
intent.getBooleanExtra("tablet", false)
|
||||
) // 强制平板模式
|
||||
putBoolean("tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式
|
||||
putInt("port", intent.getIntExtra("port", 5700)) // 主动HTTP端口
|
||||
putBoolean("ws", intent.getBooleanExtra("ws", false)) // 主动WS开关
|
||||
putBoolean(
|
||||
"http",
|
||||
intent.getBooleanExtra("http", false)
|
||||
) // HTTP回调开关
|
||||
putString(
|
||||
"http_addr",
|
||||
intent.getStringExtra("http_addr")
|
||||
) // WebHook回调地址
|
||||
putBoolean(
|
||||
"ws_client",
|
||||
intent.getBooleanExtra("ws_client", false)
|
||||
) // 被动WS开关
|
||||
putBoolean(
|
||||
"use_cqcode",
|
||||
intent.getBooleanExtra("use_cqcode", false)
|
||||
) // 使用CQ码
|
||||
putBoolean(
|
||||
"inject_packet",
|
||||
intent.getBooleanExtra("inject_packet", false)
|
||||
) // 拦截无用包
|
||||
putBoolean("http", intent.getBooleanExtra("http", false)) // HTTP回调开关
|
||||
putString("http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址
|
||||
putBoolean("ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关
|
||||
putBoolean("use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码
|
||||
putBoolean("inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包
|
||||
putBoolean("debug", intent.getBooleanExtra("debug", false)) // 调试模式
|
||||
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
|
||||
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
|
||||
@ -78,22 +61,24 @@ internal object ShamrockConfig {
|
||||
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
|
||||
putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程
|
||||
putBoolean("enable_old_bdh", intent.getBooleanExtra("enable_old_bdh", false)) // 启用旧版BDH
|
||||
intent.getStringExtra("up_res_group")?.let { putString("up_res_group", it) }
|
||||
} else {
|
||||
XposedBridge.log("[Shamrock] 已禁用自动同步配置")
|
||||
}
|
||||
Config.defaultToken = intent.getStringExtra("token")
|
||||
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
|
||||
config.defaultToken = intent.getStringExtra("token")
|
||||
config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
|
||||
val wsPort = intent.getIntExtra("ws_port", 5800)
|
||||
Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig(
|
||||
config.activeWebSocket = if (config.activeWebSocket == null) ConnectionConfig(
|
||||
address = "0.0.0.0",
|
||||
port = wsPort,
|
||||
) else Config.activeWebSocket?.also {
|
||||
) else config.activeWebSocket?.also {
|
||||
it.port = wsPort
|
||||
}
|
||||
Config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", ",")?.filter { address ->
|
||||
config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", ",")?.filter { address ->
|
||||
address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://"))
|
||||
}?.map {
|
||||
ConnectionConfig(address = it)
|
||||
}?.toMutableList()
|
||||
|
||||
putBoolean("isInit", true)
|
||||
}
|
||||
if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
|
||||
@ -101,23 +86,77 @@ internal object ShamrockConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fun putDefaultSettings() {
|
||||
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
|
||||
if ((!isInit()) && (!mmkv.getBoolean("isEmergencyInitBefore", false))){
|
||||
mmkv.apply {
|
||||
putBoolean("tablet", false) // 强制平板模式
|
||||
putInt("port", 5700) // 主动HTTP端口
|
||||
putBoolean("ws", false) // 主动WS开关
|
||||
putBoolean("http", false) // HTTP回调开关
|
||||
putString("http_addr", null) // WebHook回调地址
|
||||
putBoolean("ws_client", false) // 被动WS开关
|
||||
putBoolean("use_cqcode", false) // 使用CQ码
|
||||
putBoolean("inject_packet", false) // 拦截无用包
|
||||
putBoolean("debug", false) // 调试模式
|
||||
putString("key_store", null) // 证书路径
|
||||
putString("ssl_pwd", null) // 证书密码
|
||||
putString("ssl_private_pwd", null) // 证书私钥密码
|
||||
putString("ssl_alias", null) // 证书别名
|
||||
putInt("ssl_port", 5701) // 主动HTTP端口
|
||||
putBoolean("alive_reply", true) // 自回复测试
|
||||
putBoolean("enable_self_msg", false) // 推送自己发的消息
|
||||
putBoolean("shell", false) // 开启Shell接口
|
||||
putBoolean("enable_sync_msg_as_sent_msg", true) // 推送同步消息
|
||||
putBoolean("forbid_useless_process", false) // 禁用QQ生成无用进程
|
||||
putBoolean("enable_old_bdh", false) // 启用旧版BDH
|
||||
putBoolean("antiTrace", false)
|
||||
putBoolean("super_anti", true)
|
||||
putString("up_res_group", "")
|
||||
|
||||
config.defaultToken = null
|
||||
// config.antiTrace = true
|
||||
val wsPort = 5800
|
||||
config.activeWebSocket =
|
||||
if (config.activeWebSocket == null) ConnectionConfig(
|
||||
address = "0.0.0.0",
|
||||
port = wsPort,
|
||||
) else config.activeWebSocket?.also {
|
||||
it.port = wsPort
|
||||
}
|
||||
config.passiveWebSocket = null
|
||||
putBoolean("isInit", true)
|
||||
putBoolean("isEmergencyInitBefore", true)
|
||||
}
|
||||
XposedBridge.log("[Shamrock] 强制初始化配置完成,请重新打开QQ")
|
||||
} else {
|
||||
XposedBridge.log("[Shamrock] 程序逻辑错误或错误地多次强制初始化")
|
||||
mmkv.putBoolean("isEmergencyInitBefore", false)
|
||||
XposedBridge.log("[Shamrock] 如果执意要再次强制初始化,请重新执行程序")
|
||||
}
|
||||
}
|
||||
|
||||
private val mmkv: MMKV
|
||||
get() = MMKVFetcher.mmkvWithId("shamrock_config")
|
||||
|
||||
fun getUpResGroup(): String {
|
||||
return mmkv.getString("up_res_group", "") ?: ""
|
||||
}
|
||||
|
||||
fun aliveReply(): Boolean {
|
||||
return mmkv.getBoolean("alive_reply", false)
|
||||
}
|
||||
|
||||
fun allowTempSession(): Boolean {
|
||||
return Config.allowTempSession
|
||||
return config.allowTempSession
|
||||
}
|
||||
|
||||
fun getGroupMsgRule(): GroupRule? {
|
||||
return Config.rules?.groupRule
|
||||
return config.rules?.groupRule
|
||||
}
|
||||
|
||||
fun getPrivateRule(): PrivateRule? {
|
||||
return Config.rules?.privateRule
|
||||
return config.rules?.privateRule
|
||||
}
|
||||
|
||||
fun enableSyncMsgAsSentMsg(): Boolean {
|
||||
@ -137,7 +176,7 @@ internal object ShamrockConfig {
|
||||
}
|
||||
|
||||
fun getWebSocketClientAddress(): List<ConnectionConfig> {
|
||||
return Config.passiveWebSocket ?: emptyList()
|
||||
return config.passiveWebSocket ?: emptyList()
|
||||
}
|
||||
|
||||
fun openWebSocket(): Boolean {
|
||||
@ -145,11 +184,11 @@ internal object ShamrockConfig {
|
||||
}
|
||||
|
||||
fun getActiveWebSocketConfig(): ConnectionConfig? {
|
||||
return Config.activeWebSocket
|
||||
return config.activeWebSocket
|
||||
}
|
||||
|
||||
fun getToken(): String {
|
||||
return Config.defaultToken ?: ""
|
||||
return config.defaultToken ?: ""
|
||||
}
|
||||
|
||||
fun useCQ(): Boolean {
|
||||
@ -236,7 +275,7 @@ internal object ShamrockConfig {
|
||||
}
|
||||
|
||||
fun isAntiTrace(): Boolean {
|
||||
return Config.antiTrace
|
||||
return config.antiTrace
|
||||
}
|
||||
|
||||
fun allowShell(): Boolean {
|
||||
|
@ -81,7 +81,6 @@ class AntiDetection: IAction {
|
||||
} else if (!pref.getBoolean("super_anti", false)) {
|
||||
return
|
||||
}
|
||||
//System.loadLibrary("clover")
|
||||
NativeLoader.load("clover")
|
||||
val env = XposedEntry.hasEnv()
|
||||
val injected = XposedEntry.injected()
|
||||
|
@ -70,6 +70,7 @@ class PullConfig: IAction {
|
||||
DataRequester.request("init", onFailure = {
|
||||
if (!ShamrockConfig.isInit()) {
|
||||
ctx.toast("请启动Shamrock主进程以初始化服务,进程将退出。")
|
||||
ShamrockConfig.putDefaultSettings()
|
||||
thread {
|
||||
Thread.sleep(3000)
|
||||
exitProcess(1)
|
||||
|
@ -1,9 +1,11 @@
|
||||
package moe.fuqiuluo.shamrock.xposed.loader
|
||||
|
||||
object LuoClassloader: ClassLoader() {
|
||||
internal object LuoClassloader: ClassLoader() {
|
||||
lateinit var hostClassLoader: ClassLoader
|
||||
lateinit var ctxClassLoader: ClassLoader
|
||||
|
||||
internal val moduleLoader: ClassLoader = LuoClassloader::class.java.classLoader!!
|
||||
|
||||
fun load(name: String): Class<*>? {
|
||||
return kotlin.runCatching {
|
||||
loadClass(name)
|
||||
|
@ -1,12 +1,11 @@
|
||||
package moe.fuqiuluo.shamrock.xposed.loader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.tmpnativehelper.moduleClassLoader
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader.moduleLoader
|
||||
import mqq.app.MobileQQ
|
||||
import oicq.wlogin_sdk.tools.MD5
|
||||
import java.io.File
|
||||
@ -20,28 +19,24 @@ internal object NativeLoader {
|
||||
return externalLibPath.resolve("libffmpegkit.so").exists()
|
||||
}
|
||||
|
||||
private val isEmu: Boolean
|
||||
get() {
|
||||
if (Build.SUPPORTED_ABIS.any { it.contains("x86") }) {
|
||||
XposedBridge.log("[Shamrock] 通过SUPPORTED_ABIS检测到 Android x86")
|
||||
return true
|
||||
}
|
||||
return try {
|
||||
val clazz = Class.forName("dalvik.system.VMRuntime")
|
||||
val method = clazz.getDeclaredMethod("getRuntime")
|
||||
val runtime = method.invoke(null)
|
||||
val field = clazz.getDeclaredField("vmInstructionSet")
|
||||
field.isAccessible = true
|
||||
val instructionSet = field.get(runtime) as String
|
||||
if ( instructionSet.contains("x86") ) {
|
||||
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
|
||||
true
|
||||
} else false
|
||||
} catch (e: Exception) {
|
||||
XposedBridge.log("[Shamrock] $e")
|
||||
false
|
||||
}
|
||||
private val isEmu: Boolean = runCatching {
|
||||
if (Build.SUPPORTED_ABIS.any { it.contains("x86") }) {
|
||||
XposedBridge.log("[Shamrock] 通过SUPPORTED_ABIS检测到 Android x86")
|
||||
return@runCatching true
|
||||
}
|
||||
val clazz = Class.forName("dalvik.system.VMRuntime")
|
||||
val method = clazz.getDeclaredMethod("getRuntime")
|
||||
val runtime = method.invoke(null)
|
||||
val field = clazz.getDeclaredField("vmInstructionSet")
|
||||
field.isAccessible = true
|
||||
val instructionSet = field.get(runtime) as String
|
||||
if (instructionSet.contains("x86") ) {
|
||||
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
|
||||
true
|
||||
} else false
|
||||
}.onFailure {
|
||||
XposedBridge.log("[Shamrock] ${it.stackTraceToString()}")
|
||||
}.getOrElse { false }
|
||||
|
||||
private fun getLibFilePath(name: String): String {
|
||||
return if (isEmu) "lib/x86_64/lib${name}.so" else "lib/arm64-v8a/lib$name.so"
|
||||
@ -50,48 +45,61 @@ internal object NativeLoader {
|
||||
/**
|
||||
* 使目标进程可以使用来自模块的库
|
||||
*/
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
fun load(name: String) {
|
||||
try {
|
||||
if (name == "shamrock" || name == "clover") {
|
||||
onLoadByCopiedLibrary(name, getCtx())
|
||||
} else {
|
||||
val sourceFile = externalLibPath.resolve("lib$name.so")
|
||||
val soFile = MobileQQ.getContext().filesDir.parentFile!!.resolve("txlib").resolve("lib$name.so")
|
||||
if (!soFile.exists()) {
|
||||
if (!sourceFile.exists()) {
|
||||
LogCenter.log("LoadExternalLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
|
||||
return
|
||||
} else {
|
||||
sourceFile.copyTo(soFile)
|
||||
Runtime.getRuntime().exec("chmod 755 ${soFile.absolutePath}").waitFor()
|
||||
}
|
||||
}
|
||||
LogCenter.log("LoadExternalLibrary(name = $name)")
|
||||
System.load(soFile.absolutePath)
|
||||
if (name == "shamrock"
|
||||
|| (name == "clover" && isEmu)
|
||||
) {
|
||||
onLoadByResource(name)
|
||||
} else if (!onLoadByAbsolutePath(name)) {
|
||||
onLoadByExternalFile(name)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
XposedBridge.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun getCtx() = MobileQQ.getContext()
|
||||
private fun onLoadByAbsolutePath(name: String): Boolean {
|
||||
val context = MobileQQ.getContext()
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0)
|
||||
val file = File(applicationInfo.nativeLibraryDir)
|
||||
LogCenter.log("LoadLibrary(name = $name)")
|
||||
loadLibrary(file.resolve("lib$name.so").also {
|
||||
if (!it.exists()) {
|
||||
LogCenter.log("LoadLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
|
||||
return false
|
||||
}
|
||||
}.absolutePath, false)
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
private fun onLoadByCopiedLibrary(name: String, context: Context) {
|
||||
val soDir = File(context.filesDir, "SM_LIBS")
|
||||
if (soDir.isFile) {
|
||||
soDir.delete()
|
||||
}
|
||||
if (!soDir.exists()) {
|
||||
soDir.mkdirs()
|
||||
private fun onLoadByExternalFile(name: String) {
|
||||
val sourceFile = externalLibPath.resolve("lib$name.so")
|
||||
val soFile = MobileQQ.getContext().filesDir.parentFile!!
|
||||
.resolve("txlib").resolve("lib$name.so")
|
||||
if (!soFile.exists()) {
|
||||
if (!sourceFile.exists()) {
|
||||
LogCenter.log("LoadExternalLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
|
||||
return
|
||||
} else {
|
||||
sourceFile.copyTo(soFile)
|
||||
}
|
||||
}
|
||||
LogCenter.log("LoadExternalLibrary(name = $name)")
|
||||
loadLibrary(soFile.absolutePath)
|
||||
}
|
||||
|
||||
private fun onLoadByResource(name: String) {
|
||||
val soDir = File(MobileQQ.getContext().filesDir, "SM_LIBS")
|
||||
if (soDir.isFile) soDir.delete()
|
||||
if (!soDir.exists()) soDir.mkdirs()
|
||||
val soPath = getLibFilePath(name)
|
||||
val soFile = File(soDir, name)
|
||||
fun reloadSo(tmp: File? = null) {
|
||||
LogCenter.log("SO文件大小不一致或不存在,正在重新加载", Level.INFO)
|
||||
soFile.delete()
|
||||
if (tmp == null) moduleClassLoader.getResourceAsStream(soPath).use { origin ->
|
||||
if (tmp == null) moduleLoader.getResourceAsStream(soPath).use { origin ->
|
||||
soFile.outputStream().use { origin.copyTo(it) }
|
||||
} else tmp.renameTo(soFile)
|
||||
}
|
||||
@ -99,10 +107,10 @@ internal object NativeLoader {
|
||||
if (!soFile.exists()) {
|
||||
reloadSo()
|
||||
} else {
|
||||
val tmpSoFile = soFile.resolve("$name.tmp").also { file ->
|
||||
val tmpSoFile = File(soDir, "$name.tmp").also { file ->
|
||||
if (file.exists()) file.delete()
|
||||
file.outputStream().use {
|
||||
moduleClassLoader.getResourceAsStream(soPath).use { origin ->
|
||||
moduleLoader.getResourceAsStream(soPath).use { origin ->
|
||||
origin.copyTo(it)
|
||||
}
|
||||
}
|
||||
@ -111,18 +119,20 @@ internal object NativeLoader {
|
||||
it != MD5.getFileMD5(tmpSoFile)
|
||||
}) {
|
||||
reloadSo(tmpSoFile)
|
||||
}
|
||||
}
|
||||
try {
|
||||
System.load(soFile.absolutePath)
|
||||
LogCenter.log("加载SO文件成功 -> ${soFile.path}", Level.INFO)
|
||||
} catch (e: Throwable) {
|
||||
LogCenter.log(e.toString(), Level.WARN)
|
||||
throw e
|
||||
} else { tmpSoFile.delete() }
|
||||
}
|
||||
loadLibrary(soFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
LogCenter.log(e.toString(), Level.WARN)
|
||||
LogCenter.log(e.toString(), Level.ERROR)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
private fun loadLibrary(path: String, autoChmod: Boolean = true) {
|
||||
if (autoChmod) Runtime.getRuntime()
|
||||
.exec("chmod 755 $path").waitFor()
|
||||
System.load(path)
|
||||
LogCenter.log({ "加载SO文件成功 -> $path" }, Level.DEBUG)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
@file:JvmName("ModuleInfo")
|
||||
package moe.fuqiuluo.shamrock.xposed.loader.tmpnativehelper
|
||||
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||
|
||||
val moduleClassLoader: ClassLoader = LuoClassloader::class.java.classLoader!!
|
Reference in New Issue
Block a user