refactor: MenuBuilderHook for QQNT

This commit is contained in:
klxiaoniu
2024-05-26 10:08:24 +08:00
parent 54eb5bbce3
commit 81d5d97b26
8 changed files with 352 additions and 335 deletions

View File

@@ -51,6 +51,7 @@ import com.tencent.qqnt.kernel.nativeinterface.Contact;
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService;
import com.tencent.qqnt.kernel.nativeinterface.MsgAttributeInfo;
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord;
import com.xiaoniu.dispatcher.OnMenuBuilder;
import com.xiaoniu.util.ContextUtils;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
@@ -75,7 +76,6 @@ import io.github.qauxv.util.dexkit.DexKit;
import io.github.qauxv.util.dexkit.DexKitTarget;
import io.github.qauxv.util.dexkit.VasAttrBuilder;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -90,7 +90,7 @@ import kotlinx.coroutines.flow.MutableStateFlow;
@FunctionHookEntry
@UiItemAgentEntry
public class RepeaterPlus extends BaseFunctionHook implements SessionHooker.IAIOParamUpdate {
public class RepeaterPlus extends BaseFunctionHook implements SessionHooker.IAIOParamUpdate, OnMenuBuilder {
public static final RepeaterPlus INSTANCE = new RepeaterPlus();
@@ -230,58 +230,6 @@ public class RepeaterPlus extends BaseFunctionHook implements SessionHooker.IAIO
}
}
} else {
Class msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem");
String[] component = new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.reply.AIOReplyComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.anisticker.AIOAniStickerContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.video.AIOVideoContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.multifoward.AIOMultifowardContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.longmsg.AIOLongMsgContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.mix.AIOMixContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.file.AIOFileContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.LocationShare.AIOLocationShareComponent"
};
Method getMsg = null;
String absListMethod = null;
Method[] methods = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent").getDeclaredMethods();
for (Method method : methods) {
if (getMsg == null && method.getReturnType() == msgClass && method.getParameterTypes().length == 0) {
getMsg = method;
getMsg.setAccessible(true);
} else if (absListMethod == null && Modifier.isAbstract(method.getModifiers()) && method.getReturnType() == List.class
&& method.getParameterTypes().length == 0) {
absListMethod = method.getName();
}
}
Method finalGetMsg = getMsg;
for (String s : component) {
Class componentClazz = Initiator.loadClass(s);
Method listMethod = componentClazz.getMethod(absListMethod);
HookUtils.hookAfterIfEnabled(this, listMethod, param -> {
if (ContextUtils.getCurrentActivity().getClass().getName().contains("MultiForwardActivity")) {
return;
}
Object msg = finalGetMsg.invoke(param.thisObject);
Object item = CustomMenu.createItemNt(msg, "+1", R.id.item_repeat, () -> {
if (isMessageRepeatable(msg)) {
repeatByForwardNt(msg);
} else {
Toasts.error(ContextUtils.getCurrentActivity(), "该消息不支持复读");
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
List result = new ArrayList<>();
result.add(0, item);
result.addAll(list);
param.setResult(result);
});
}
}
return true;
}
@@ -451,4 +399,44 @@ public class RepeaterPlus extends BaseFunctionHook implements SessionHooker.IAIO
}
}
@NonNull
@Override
public String[] getTargetComponentTypes() {
return new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.reply.AIOReplyComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.anisticker.AIOAniStickerContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.video.AIOVideoContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.multifoward.AIOMultifowardContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.longmsg.AIOLongMsgContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.mix.AIOMixContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.file.AIOFileContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.LocationShare.AIOLocationShareComponent"
};
}
@Override
public void onGetMenuNt(@NonNull Object msg, @NonNull String componentType, @NonNull XC_MethodHook.MethodHookParam param) throws Exception {
if (!isEnabled() || !RepeaterPlusIconSettingDialog.getIsShowInMenu()) {
return;
}
if (ContextUtils.getCurrentActivity().getClass().getName().contains("MultiForwardActivity")) {
return;
}
Object item = CustomMenu.createItemNt(msg, "+1", R.id.item_repeat, () -> {
if (isMessageRepeatable(msg)) {
repeatByForwardNt(msg);
} else {
Toasts.error(ContextUtils.getCurrentActivity(), "该消息不支持复读");
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
List result = new ArrayList<>();
result.add(0, item);
result.addAll(list);
param.setResult(result);
}
}

View File

@@ -46,7 +46,9 @@ import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService;
import com.tencent.qqnt.kernel.nativeinterface.MsgElement;
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord;
import com.tencent.qqnt.kernel.nativeinterface.PicElement;
import com.xiaoniu.dispatcher.OnMenuBuilder;
import com.xiaoniu.util.ContextUtils;
import de.robv.android.xposed.XC_MethodHook;
import io.github.qauxv.R;
import io.github.qauxv.base.annotation.FunctionHookEntry;
import io.github.qauxv.base.annotation.UiItemAgentEntry;
@@ -56,9 +58,7 @@ import io.github.qauxv.dsl.FunctionEntryRouter;
import io.github.qauxv.hook.CommonSwitchFunctionHook;
import io.github.qauxv.ui.CommonContextWrapper;
import io.github.qauxv.util.CustomMenu;
import io.github.qauxv.util.Initiator;
import io.github.qauxv.util.SyncUtils;
import io.github.qauxv.util.Toasts;
import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem;
import io.github.qauxv.util.dexkit.ChatPanel_InitPanel_QQNT;
import io.github.qauxv.util.dexkit.DexKit;
@@ -74,7 +74,7 @@ import org.json.JSONObject;
@FunctionHookEntry
@UiItemAgentEntry
public class StickerPanelEntryHooker extends CommonSwitchFunctionHook implements SessionHooker.IAIOParamUpdate {
public class StickerPanelEntryHooker extends CommonSwitchFunctionHook implements SessionHooker.IAIOParamUpdate, OnMenuBuilder {
public static final StickerPanelEntryHooker INSTANCE = new StickerPanelEntryHooker();
public static Object AIOParam;
private StickerPanelEntryHooker() {
@@ -146,100 +146,6 @@ public class StickerPanelEntryHooker extends CommonSwitchFunctionHook implements
}
);
//Hook for longClick msgItem
{
Class msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem");
String[] component = new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.mix.AIOMixContentComponent",
};
Method getMsg = null;
Method[] methods = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent").getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == msgClass && method.getParameterTypes().length == 0) {
getMsg = method;
getMsg.setAccessible(true);
break;
}
}
for (String s : component) {
Class componentClazz = Initiator.loadClass(s);
Method listMethod = null;
methods = componentClazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == List.class && method.getParameterTypes().length == 0) {
listMethod = method;
listMethod.setAccessible(true);
break;
}
}
Method finalGetMsg = getMsg;
HookUtils.hookAfterIfEnabled(this, listMethod, param -> {
Object msg = finalGetMsg.invoke(param.thisObject);
Object item = CustomMenu.createItemNt(msg, "保存到面板", R.id.item_save_to_panel, () -> {
try {
long msgID = (long) Reflex.invokeVirtual(msg, "getMsgId");
IKernelMsgService service = MsgServiceHelper.getKernelMsgService(AppRuntimeHelper.getAppRuntime());
ArrayList<Long> msgIDs = new ArrayList<>();
msgIDs.add(msgID);
service.getMsgsByMsgId(SessionUtils.AIOParam2Contact(AIOParam), msgIDs, (result, errMsg, msgList) -> {
SyncUtils.runOnUiThread(()->{
for (MsgRecord msgRecord : msgList) {
ArrayList<String> md5s = new ArrayList<>();
ArrayList<String> urls = new ArrayList<>();
for (MsgElement element : msgRecord.getElements()){
if (element.getPicElement() != null){
PicElement picElement = element.getPicElement();
//md5必须大写才能加载
md5s.add(picElement.getMd5HexStr().toUpperCase());
String originUrl = picElement.getOriginImageUrl();
if (TextUtils.isEmpty(originUrl)){
urls.add("https://gchat.qpic.cn/gchatpic_new/0/0-0-" + picElement.getMd5HexStr().toUpperCase() + "/0");
}else {
if (originUrl.startsWith("/download")){
if (originUrl.contains("appid=1406")){
urls.add("https://multimedia.nt.qq.com.cn" + originUrl + rkey_group);
}else {
urls.add("https://multimedia.nt.qq.com.cn" + originUrl + rkey_private);
}
}else {
urls.add("https://gchat.qpic.cn"+picElement.getOriginImageUrl());
}
}
}
}
if (!md5s.isEmpty()){
if (md5s.size() > 1){
PanelUtils.PreSaveMultiPicList(urls,md5s, CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()));
}else {
PanelUtils.PreSavePicToList(urls.get(0),md5s.get(0), CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()));
}
}
}
});
});
} catch (Exception e) {
XLog.e("StickerPanelEntryHooker.msgLongClickSaveToLocal", e);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
List result = new ArrayList<>();
result.add(0,item);
result.addAll(list);
param.setResult(result);
});
}
}
//Hook for change title
Method sendMsgMethod = XMethod
@@ -330,4 +236,75 @@ public class StickerPanelEntryHooker extends CommonSwitchFunctionHook implements
public void onAIOParamUpdate(Object AIOParam) {
StickerPanelEntryHooker.AIOParam = AIOParam;
}
@NonNull
@Override
public String[] getTargetComponentTypes() {
return new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent",
"com.tencent.mobileqq.aio.msglist.holder.component.mix.AIOMixContentComponent",
};
}
@Override
public void onGetMenuNt(@NonNull Object msg, @NonNull String componentType, @NonNull XC_MethodHook.MethodHookParam param) throws Exception {
if (!isEnabled()) return;
//Hook for longClick msgItem
Object item = CustomMenu.createItemNt(msg, "保存到面板", R.id.item_save_to_panel, () -> {
try {
long msgID = (long) Reflex.invokeVirtual(msg, "getMsgId");
IKernelMsgService service = MsgServiceHelper.getKernelMsgService(AppRuntimeHelper.getAppRuntime());
ArrayList<Long> msgIDs = new ArrayList<>();
msgIDs.add(msgID);
service.getMsgsByMsgId(SessionUtils.AIOParam2Contact(AIOParam), msgIDs, (result, errMsg, msgList) -> {
SyncUtils.runOnUiThread(()->{
for (MsgRecord msgRecord : msgList) {
ArrayList<String> md5s = new ArrayList<>();
ArrayList<String> urls = new ArrayList<>();
for (MsgElement element : msgRecord.getElements()){
if (element.getPicElement() != null){
PicElement picElement = element.getPicElement();
//md5必须大写才能加载
md5s.add(picElement.getMd5HexStr().toUpperCase());
String originUrl = picElement.getOriginImageUrl();
if (TextUtils.isEmpty(originUrl)){
urls.add("https://gchat.qpic.cn/gchatpic_new/0/0-0-" + picElement.getMd5HexStr().toUpperCase() + "/0");
}else {
if (originUrl.startsWith("/download")){
if (originUrl.contains("appid=1406")){
urls.add("https://multimedia.nt.qq.com.cn" + originUrl + rkey_group);
}else {
urls.add("https://multimedia.nt.qq.com.cn" + originUrl + rkey_private);
}
}else {
urls.add("https://gchat.qpic.cn"+picElement.getOriginImageUrl());
}
}
}
}
if (!md5s.isEmpty()){
if (md5s.size() > 1){
PanelUtils.PreSaveMultiPicList(urls,md5s, CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()));
}else {
PanelUtils.PreSavePicToList(urls.get(0),md5s.get(0), CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()));
}
}
}
});
});
} catch (Exception e) {
XLog.e("StickerPanelEntryHooker.msgLongClickSaveToLocal", e);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
List result = new ArrayList<>();
result.add(0,item);
result.addAll(list);
param.setResult(result);
}
}

