feat(tng): 红包领取台双通道推送,并修复金额/按包分组
Hook Quake RPC 抓 Money Packet;解析 Money 对象金额;调试台按单个红包展示排行;支持 USB 本机与共享网段双地址转发。
This commit is contained in:
@@ -49,6 +49,7 @@ public final class TngMoneyPacketHook {
|
||||
hookOkHttpUrl(lpparam);
|
||||
hookResponseBody(lpparam);
|
||||
hookGsonFromJson(lpparam);
|
||||
hookQuakeRpc(lpparam);
|
||||
XposedBridge.log(TAG + " installed for " + lpparam.packageName);
|
||||
}
|
||||
|
||||
@@ -167,11 +168,110 @@ public final class TngMoneyPacketHook {
|
||||
if (TextUtils.isEmpty(className)) {
|
||||
return false;
|
||||
}
|
||||
return className.contains("MmpDetailResult")
|
||||
|| className.contains("MmpClaimQueryResult")
|
||||
|| className.contains("MmpClaimResult")
|
||||
|| className.contains("MmpReceiver")
|
||||
|| className.contains("MmpDetailLeaderboard");
|
||||
String n = className;
|
||||
return n.contains("MmpDetail")
|
||||
|| n.contains("MmpClaim")
|
||||
|| n.contains("MmpReceiver")
|
||||
|| n.contains("MmpLeaderboard")
|
||||
|| n.contains("MoneyPacketDetail")
|
||||
|| n.contains("MoneyPacketClaim")
|
||||
|| n.contains("MoneyPacketLeader")
|
||||
|| n.contains("MoneyPacketDoClaim")
|
||||
|| n.contains("ClaimResultDto")
|
||||
|| n.contains("ClaimDto")
|
||||
|| n.contains("ReceiverList")
|
||||
|| n.contains("packetReceiver");
|
||||
}
|
||||
|
||||
private static void hookQuakeRpc(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
String[] rpcClasses = {
|
||||
"my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost",
|
||||
"com.alipay.imobile.network.quake.rpc.RpcInvocationHandler",
|
||||
};
|
||||
XC_MethodHook hook = new XC_MethodHook() {
|
||||
@Override
|
||||
protected void afterHookedMethod(MethodHookParam param) {
|
||||
if (param.hasThrowable() || param.getResult() == null) {
|
||||
return;
|
||||
}
|
||||
String op = extractMoneyPacketRpcOp(param.args);
|
||||
if (op == null) {
|
||||
return;
|
||||
}
|
||||
Object result = param.getResult();
|
||||
String className = result.getClass().getName();
|
||||
XposedBridge.log(TAG + " rpc hit op=" + op + " type=" + className);
|
||||
try {
|
||||
String json = buildJsonFromObject(result);
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
json = tryFastjson(result);
|
||||
}
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
XposedBridge.log(TAG + " rpc serialize empty op=" + op);
|
||||
return;
|
||||
}
|
||||
forwardParsedJson(json, op, "rpc");
|
||||
MmpSnapshot probe = parseSnapshot(json);
|
||||
if (probe == null || probe.claims.isEmpty()) {
|
||||
String snippet = json.length() > 500 ? json.substring(0, 500) + "..." : json;
|
||||
XposedBridge.log(TAG + " rpc no-claims op=" + op + " body=" + snippet);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " rpc capture failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
for (String className : rpcClasses) {
|
||||
try {
|
||||
Class<?> clazz = XposedHelpers.findClass(className, lpparam.classLoader);
|
||||
int hooked = 0;
|
||||
for (Method method : clazz.getDeclaredMethods()) {
|
||||
if (!"invoke".equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
XposedBridge.hookMethod(method, hook);
|
||||
hooked++;
|
||||
}
|
||||
if (hooked > 0) {
|
||||
XposedBridge.log(TAG + " hooked RPC " + className + " invoke=" + hooked);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " skip RPC " + className + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractMoneyPacketRpcOp(Object[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof Method) {
|
||||
String name = ((Method) arg).getName().toLowerCase(Locale.US);
|
||||
if (name.contains("moneypacket") || name.contains("mmp")) {
|
||||
return ((Method) arg).getName();
|
||||
}
|
||||
} else if (arg instanceof String) {
|
||||
String text = (String) arg;
|
||||
String lower = text.toLowerCase(Locale.US);
|
||||
if (lower.contains("moneypacket") || lower.contains(".mmp")
|
||||
|| lower.contains("tngdwallet.money")) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String tryFastjson(Object obj) {
|
||||
try {
|
||||
Class<?> json = Class.forName("com.alibaba.fastjson.JSON", false,
|
||||
obj.getClass().getClassLoader());
|
||||
Object out = XposedHelpers.callStaticMethod(json, "toJSONString", obj);
|
||||
return out != null ? String.valueOf(out) : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void forwardParsedJson(String json, String sourceHint, String channel) {
|
||||
@@ -185,9 +285,11 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
XposedBridge.log(TAG + " skip forward: no context, claims="
|
||||
+ snapshot.claims.size());
|
||||
return;
|
||||
}
|
||||
String title = TextUtils.isEmpty(snapshot.title) ? "TNG Money Packet" : snapshot.title;
|
||||
String title = TextUtils.isEmpty(snapshot.title) ? "TNG 红包" : snapshot.title;
|
||||
String content = snapshot.formatForForward(channel, sourceHint);
|
||||
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
|
||||
XposedBridge.log(TAG + " captured packet=" + snapshot.packetId
|
||||
@@ -205,7 +307,11 @@ public final class TngMoneyPacketHook {
|
||||
|
||||
private static MmpSnapshot parseSnapshot(JSONObject root, MmpSnapshot base) {
|
||||
MmpSnapshot snapshot = base != null ? base : new MmpSnapshot();
|
||||
fillMeta(root, snapshot);
|
||||
boolean rootPass = base == null;
|
||||
// 领取行不再回填 meta;其它嵌套节点可补 packetId/sender/total
|
||||
if (rootPass || parseClaimLine(root) == null) {
|
||||
fillMeta(root, snapshot);
|
||||
}
|
||||
|
||||
JSONArray receiverList = findReceiverList(root);
|
||||
if (receiverList != null) {
|
||||
@@ -233,7 +339,7 @@ public final class TngMoneyPacketHook {
|
||||
Object elem = arr.opt(i);
|
||||
if (elem instanceof JSONObject) {
|
||||
ClaimLine line = parseClaimLine((JSONObject) elem);
|
||||
if (line != null && looksLikeClaimRow((JSONObject) elem)) {
|
||||
if (line != null) {
|
||||
snapshot.claims.add(line);
|
||||
}
|
||||
parseSnapshot((JSONObject) elem, snapshot);
|
||||
@@ -241,6 +347,9 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rootPass && TextUtils.isEmpty(snapshot.packetId) && !snapshot.claims.isEmpty()) {
|
||||
snapshot.packetId = "fp" + Integer.toHexString(snapshot.dedupKey().hashCode());
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@@ -249,7 +358,7 @@ public final class TngMoneyPacketHook {
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
Object val = obj.opt(key);
|
||||
if ("receiverList".equalsIgnoreCase(key) && val instanceof JSONArray) {
|
||||
if (val instanceof JSONArray && isReceiverListKey(key)) {
|
||||
return (JSONArray) val;
|
||||
}
|
||||
if (val instanceof JSONObject) {
|
||||
@@ -262,12 +371,32 @@ public final class TngMoneyPacketHook {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isReceiverListKey(String key) {
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
return false;
|
||||
}
|
||||
String k = key.toLowerCase(Locale.US);
|
||||
return "receiverlist".equals(k)
|
||||
|| "leaderboardlist".equals(k)
|
||||
|| "leaderboard".equals(k)
|
||||
|| "rankinglist".equals(k)
|
||||
|| "claimlist".equals(k)
|
||||
|| "claimers".equals(k)
|
||||
|| "packetreceiverlist".equals(k)
|
||||
|| "receivers".equals(k)
|
||||
|| k.contains("receiverlist")
|
||||
|| k.contains("leaderboard");
|
||||
}
|
||||
|
||||
private static void fillMeta(JSONObject obj, MmpSnapshot snapshot) {
|
||||
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "id");
|
||||
// 勿用裸 amount/id:会把领取金额或嵌套 id 误当成红包总额/编号
|
||||
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "packetCode",
|
||||
"fundOrderId", "orderNo", "bizNo", "bizOrderId", "activityId");
|
||||
putIfPresent(obj, snapshot, "groupId", "chatId", "conversationId");
|
||||
putIfPresent(obj, snapshot, "groupName", "chatName", "conversationName");
|
||||
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName");
|
||||
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "amount");
|
||||
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName", "creatorName");
|
||||
putIfPresent(obj, snapshot, "totalAmount", "packetAmount", "packetTotalAmount",
|
||||
"totalPacketAmount", "originalAmount");
|
||||
if (TextUtils.isEmpty(snapshot.title)) {
|
||||
String merchant = firstNonEmpty(
|
||||
obj.optString("merchantName", null),
|
||||
@@ -283,70 +412,142 @@ public final class TngMoneyPacketHook {
|
||||
if (!obj.has(key)) {
|
||||
continue;
|
||||
}
|
||||
String val = obj.optString(key, null);
|
||||
String val = readFieldText(obj, key);
|
||||
if (TextUtils.isEmpty(val) || "null".equalsIgnoreCase(val)) {
|
||||
continue;
|
||||
}
|
||||
if (key.toLowerCase(Locale.US).contains("packet") || key.equals("mmpId") || key.equals("id")) {
|
||||
if (TextUtils.isEmpty(snapshot.packetId)) {
|
||||
String keyLower = key.toLowerCase(Locale.US);
|
||||
if (keyLower.contains("packet") || keyLower.contains("mmp")
|
||||
|| keyLower.contains("order") || keyLower.contains("biz")
|
||||
|| keyLower.contains("activity") || keyLower.equals("packetcode")) {
|
||||
if (TextUtils.isEmpty(snapshot.packetId) && !looksLikeMoneyJson(val)) {
|
||||
snapshot.packetId = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("group")) {
|
||||
if (TextUtils.isEmpty(snapshot.groupId)) {
|
||||
} else if (keyLower.contains("group") || keyLower.contains("chat")
|
||||
|| keyLower.contains("conversation")) {
|
||||
if (keyLower.contains("name")) {
|
||||
if (TextUtils.isEmpty(snapshot.title)) {
|
||||
snapshot.title = val;
|
||||
}
|
||||
} else if (TextUtils.isEmpty(snapshot.groupId)) {
|
||||
snapshot.groupId = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("name") && key.toLowerCase(Locale.US).contains("group")) {
|
||||
snapshot.title = val;
|
||||
} else if (key.toLowerCase(Locale.US).contains("sender")
|
||||
|| key.toLowerCase(Locale.US).contains("operator")) {
|
||||
if (TextUtils.isEmpty(snapshot.senderName)) {
|
||||
} else if (keyLower.contains("sender") || keyLower.contains("operator")
|
||||
|| keyLower.contains("creator")) {
|
||||
if (TextUtils.isEmpty(snapshot.senderName) && !looksLikeMoneyJson(val)) {
|
||||
snapshot.senderName = val;
|
||||
}
|
||||
} else if (key.toLowerCase(Locale.US).contains("total") || key.equals("amount")) {
|
||||
if (TextUtils.isEmpty(snapshot.totalAmount)) {
|
||||
snapshot.totalAmount = val;
|
||||
} else if (keyLower.contains("total") || keyLower.contains("packetamount")
|
||||
|| keyLower.contains("original")) {
|
||||
String money = normalizeMoneyText(val);
|
||||
if (TextUtils.isEmpty(snapshot.totalAmount) && !TextUtils.isEmpty(money)) {
|
||||
snapshot.totalAmount = money;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean looksLikeClaimRow(JSONObject obj) {
|
||||
String name = firstNonEmpty(
|
||||
obj.optString("nickName", null),
|
||||
obj.optString("displayName", null),
|
||||
obj.optString("userName", null),
|
||||
obj.optString("receiverName", null));
|
||||
String amount = firstNonEmpty(
|
||||
obj.optString("claimedAmount", null),
|
||||
obj.optString("receiveAmount", null),
|
||||
obj.optString("amount", null));
|
||||
return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(amount);
|
||||
return parseClaimLine(obj) != null;
|
||||
}
|
||||
|
||||
private static ClaimLine parseClaimLine(JSONObject obj) {
|
||||
String name = firstNonEmpty(
|
||||
obj.optString("nickName", null),
|
||||
obj.optString("displayName", null),
|
||||
obj.optString("userName", null),
|
||||
obj.optString("receiverName", null),
|
||||
obj.optString("name", null));
|
||||
readFieldText(obj, "nickName"),
|
||||
readFieldText(obj, "nickname"),
|
||||
readFieldText(obj, "displayName"),
|
||||
readFieldText(obj, "userName"),
|
||||
readFieldText(obj, "receiverName"),
|
||||
readFieldText(obj, "receiverAccountName"),
|
||||
readFieldText(obj, "accountName"),
|
||||
readFieldText(obj, "participantName"),
|
||||
readFieldText(obj, "name"));
|
||||
String amount = firstNonEmpty(
|
||||
obj.optString("claimedAmount", null),
|
||||
obj.optString("receiveAmount", null),
|
||||
obj.optString("amount", null));
|
||||
normalizeMoneyText(readFieldText(obj, "claimedAmount")),
|
||||
normalizeMoneyText(readFieldText(obj, "receiveAmount")),
|
||||
normalizeMoneyText(readFieldText(obj, "receiverClaimedAmount")),
|
||||
normalizeMoneyText(readFieldText(obj, "claimAmount")),
|
||||
normalizeMoneyText(readFieldText(obj, "amount")));
|
||||
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(amount)) {
|
||||
return null;
|
||||
}
|
||||
if (name.length() > 64 || looksLikeMoneyJson(name)) {
|
||||
return null;
|
||||
}
|
||||
ClaimLine line = new ClaimLine();
|
||||
line.nickname = name;
|
||||
line.nickname = name.trim();
|
||||
line.amount = amount;
|
||||
line.claimTime = firstNonEmpty(
|
||||
obj.optString("claimTime", null),
|
||||
obj.optString("claimedTime", null),
|
||||
obj.optString("receiveTime", null));
|
||||
readFieldText(obj, "claimTime"),
|
||||
readFieldText(obj, "claimedTime"),
|
||||
readFieldText(obj, "receiveTime"),
|
||||
readFieldText(obj, "gmtClaim"),
|
||||
readFieldText(obj, "gmtCreate"),
|
||||
readFieldText(obj, "time"));
|
||||
return line;
|
||||
}
|
||||
|
||||
/** 读取字段:兼容 Money 对象 / 嵌套 JSON / 普通字符串。 */
|
||||
private static String readFieldText(JSONObject obj, String key) {
|
||||
if (obj == null || TextUtils.isEmpty(key) || !obj.has(key)) {
|
||||
return null;
|
||||
}
|
||||
Object raw = obj.opt(key);
|
||||
if (raw == null || raw == JSONObject.NULL) {
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof JSONObject) {
|
||||
return normalizeMoneyText(((JSONObject) raw).toString());
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (TextUtils.isEmpty(text) || "null".equalsIgnoreCase(text)) {
|
||||
return null;
|
||||
}
|
||||
if (looksLikeMoneyJson(text)) {
|
||||
return normalizeMoneyText(text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static boolean looksLikeMoneyJson(String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
String t = text.trim();
|
||||
return t.startsWith("{") && t.contains("\"amount\"");
|
||||
}
|
||||
|
||||
private static String normalizeMoneyText(String text) {
|
||||
if (TextUtils.isEmpty(text) || "null".equalsIgnoreCase(text)) {
|
||||
return null;
|
||||
}
|
||||
String t = text.trim().replace("RM", "").trim();
|
||||
if (looksLikeMoneyJson(t)) {
|
||||
try {
|
||||
JSONObject money = new JSONObject(t);
|
||||
String amount = money.optString("amount", null);
|
||||
if (!TextUtils.isEmpty(amount) && !"null".equalsIgnoreCase(amount)) {
|
||||
return amount.trim();
|
||||
}
|
||||
String cent = money.optString("cent", null);
|
||||
if (!TextUtils.isEmpty(cent) && !"null".equalsIgnoreCase(cent)) {
|
||||
try {
|
||||
return String.format(Locale.US, "%.2f", Integer.parseInt(cent) / 100.0);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// 纯数字或 1.00
|
||||
try {
|
||||
return String.format(Locale.US, "%.2f", Double.parseDouble(t.replace(",", "")));
|
||||
} catch (Throwable ignored) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildJsonFromObject(Object root) {
|
||||
JSONObject json = objectToJson(root, new HashSet<Integer>(), 0);
|
||||
return json != null ? json.toString() : null;
|
||||
@@ -541,23 +742,24 @@ public final class TngMoneyPacketHook {
|
||||
|
||||
String formatForForward(String channel, String sourceHint) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[MMP统计] ");
|
||||
sb.append("[MMP统计]");
|
||||
if (!TextUtils.isEmpty(packetId)) {
|
||||
sb.append("packet=").append(packetId).append(' ');
|
||||
sb.append(" packet=").append(sanitizeMeta(packetId));
|
||||
}
|
||||
if (!TextUtils.isEmpty(groupId)) {
|
||||
sb.append("group=").append(groupId).append(' ');
|
||||
sb.append(" | group=").append(sanitizeMeta(groupId));
|
||||
}
|
||||
if (!TextUtils.isEmpty(senderName)) {
|
||||
sb.append("sender=").append(senderName).append(' ');
|
||||
sb.append(" | sender=").append(sanitizeMeta(senderName));
|
||||
}
|
||||
if (!TextUtils.isEmpty(totalAmount)) {
|
||||
sb.append("total=").append(totalAmount).append(' ');
|
||||
sb.append(" | total=").append(sanitizeMeta(totalAmount));
|
||||
}
|
||||
sb.append("via=").append(channel);
|
||||
sb.append(" | via=").append(sanitizeMeta(channel));
|
||||
if (!TextUtils.isEmpty(sourceHint)) {
|
||||
sb.append(" src=").append(sourceHint.length() > 120
|
||||
? sourceHint.substring(0, 120) + "..." : sourceHint);
|
||||
String src = sourceHint.length() > 120
|
||||
? sourceHint.substring(0, 120) + "..." : sourceHint;
|
||||
sb.append(" | src=").append(sanitizeMeta(src));
|
||||
}
|
||||
sb.append("\n");
|
||||
for (ClaimLine line : claims) {
|
||||
@@ -569,6 +771,14 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
private static String sanitizeMeta(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
// 避免空格拆坏 meta;竖线是分隔符
|
||||
return value.replace('|', '/').trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ClaimLine {
|
||||
|
||||
Reference in New Issue
Block a user