View File

@@ -24,11 +24,12 @@ package cc.ioctl.hook.msg
import android.app.Activity
import android.content.Context
import android.view.View
import cc.ioctl.util.HookUtils
import cc.hicore.QApp.QAppUtils
import cc.ioctl.util.Reflex
import cc.ioctl.util.afterHookIfEnabled
import cc.ioctl.util.beforeHookIfEnabled
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.xiaoniu.dispatcher.OnMenuBuilder
import com.xiaoniu.util.ContextUtils
import de.robv.android.xposed.XC_MethodHook.MethodHookParam
import de.robv.android.xposed.XposedBridge
@@ -41,48 +42,24 @@ import io.github.qauxv.hook.CommonSwitchFunctionHook
import io.github.qauxv.util.CustomMenu
import io.github.qauxv.util.CustomMenu.createItemNt
import io.github.qauxv.util.Initiator
import io.github.qauxv.util.QQVersion
import io.github.qauxv.util.Toasts
import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem
import io.github.qauxv.util.dexkit.CArkAppItemBubbleBuilder
import io.github.qauxv.util.dexkit.DexKit
import io.github.qauxv.util.requireMinQQVersion
import xyz.nextalone.util.SystemServiceUtils.copyToClipboard
import xyz.nextalone.util.throwOrTrue
import java.lang.reflect.Array
import java.lang.reflect.Method
@FunctionHookEntry
@UiItemAgentEntry
object CopyCardMsg : CommonSwitchFunctionHook("CopyCardMsg::BaseChatPie", arrayOf(CArkAppItemBubbleBuilder, AbstractQQCustomMenuItem)) {
object CopyCardMsg : CommonSwitchFunctionHook("CopyCardMsg::BaseChatPie", arrayOf(CArkAppItemBubbleBuilder, AbstractQQCustomMenuItem)), OnMenuBuilder {
override val name = "复制卡片消息"
override val uiItemLocation = FunctionEntryRouter.Locations.Auxiliary.MESSAGE_CATEGORY
override fun initOnce() = throwOrTrue {
if (requireMinQQVersion(QQVersion.QQ_8_9_63)) {
val msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")
val getMsg: Method = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent").declaredMethods.first {
it.returnType == msgClass && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }
val componentClazz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent")
val listMethod: Method = componentClazz.declaredMethods.first {
it.returnType == MutableList::class.java && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }
HookUtils.hookAfterIfEnabled(this, listMethod) { param: MethodHookParam ->
val ctx = ContextUtils.getCurrentActivity()
val msg = getMsg.invoke(param.thisObject)
val item = createItemNt(msg, "复制代码", R.id.item_copy_code) {
val element = (msg.javaClass.declaredMethods.first {
it.returnType == MsgElement::class.java && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }.invoke(msg) as MsgElement).arkElement
copyToClipboard(ctx, element.bytesData)
Toasts.info(ctx, "复制成功")
}
val list = param.result as MutableList<Any>
list.add(item)
}
if (QAppUtils.isQQnt()) {
return@throwOrTrue
}
@@ -175,4 +152,19 @@ object CopyCardMsg : CommonSwitchFunctionHook("CopyCardMsg::BaseChatPie", arrayO
}
}
}
override val targetComponentTypes = arrayOf("com.tencent.mobileqq.aio.msglist.holder.component.ark.AIOArkContentComponent")
override fun onGetMenuNt(msg: Any, componentType: String, param: MethodHookParam) {
if (!isEnabled) return
val ctx = ContextUtils.getCurrentActivity()
val item = createItemNt(msg, "复制代码", R.id.item_copy_code) {
val element = (msg.javaClass.declaredMethods.first {
it.returnType == MsgElement::class.java && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }.invoke(msg) as MsgElement).arkElement
copyToClipboard(ctx, element.bytesData)
Toasts.info(ctx, "复制成功")
}
val list = param.result as MutableList<Any>
list.add(item)
}
}

View File

@@ -29,15 +29,15 @@ import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import cc.hicore.QApp.QAppUtils;
import cc.hicore.ReflectUtil.XField;
import cc.hicore.ReflectUtil.XMethod;
import cc.hicore.Utils.FunProtoData;
import cc.ioctl.util.HookUtils;
import cc.ioctl.util.HostInfo;
import cc.ioctl.util.Reflex;
import com.tencent.qphone.base.remote.FromServiceMsg;
import com.tencent.qphone.base.remote.ToServiceMsg;
import com.tencent.qqnt.kernel.nativeinterface.PicElement;
import com.xiaoniu.dispatcher.OnMenuBuilder;
import com.xiaoniu.util.ContextUtils;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
@@ -51,7 +51,6 @@ import io.github.qauxv.ui.CustomDialog;
import io.github.qauxv.util.CustomMenu;
import io.github.qauxv.util.Initiator;
import io.github.qauxv.util.LicenseStatus;
import io.github.qauxv.util.QQVersion;
import io.github.qauxv.util.Toasts;
import io.github.qauxv.util.dexkit.AbstractQQCustomMenuItem;
import io.github.qauxv.util.dexkit.DexKitTarget;
@@ -65,7 +64,7 @@ import xyz.nextalone.util.SystemServiceUtils;
@FunctionHookEntry
@UiItemAgentEntry
public class PicMd5Hook extends CommonSwitchFunctionHook {
public class PicMd5Hook extends CommonSwitchFunctionHook implements OnMenuBuilder {
public static final PicMd5Hook INSTANCE = new PicMd5Hook();
@@ -101,7 +100,7 @@ public class PicMd5Hook extends CommonSwitchFunctionHook {
HookUtils.hookBeforeIfEnabled(this, XMethod.clz("mqq.app.msghandle.MsgRespHandler").name("dispatchRespMsg").ignoreParam().get(), param -> {
FromServiceMsg fromServiceMsg = XField.obj(param.args[1]).name("fromServiceMsg").get();
if ("OidbSvcTrpcTcp.0x9067_202".equals(fromServiceMsg.getServiceCmd())){
if ("OidbSvcTrpcTcp.0x9067_202".equals(fromServiceMsg.getServiceCmd())) {
FunProtoData data = new FunProtoData();
data.fromBytes(getUnpPackage(fromServiceMsg.getWupBuffer()));
@@ -118,51 +117,7 @@ public class PicMd5Hook extends CommonSwitchFunctionHook {
}
});
if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) {
Class msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem");
Method getMsg = null;
Method[] methods = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent").getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == msgClass && method.getParameterTypes().length == 0) {
getMsg = method;
getMsg.setAccessible(true);
break;
}
}
Class componentClazz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent");
Method listMethod = null;
methods = componentClazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == List.class && method.getParameterTypes().length == 0) {
listMethod = method;
listMethod.setAccessible(true);
break;
}
}
Method finalGetMsg = getMsg;
HookUtils.hookAfterIfEnabled(this, listMethod, param -> {
Object msg = finalGetMsg.invoke(param.thisObject);
Object item = CustomMenu.createItemNt(msg, "MD5", R.id.item_showPicMd5, () -> {
try {
Method getElement = null;
for (Method m : msg.getClass().getDeclaredMethods()) {
if (m.getReturnType() == PicElement.class) {
getElement = m;
break;
}
}
PicElement element = (PicElement) getElement.invoke(msg);
String md5 = element.getMd5HexStr().toUpperCase();
showMd5Dialog(ContextUtils.getCurrentActivity(), md5, element);
} catch (Throwable e) {
traceError(e);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
list.add(item);
});
if (QAppUtils.isQQnt()) {
return true;
}
Class<?> cl_PicItemBuilder = Initiator._PicItemBuilder();
@@ -199,6 +154,38 @@ public class PicMd5Hook extends CommonSwitchFunctionHook {
return true;
}
@NonNull
@Override
public String[] getTargetComponentTypes() {
return new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent"
};
}
@Override
public void onGetMenuNt(@NonNull Object msg, @NonNull String componentType, @NonNull XC_MethodHook.MethodHookParam param) throws Exception {
if (!isEnabled()) return;
Object item = CustomMenu.createItemNt(msg, "MD5", R.id.item_showPicMd5, () -> {
try {
Method getElement = null;
for (Method m : msg.getClass().getDeclaredMethods()) {
if (m.getReturnType() == PicElement.class) {
getElement = m;
break;
}
}
PicElement element = (PicElement) getElement.invoke(msg);
String md5 = element.getMd5HexStr().toUpperCase();
showMd5Dialog(ContextUtils.getCurrentActivity(), md5, element);
} catch (Throwable e) {
traceError(e);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
list.add(item);
}
public static class GetMenuItemCallBack extends XC_MethodHook {
public GetMenuItemCallBack() {

View File

@@ -52,11 +52,13 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import cc.hicore.QApp.QAppUtils;
import cc.ioctl.util.DebugUtils;
import cc.ioctl.util.HookUtils;
import cc.ioctl.util.HostStyledViewBuilder;
import cc.ioctl.util.Reflex;
import com.tencent.qqnt.kernel.nativeinterface.PttElement;
import com.xiaoniu.dispatcher.OnMenuBuilder;
import com.xiaoniu.util.ContextUtils;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
@@ -74,10 +76,7 @@ import io.github.qauxv.ui.CommonContextWrapper;
import io.github.qauxv.ui.CustomDialog;
import io.github.qauxv.ui.ResUtils;
import io.github.qauxv.util.CustomMenu;
import io.github.qauxv.util.HostInfo;
import io.github.qauxv.util.Initiator;
import io.github.qauxv.util.Log;
import io.github.qauxv.util.QQVersion;
import io.github.qauxv.util.SyncUtils;
import io.github.qauxv.util.Toasts;
import io.github.qauxv.util.data.ContactDescriptor;
@@ -99,7 +98,7 @@ import kotlin.Unit;
@FunctionHookEntry
@UiItemAgentEntry
public class PttForwardHook extends CommonSwitchFunctionHook {
public class PttForwardHook extends CommonSwitchFunctionHook implements OnMenuBuilder {
public static final String qn_cache_ptt_save_last_parent_dir = "qn_cache_ptt_save_last_parent_dir";
public static final PttForwardHook INSTANCE = new PttForwardHook();
@@ -386,54 +385,7 @@ public class PttForwardHook extends CommonSwitchFunctionHook {
}
});
if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) {
Class msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem");
Method getMsg = null;
Method[] methods = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent").getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == msgClass && method.getParameterTypes().length == 0) {
getMsg = method;
getMsg.setAccessible(true);
break;
}
}
Class componentClazz = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.ptt.AIOPttContentComponent");
Method listMethod = null;
methods = componentClazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getReturnType() == List.class && method.getParameterTypes().length == 0) {
listMethod = method;
listMethod.setAccessible(true);
break;
}
}
Method finalGetMsg = getMsg;
HookUtils.hookAfterIfEnabled(this, listMethod, param -> {
Object msg = finalGetMsg.invoke(param.thisObject);
Activity context = ContextUtils.getCurrentActivity();
Object item = CustomMenu.createItemNt(msg, "转发", R.id.item_ptt_forward, () -> {
File file = getPttFileByMsgNt(msg);
if (!file.exists()) {
Toasts.error(context, "未找到语音文件");
} else {
sendForwardIntent(context, file);
}
return Unit.INSTANCE;
});
Object item2 = CustomMenu.createItemNt(msg, "保存", R.id.item_ptt_save, () -> {
File file = getPttFileByMsgNt(msg);
if (!file.exists()) {
Toasts.error(context, "未找到语音文件");
} else {
showSavePttFileDialog(context, file);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
list.add(item);
list.add(item2);
});
if (QAppUtils.isQQnt()) {
return true;
}
Class<?> kPttItemBuilder = _PttItemBuilder();
@@ -525,4 +477,40 @@ public class PttForwardHook extends CommonSwitchFunctionHook {
context.startActivity(intent);
}
@NonNull
@Override
public String[] getTargetComponentTypes() {
return new String[]{
"com.tencent.mobileqq.aio.msglist.holder.component.ptt.AIOPttContentComponent"
};
}
@Override
public void onGetMenuNt(@NonNull Object msg, @NonNull String componentType, @NonNull XC_MethodHook.MethodHookParam param) throws Exception {
if (!isEnabled()) {
return;
}
Activity context = ContextUtils.getCurrentActivity();
Object item = CustomMenu.createItemNt(msg, "转发", R.id.item_ptt_forward, () -> {
File file = getPttFileByMsgNt(msg);
if (!file.exists()) {
Toasts.error(context, "未找到语音文件");
} else {
sendForwardIntent(context, file);
}
return Unit.INSTANCE;
});
Object item2 = CustomMenu.createItemNt(msg, "保存", R.id.item_ptt_save, () -> {
File file = getPttFileByMsgNt(msg);
if (!file.exists()) {
Toasts.error(context, "未找到语音文件");
} else {
showSavePttFileDialog(context, file);
}
return Unit.INSTANCE;
});
List list = (List) param.getResult();
list.add(item);
list.add(item2);
}
}

View File

@@ -0,0 +1,97 @@
/*
* QAuxiliary - An Xposed module for QQ/TIM
* Copyright (C) 2019-2024 QAuxiliary developers
* https://github.com/cinit/QAuxiliary
*
* This software is an opensource software: you can redistribute it
* and/or modify it under the terms of the General Public License
* as published by the Free Software Foundation; either
* version 3 of the License, or any later version as published
* by QAuxiliary contributors.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the General Public License for more details.
*
* You should have received a copy of the General Public License
* along with this software.
* If not, see
* <https://github.com/cinit/QAuxiliary/blob/master/LICENSE.md>.
*/
package com.xiaoniu.dispatcher
import cc.hicore.QApp.QAppUtils
import cc.hicore.hook.RepeaterPlus
import cc.hicore.hook.stickerPanel.Hooker.StickerPanelEntryHooker
import cc.ioctl.hook.msg.CopyCardMsg
import cc.ioctl.hook.msg.PicMd5Hook
import cc.ioctl.hook.msg.PttForwardHook
import cc.ioctl.util.HookUtils
import com.github.kyuubiran.ezxhelper.utils.isAbstract
import de.robv.android.xposed.XC_MethodHook
import io.github.duzhaokun123.hook.MessageCopyHook
import io.github.qauxv.base.annotation.FunctionHookEntry
import io.github.qauxv.hook.BasePersistBackgroundHook
import io.github.qauxv.util.Initiator
import me.ketal.hook.PicCopyToClipboard
import java.lang.reflect.Method
@FunctionHookEntry
object MenuBuilderHook : BasePersistBackgroundHook() {
// These hooks are called when the menu is being built.
private val decorators: Array<OnMenuBuilder> = arrayOf(
RepeaterPlus.INSTANCE,
StickerPanelEntryHooker.INSTANCE,
PicMd5Hook.INSTANCE,
PttForwardHook.INSTANCE,
CopyCardMsg,
MessageCopyHook,
PicCopyToClipboard
)
override fun initOnce(): Boolean {
if (QAppUtils.isQQnt()) { // NT only
val msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")
val baseContentComponentClass = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent")
val getMsgMethod: Method = baseContentComponentClass.declaredMethods.first {
it.returnType == msgClass && it.parameterTypes.isEmpty()
}.apply { isAccessible = true }
val listMethodName: String = baseContentComponentClass.declaredMethods.first {
it.isAbstract && it.returnType == MutableList::class.java && it.parameterTypes.isEmpty()
}.name
val targets = mutableSetOf<String>()
for (decorator in decorators) {
targets.addAll(decorator.targetComponentTypes)
}
for (target in targets) {
val targetClass = Initiator.loadClass(target)
HookUtils.hookAfterAlways(this, targetClass.getMethod(listMethodName), 48) {
val msg = getMsgMethod.invoke(it.thisObject)!!
for (decorator in decorators) {
if (target in decorator.targetComponentTypes) {
try {
decorator.onGetMenuNt(msg, target, it)
} catch (e: Exception) {
traceError(e)
}
}
}
}
}
}
return true
}
}
interface OnMenuBuilder {
val targetComponentTypes: Array<String>
@Throws(Exception::class)
fun onGetMenuNt(
msg: Any,
componentType: String,
param: XC_MethodHook.MethodHookParam
)
}

View File

@@ -28,13 +28,13 @@ import android.content.Context
import android.util.Log
import android.view.View
import android.widget.TextView
import cc.hicore.QApp.QAppUtils
import cc.ioctl.util.HostInfo
import cc.ioctl.util.Reflex
import cc.ioctl.util.afterHookIfEnabled
import cc.ioctl.util.hookAfterIfEnabled
import com.github.kyuubiran.ezxhelper.utils.invokeMethodAutoAs
import com.github.kyuubiran.ezxhelper.utils.paramCount
import com.xiaoniu.dispatcher.OnMenuBuilder
import com.xiaoniu.util.ContextUtils
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import io.github.qauxv.R
@@ -51,40 +51,17 @@ import io.github.qauxv.util.dexkit.DexDeobfsProvider
import io.github.qauxv.util.dexkit.DexKit
import io.github.qauxv.util.dexkit.DexKitFinder
import io.github.qauxv.util.dexkit.TextMsgItem_getText
import xyz.nextalone.util.method
import java.lang.reflect.Modifier
@FunctionHookEntry
@UiItemAgentEntry
object MessageCopyHook : CommonSwitchFunctionHook(), DexKitFinder {
object MessageCopyHook : CommonSwitchFunctionHook(), DexKitFinder, OnMenuBuilder {
const val TAG = "MessageCopyHook"
override val name: String
get() = "文本消息自由复制"
override fun initOnce(): Boolean {
if (HostInfo.requireMinQQVersion(QQVersion.QQ_8_9_63)) { // TODO: support ark message
val class_AIOMsgItem = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")
val class_BaseContentComponent = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.BaseContentComponent")
val method_getMsg = class_BaseContentComponent.method { it.returnType == class_AIOMsgItem && it.paramCount == 0 }!!
method_getMsg.isAccessible = true
val class_AIOTextContentComponent = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent")
val method_list = class_AIOTextContentComponent.method { it.returnType == List::class.java && it.paramCount == 0 }!!
method_list.isAccessible = true
hookAfterIfEnabled(method_list) { param ->
val msg = method_getMsg.invoke(param.thisObject)
val item = CustomMenu.createItemNt(msg, "自由复制", R.id.item_free_copy) {
// Log.d(msg.javaClass.name)
val text = try {
DexKit.requireMethodFromCache(TextMsgItem_getText).also {
it.isAccessible = true
}.invoke(msg) as CharSequence
} catch (e: Exception) {
"${e.javaClass.name}: ${e.message}\n" + (e.stackTrace.joinToString("\n"))
}
showDialog(CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()), text)
}
param.result = (param.result as List<*>) + item
}
if (QAppUtils.isQQnt()) {
return true
}
@@ -191,4 +168,22 @@ object MessageCopyHook : CommonSwitchFunctionHook(), DexKitFinder {
}
return true
}
override val targetComponentTypes = arrayOf("com.tencent.mobileqq.aio.msglist.holder.component.text.AIOTextContentComponent")
override fun onGetMenuNt(msg: Any, componentType: String, param: XC_MethodHook.MethodHookParam) {
if (!isEnabled) return
// TODO: support ark message
val item = CustomMenu.createItemNt(msg, "自由复制", R.id.item_free_copy) {
val text = try {
DexKit.requireMethodFromCache(TextMsgItem_getText).also {
it.isAccessible = true
}.invoke(msg) as CharSequence
} catch (e: Exception) {
"${e.javaClass.name}: ${e.message}\n" + (e.stackTrace.joinToString("\n"))
}
showDialog(CommonContextWrapper.createAppCompatContext(ContextUtils.getCurrentActivity()), text)
}
param.result = (param.result as List<*>) + item
}
}

View File

@@ -33,6 +33,8 @@ import com.github.kyuubiran.ezxhelper.utils.Log
import com.github.kyuubiran.ezxhelper.utils.findMethod
import com.github.kyuubiran.ezxhelper.utils.findMethodOrNull
import com.github.kyuubiran.ezxhelper.utils.tryOrLogFalse
import com.xiaoniu.dispatcher.OnMenuBuilder
import de.robv.android.xposed.XC_MethodHook
import io.github.qauxv.R
import io.github.qauxv.base.annotation.FunctionHookEntry
import io.github.qauxv.base.annotation.UiItemAgentEntry
@@ -62,7 +64,7 @@ object PicCopyToClipboard : CommonSwitchFunctionHook(
arrayOf(
AbstractQQCustomMenuItem
)
) {
), OnMenuBuilder {
override val name: String = "复制图片到剪贴板"
override val description: String = "复制图片到剪贴板,可以在聊天窗口中粘贴使用"
@@ -73,7 +75,6 @@ object PicCopyToClipboard : CommonSwitchFunctionHook(
override fun initOnce() = tryOrLogFalse {
if (QAppUtils.isQQnt()) {
hookNt()
return@tryOrLogFalse
}
val clsPicItemBuilder = _PicItemBuilder()
@@ -119,31 +120,6 @@ object PicCopyToClipboard : CommonSwitchFunctionHook(
}
}
private fun hookNt() {
val msgClass = Initiator.loadClass("com.tencent.mobileqq.aio.msg.AIOMsgItem")
val picContentComponent = Initiator.loadClass("com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent")
val listMethod = picContentComponent.findMethod {
returnType == List::class.java && parameterTypes.isEmpty()
}
val getMsg = picContentComponent.findMethod(findSuper = true) {
parameterTypes.isEmpty() && returnType == msgClass
}
listMethod.hookAfter(this) {
val list = it.result as MutableList<Any>
val msg = getMsg.invoke(it.thisObject)!!
val context = it.thisObject.invoke("getMContext")!!
val item = CustomMenu.createItemNt(msg, "复制图片", R.id.item_copyToClipboard) {
runCatching {
val file = File(getFilePathNt(msg))
onClick(context as Context, file)
}.onFailure { t ->
Log.e(t)
}
}
list.add(item)
}
}
private fun onClick(context: Context, file: File) {
if (!file.exists()) {
Toasts.info(context, "请查看原图后复制")
@@ -224,4 +200,21 @@ object PicCopyToClipboard : CommonSwitchFunctionHook(
}
return path
}
override val targetComponentTypes = arrayOf("com.tencent.mobileqq.aio.msglist.holder.component.pic.AIOPicContentComponent")
override fun onGetMenuNt(msg: Any, componentType: String, param: XC_MethodHook.MethodHookParam) {
if (!isEnabled) return
val list = param.result as MutableList<Any>
val context = param.thisObject.invoke("getMContext")!!
val item = CustomMenu.createItemNt(msg, "复制图片", R.id.item_copyToClipboard) {
runCatching {
val file = File(getFilePathNt(msg))
onClick(context as Context, file)
}.onFailure { t ->
Log.e(t)
}
}
list.add(item)
}
}