docs(tng): 补充 Money Packet 领取台说明,并同步自动拉详情 Hook
说明 activityId、手动缓存模板后再自动拉历史详情的流程,以及 /mmp 双通道与去重行为。
This commit is contained in:
@@ -37,8 +37,32 @@ public final class TngMoneyPacketHook {
|
||||
|
||||
private static final ArrayDeque<String> RECENT_KEYS = new ArrayDeque<>();
|
||||
private static final HashSet<String> RECENT_SET = new HashSet<>();
|
||||
/** activityId → 上次转发时间 */
|
||||
private static final java.util.HashMap<String, Long> RECENT_PACKET_AT = new java.util.HashMap<>();
|
||||
/** activityId → 领取指纹 */
|
||||
private static final java.util.HashMap<String, String> RECENT_PACKET_CLAIMS = new java.util.HashMap<>();
|
||||
/** 同一 activityId + 相同领取名单,20 秒内不重复推(防双 Hook / 连点);超时可刷新 */
|
||||
private static final long PACKET_DEDUP_MS = 20_000L;
|
||||
private static final ThreadLocal<String> CURRENT_REQUEST_URL = new ThreadLocal<>();
|
||||
|
||||
/** 历史列表触发的自动拉详情:已排队 / 上次成功时间 */
|
||||
private static final java.util.HashSet<String> AUTO_DETAIL_PENDING = new java.util.HashSet<>();
|
||||
private static final java.util.HashMap<String, Long> AUTO_DETAIL_AT = new java.util.HashMap<>();
|
||||
private static final long AUTO_DETAIL_COOLDOWN_MS = 60_000L;
|
||||
private static volatile Object sMoneyPacketRpcTask;
|
||||
private static volatile ClassLoader sAppClassLoader;
|
||||
private static volatile String sCachedUserId;
|
||||
private static volatile String sCachedSessionId;
|
||||
private static volatile String sCachedLoginId;
|
||||
/** 手动打开详情时抓到的真实请求,自动拉详情时克隆它 */
|
||||
private static volatile Object sTemplateDetailRequest;
|
||||
private static final java.util.concurrent.ExecutorService AUTO_DETAIL_EXEC =
|
||||
java.util.concurrent.Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "TngMmp-auto-detail");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private TngMoneyPacketHook() {
|
||||
}
|
||||
|
||||
@@ -46,6 +70,7 @@ public final class TngMoneyPacketHook {
|
||||
if (!TngRootBypassHook.isTargetPackage(lpparam.packageName)) {
|
||||
return;
|
||||
}
|
||||
sAppClassLoader = lpparam.classLoader;
|
||||
hookOkHttpUrl(lpparam);
|
||||
hookResponseBody(lpparam);
|
||||
hookGsonFromJson(lpparam);
|
||||
@@ -184,9 +209,9 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
|
||||
private static void hookQuakeRpc(XC_LoadPackage.LoadPackageParam lpparam) {
|
||||
// 只挂 TNG 封装层,避免与底层 RpcInvocationHandler 双触发导致重复
|
||||
String[] rpcClasses = {
|
||||
"my.com.tngdigital.common.aliservice.quake.TngdRpcInvocationHandlerHost",
|
||||
"com.alipay.imobile.network.quake.rpc.RpcInvocationHandler",
|
||||
};
|
||||
XC_MethodHook hook = new XC_MethodHook() {
|
||||
@Override
|
||||
@@ -194,14 +219,26 @@ public final class TngMoneyPacketHook {
|
||||
if (param.hasThrowable() || param.getResult() == null) {
|
||||
return;
|
||||
}
|
||||
cacheMoneyPacketRpcTask(param.args);
|
||||
cacheLoginFromInvokeArgs(param.args);
|
||||
String op = extractMoneyPacketRpcOp(param.args);
|
||||
if (op == null) {
|
||||
return;
|
||||
}
|
||||
Object result = param.getResult();
|
||||
// 历史列表:无领取排行,但可用 activityId 自动拉详情
|
||||
if (isHistoryRpcOp(op)) {
|
||||
XposedBridge.log(TAG + " history hit → schedule auto detail");
|
||||
scheduleAutoDetailsFromHistory(result);
|
||||
return;
|
||||
}
|
||||
if (!isDetailRpcOp(op)) {
|
||||
return;
|
||||
}
|
||||
String className = result.getClass().getName();
|
||||
XposedBridge.log(TAG + " rpc hit op=" + op + " type=" + className);
|
||||
try {
|
||||
Object detailReq = extractRpcRequestArg(param.args);
|
||||
String json = buildJsonFromObject(result);
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
json = tryFastjson(result);
|
||||
@@ -210,9 +247,20 @@ public final class TngMoneyPacketHook {
|
||||
XposedBridge.log(TAG + " rpc serialize empty op=" + op);
|
||||
return;
|
||||
}
|
||||
forwardParsedJson(json, op, "rpc");
|
||||
MmpSnapshot probe = parseSnapshot(json);
|
||||
if (probe == null || probe.claims.isEmpty()) {
|
||||
if (probe != null && !probe.claims.isEmpty()) {
|
||||
if (detailReq != null) {
|
||||
sTemplateDetailRequest = cloneRequestShallow(detailReq);
|
||||
cacheLoginFromRequestObject(detailReq);
|
||||
XposedBridge.log(TAG + " cached detail request template"
|
||||
+ " userId=" + stringField(detailReq, "userId", "getUserId")
|
||||
+ " session=" + abbreviate(stringField(detailReq,
|
||||
"sessionId", "getSessionId"))
|
||||
+ " page=" + stringField(detailReq, "page", "getPage")
|
||||
+ " max=" + stringField(detailReq, "maxResult", "getMaxResult"));
|
||||
}
|
||||
forwardParsedJson(json, op, "rpc");
|
||||
} else {
|
||||
String snippet = json.length() > 500 ? json.substring(0, 500) + "..." : json;
|
||||
XposedBridge.log(TAG + " rpc no-claims op=" + op + " body=" + snippet);
|
||||
}
|
||||
@@ -241,6 +289,462 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHistoryRpcOp(String op) {
|
||||
if (TextUtils.isEmpty(op)) {
|
||||
return false;
|
||||
}
|
||||
String lower = op.toLowerCase(Locale.US);
|
||||
return lower.contains("historylist")
|
||||
|| lower.contains("history")
|
||||
|| lower.contains("list.retrieve")
|
||||
|| lower.equals("moneypackethistorylist");
|
||||
}
|
||||
|
||||
/** 只要详情/领取查询,历史列表走 auto-detail */
|
||||
private static boolean isDetailRpcOp(String op) {
|
||||
if (TextUtils.isEmpty(op) || isHistoryRpcOp(op)) {
|
||||
return false;
|
||||
}
|
||||
String lower = op.toLowerCase(Locale.US);
|
||||
return lower.contains("detail")
|
||||
|| lower.contains("retrieve")
|
||||
|| lower.contains("claim.result")
|
||||
|| lower.contains("leaderboard")
|
||||
|| lower.contains("moneypacketdetail");
|
||||
}
|
||||
|
||||
/** InvocationHandler.invoke(proxy, method, args) → 缓存 MoneyPacketRpcTask 代理 */
|
||||
private static void cacheMoneyPacketRpcTask(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object proxy = args[0];
|
||||
if (proxy == null || !java.lang.reflect.Proxy.isProxyClass(proxy.getClass())) {
|
||||
return;
|
||||
}
|
||||
if (sMoneyPacketRpcTask != null) {
|
||||
return;
|
||||
}
|
||||
for (Class<?> iface : proxy.getClass().getInterfaces()) {
|
||||
String name = iface.getName();
|
||||
if (name.endsWith("MoneyPacketRpcTask") || name.contains("MoneyPacketRpc")) {
|
||||
sMoneyPacketRpcTask = proxy;
|
||||
XposedBridge.log(TAG + " cached RPC task " + name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
proxy.getClass().getMethod("moneyPacketDetail",
|
||||
XposedHelpers.findClass(
|
||||
"my.com.tngdigital.funding.moneypacket.common.data.rpc.MoneyPacketDetailRequest",
|
||||
sAppClassLoader));
|
||||
sMoneyPacketRpcTask = proxy;
|
||||
XposedBridge.log(TAG + " cached RPC task via moneyPacketDetail method");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 从历史/详情请求里偷登录态,供自动拉详情复用 */
|
||||
private static void cacheLoginFromInvokeArgs(Object[] args) {
|
||||
cacheLoginFromRequestObject(extractRpcRequestArg(args));
|
||||
}
|
||||
|
||||
private static void cacheLoginFromRequestObject(Object req) {
|
||||
if (req == null) {
|
||||
return;
|
||||
}
|
||||
String userId = stringField(req, "userId", "getUserId");
|
||||
String sessionId = stringField(req, "sessionId", "getSessionId");
|
||||
String loginId = stringField(req, "loginId", "getLoginId");
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
sCachedUserId = userId;
|
||||
}
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
sCachedSessionId = sessionId;
|
||||
}
|
||||
if (!TextUtils.isEmpty(loginId)) {
|
||||
sCachedLoginId = loginId;
|
||||
}
|
||||
}
|
||||
|
||||
private static String abbreviate(String s) {
|
||||
if (TextUtils.isEmpty(s)) {
|
||||
return "-";
|
||||
}
|
||||
return s.length() <= 8 ? s : s.substring(0, 4) + "…" + s.substring(s.length() - 4);
|
||||
}
|
||||
|
||||
private static Object cloneRequestShallow(Object src) {
|
||||
if (src == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object copy = src.getClass().getDeclaredConstructor().newInstance();
|
||||
copyAllFields(src, copy);
|
||||
return copy;
|
||||
} catch (Throwable t) {
|
||||
// Kotlin data class 可能无无参构造,直接复用引用(自动拉取在单线程)
|
||||
return src;
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyAllFields(Object src, Object dst) {
|
||||
Class<?> c = src.getClass();
|
||||
while (c != null && c != Object.class) {
|
||||
for (java.lang.reflect.Field f : c.getDeclaredFields()) {
|
||||
if (f.getName().contains("$")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
f.set(dst, f.get(src));
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyActivityToRequest(Object req, String activityId, String senderUserId) {
|
||||
try {
|
||||
XposedHelpers.setObjectField(req, "activityId", activityId);
|
||||
} catch (Throwable ignored) {
|
||||
trySet(req, "setActivityId", activityId);
|
||||
}
|
||||
try {
|
||||
XposedHelpers.setObjectField(req, "senderUserId", senderUserId);
|
||||
} catch (Throwable ignored) {
|
||||
trySet(req, "setSenderUserId", senderUserId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Object extractRpcRequestArg(Object[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
// InvocationHandler.invoke(proxy, method, Object[] methodArgs)
|
||||
if (args.length >= 3 && args[2] instanceof Object[]) {
|
||||
Object[] methodArgs = (Object[]) args[2];
|
||||
if (methodArgs.length > 0 && methodArgs[0] != null) {
|
||||
String n = methodArgs[0].getClass().getName();
|
||||
if (n.contains("Request") || n.contains("moneypacket") || n.contains("MoneyPacket")) {
|
||||
return methodArgs[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if (arg == null || arg instanceof Method || arg instanceof String) {
|
||||
continue;
|
||||
}
|
||||
if (arg instanceof Object[]) {
|
||||
continue;
|
||||
}
|
||||
String n = arg.getClass().getName();
|
||||
if (n.contains("Request") && (n.contains("MoneyPacket") || n.contains("moneypacket")
|
||||
|| n.contains("Mmp"))) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void scheduleAutoDetailsFromHistory(Object historyResult) {
|
||||
if (historyResult == null) {
|
||||
return;
|
||||
}
|
||||
AUTO_DETAIL_EXEC.execute(() -> {
|
||||
try {
|
||||
List<String[]> jobs = extractHistoryJobs(historyResult);
|
||||
XposedBridge.log(TAG + " auto-detail jobs=" + jobs.size());
|
||||
for (String[] job : jobs) {
|
||||
final String activityId = job[0];
|
||||
final String senderUserId = job[1];
|
||||
long now = System.currentTimeMillis();
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
Long last = AUTO_DETAIL_AT.get(activityId);
|
||||
if (last != null && now - last < AUTO_DETAIL_COOLDOWN_MS) {
|
||||
continue;
|
||||
}
|
||||
if (!AUTO_DETAIL_PENDING.add(activityId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
boolean ok = fetchDetailByActivityId(activityId, senderUserId);
|
||||
if (ok) {
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
AUTO_DETAIL_AT.put(activityId, System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
synchronized (AUTO_DETAIL_AT) {
|
||||
AUTO_DETAIL_PENDING.remove(activityId);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(400);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " auto-detail schedule failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String[]> extractHistoryJobs(Object historyResult) {
|
||||
List<String[]> jobs = new ArrayList<>();
|
||||
try {
|
||||
Object infos = null;
|
||||
try {
|
||||
infos = XposedHelpers.callMethod(historyResult, "getParticipantInfos");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (infos == null) {
|
||||
try {
|
||||
infos = XposedHelpers.getObjectField(historyResult, "participantInfos");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (!(infos instanceof Iterable)) {
|
||||
return jobs;
|
||||
}
|
||||
for (Object info : (Iterable<?>) infos) {
|
||||
if (info == null) {
|
||||
continue;
|
||||
}
|
||||
String activityId = stringField(info, "activityId", "getActivityId");
|
||||
String senderUserId = stringField(info, "senderUserId", "getSenderUserId");
|
||||
if (looksLikeActivityId(activityId) && !TextUtils.isEmpty(senderUserId)) {
|
||||
jobs.add(new String[]{activityId, senderUserId});
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " extract history jobs failed: " + t.getMessage());
|
||||
}
|
||||
return jobs;
|
||||
}
|
||||
|
||||
private static String stringField(Object obj, String field, String getter) {
|
||||
try {
|
||||
Object v = XposedHelpers.callMethod(obj, getter);
|
||||
if (v != null) {
|
||||
return String.valueOf(v);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
Object v = XposedHelpers.getObjectField(obj, field);
|
||||
if (v != null) {
|
||||
return String.valueOf(v);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean fetchDetailByActivityId(String activityId, String senderUserId) {
|
||||
ClassLoader cl = sAppClassLoader;
|
||||
Object task = sMoneyPacketRpcTask;
|
||||
if (cl == null || task == null) {
|
||||
XposedBridge.log(TAG + " auto-detail skip: no rpc task yet, id=" + activityId);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Class<?> reqCls = XposedHelpers.findClass(
|
||||
"my.com.tngdigital.funding.moneypacket.common.data.rpc.MoneyPacketDetailRequest",
|
||||
cl);
|
||||
Object req;
|
||||
if (sTemplateDetailRequest != null
|
||||
&& reqCls.isInstance(sTemplateDetailRequest)) {
|
||||
req = cloneRequestShallow(sTemplateDetailRequest);
|
||||
applyActivityToRequest(req, activityId, senderUserId);
|
||||
XposedBridge.log(TAG + " auto-detail using template request id=" + activityId);
|
||||
} else {
|
||||
req = newDetailRequest(reqCls, activityId, senderUserId);
|
||||
if (req == null) {
|
||||
XposedBridge.log(TAG + " auto-detail newRequest failed id=" + activityId);
|
||||
return false;
|
||||
}
|
||||
fillLoginOnRequest(req, cl);
|
||||
}
|
||||
cacheLoginFromRequestObject(req);
|
||||
XposedBridge.log(TAG + " auto-detail req id=" + activityId
|
||||
+ " userId=" + stringField(req, "userId", "getUserId")
|
||||
+ " session=" + abbreviate(stringField(req, "sessionId", "getSessionId"))
|
||||
+ " loginId=" + abbreviate(stringField(req, "loginId", "getLoginId"))
|
||||
+ " page=" + stringField(req, "page", "getPage")
|
||||
+ " max=" + stringField(req, "maxResult", "getMaxResult"));
|
||||
if (TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))
|
||||
&& TextUtils.isEmpty(sCachedSessionId)) {
|
||||
XposedBridge.log(TAG + " auto-detail skip: no sessionId id=" + activityId
|
||||
+ " (先手动点开任意一个红包详情一次以缓存模板)");
|
||||
return false;
|
||||
}
|
||||
if (TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))
|
||||
&& !TextUtils.isEmpty(sCachedSessionId)) {
|
||||
fillLoginOnRequest(req, cl);
|
||||
}
|
||||
Object result = XposedHelpers.callMethod(task, "moneyPacketDetail", req);
|
||||
if (result == null) {
|
||||
XposedBridge.log(TAG + " auto-detail null result id=" + activityId);
|
||||
return false;
|
||||
}
|
||||
String json = buildJsonFromObject(result);
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
json = tryFastjson(result);
|
||||
}
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
XposedBridge.log(TAG + " auto-detail empty json id=" + activityId);
|
||||
return false;
|
||||
}
|
||||
MmpSnapshot probe = parseSnapshot(json);
|
||||
if (probe == null || probe.claims.isEmpty()) {
|
||||
String snippet = json.length() > 240 ? json.substring(0, 240) + "..." : json;
|
||||
XposedBridge.log(TAG + " auto-detail empty claims id=" + activityId
|
||||
+ " body=" + snippet
|
||||
+ " userId=" + stringField(req, "userId", "getUserId"));
|
||||
return false;
|
||||
}
|
||||
forwardParsedJson(json, "auto:" + activityId, "rpc");
|
||||
XposedBridge.log(TAG + " auto-detail ok activityId=" + activityId
|
||||
+ " claims=" + probe.claims.size());
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " auto-detail fail id=" + activityId + ": " + t.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Object newDetailRequest(Class<?> reqCls, String activityId, String senderUserId) {
|
||||
// 优先: (activityId, senderUserId, loadTime, page, maxResult)
|
||||
try {
|
||||
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 20);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
return XposedHelpers.newInstance(reqCls, activityId, senderUserId, null, 0, 20,
|
||||
31, null);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
Object req = reqCls.getDeclaredConstructor().newInstance();
|
||||
XposedHelpers.setObjectField(req, "activityId", activityId);
|
||||
XposedHelpers.setObjectField(req, "senderUserId", senderUserId);
|
||||
try {
|
||||
XposedHelpers.setIntField(req, "page", 0);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
XposedHelpers.setIntField(req, "maxResult", 20);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return req;
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " newDetailRequest error: " + t.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillLoginOnRequest(Object req, ClassLoader cl) {
|
||||
// 1) 优先用历史请求里缓存的登录态
|
||||
trySet(req, "setUserId", sCachedUserId);
|
||||
trySet(req, "setSessionId", sCachedSessionId);
|
||||
trySet(req, "setLoginId", sCachedLoginId);
|
||||
try {
|
||||
if (!TextUtils.isEmpty(sCachedUserId)) {
|
||||
XposedHelpers.setObjectField(req, "userId", sCachedUserId);
|
||||
}
|
||||
if (!TextUtils.isEmpty(sCachedSessionId)) {
|
||||
XposedHelpers.setObjectField(req, "sessionId", sCachedSessionId);
|
||||
}
|
||||
if (!TextUtils.isEmpty(sCachedLoginId)) {
|
||||
XposedHelpers.setObjectField(req, "loginId", sCachedLoginId);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (!TextUtils.isEmpty(stringField(req, "userId", "getUserId"))
|
||||
&& !TextUtils.isEmpty(stringField(req, "sessionId", "getSessionId"))) {
|
||||
return;
|
||||
}
|
||||
// 2) Hilt EntryPoint 兜底
|
||||
try {
|
||||
Object repo = obtainMoneyPacketRepository(cl);
|
||||
if (repo == null) {
|
||||
return;
|
||||
}
|
||||
Object loginStorage = null;
|
||||
try {
|
||||
loginStorage = XposedHelpers.getObjectField(repo, "loginStorage");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (loginStorage == null) {
|
||||
return;
|
||||
}
|
||||
Object accountId = XposedHelpers.callMethod(loginStorage, "getAccountId");
|
||||
Object sessionId = XposedHelpers.callMethod(loginStorage, "getSessionId");
|
||||
Object loginId = XposedHelpers.callMethod(loginStorage, "getLoginId");
|
||||
trySet(req, "setUserId", accountId);
|
||||
trySet(req, "setSessionId", sessionId);
|
||||
trySet(req, "setLoginId", loginId);
|
||||
if (accountId != null) {
|
||||
sCachedUserId = String.valueOf(accountId);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
sCachedSessionId = String.valueOf(sessionId);
|
||||
}
|
||||
if (loginId != null) {
|
||||
sCachedLoginId = String.valueOf(loginId);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " fillLogin skip: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void trySet(Object req, String setter, Object value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
XposedHelpers.callMethod(req, setter, value);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static Object obtainMoneyPacketRepository(ClassLoader cl) {
|
||||
try {
|
||||
Context ctx = getContext();
|
||||
if (ctx == null) {
|
||||
return null;
|
||||
}
|
||||
Class<?> entryPoint = Class.forName(
|
||||
"my.com.tngdigital.funding.moneypacket.common.di.MoneyPacketEntryPoint",
|
||||
false, cl);
|
||||
Class<?> entryPoints = Class.forName("dagger.hilt.EntryPoints", false, cl);
|
||||
Object ep = XposedHelpers.callStaticMethod(
|
||||
entryPoints, "get", ctx.getApplicationContext(), entryPoint);
|
||||
try {
|
||||
return XposedHelpers.callMethod(ep, "repository");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
for (Method m : ep.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length == 0
|
||||
&& m.getReturnType().getName().contains("MoneyPacketRepository")) {
|
||||
return m.invoke(ep);
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
XposedBridge.log(TAG + " obtain repo fail: " + t.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String extractMoneyPacketRpcOp(Object[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
@@ -279,8 +783,7 @@ public final class TngMoneyPacketHook {
|
||||
if (snapshot == null || snapshot.claims.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String dedupKey = snapshot.dedupKey();
|
||||
if (!remember(dedupKey)) {
|
||||
if (!shouldForwardPacket(snapshot)) {
|
||||
return;
|
||||
}
|
||||
Context context = getContext();
|
||||
@@ -290,9 +793,12 @@ public final class TngMoneyPacketHook {
|
||||
return;
|
||||
}
|
||||
String title = TextUtils.isEmpty(snapshot.title) ? "TNG 红包" : snapshot.title;
|
||||
if (!TextUtils.isEmpty(snapshot.packetId) && snapshot.packetId.length() >= 8) {
|
||||
title = title + " · " + snapshot.packetId.substring(0, 8);
|
||||
}
|
||||
String content = snapshot.formatForForward(channel, sourceHint);
|
||||
HookForwarder.forward(context, PACKAGE, title, content, HookBridge.SOURCE_XPOSED_TNG_MMP);
|
||||
XposedBridge.log(TAG + " captured packet=" + snapshot.packetId
|
||||
XposedBridge.log(TAG + " captured activityId=" + snapshot.packetId
|
||||
+ " claims=" + snapshot.claims.size() + " via " + channel);
|
||||
}
|
||||
|
||||
@@ -384,14 +890,16 @@ public final class TngMoneyPacketHook {
|
||||
|| "claimers".equals(k)
|
||||
|| "packetreceiverlist".equals(k)
|
||||
|| "receivers".equals(k)
|
||||
|| "activitypoolinfos".equals(k)
|
||||
|| k.contains("receiverlist")
|
||||
|| k.contains("leaderboard");
|
||||
|| k.contains("leaderboard")
|
||||
|| k.contains("activitypool");
|
||||
}
|
||||
|
||||
private static void fillMeta(JSONObject obj, MmpSnapshot snapshot) {
|
||||
// 勿用裸 amount/id:会把领取金额或嵌套 id 误当成红包总额/编号
|
||||
putIfPresent(obj, snapshot, "packetId", "mmpId", "moneyPacketId", "packetCode",
|
||||
"fundOrderId", "orderNo", "bizNo", "bizOrderId", "activityId");
|
||||
// activityId 是红包唯一标识(UUID),优先于其它 id 字段
|
||||
putIfPresent(obj, snapshot, "activityId", "packetId", "mmpId", "moneyPacketId",
|
||||
"packetCode", "fundOrderId", "orderNo", "bizNo", "bizOrderId");
|
||||
putIfPresent(obj, snapshot, "groupId", "chatId", "conversationId");
|
||||
putIfPresent(obj, snapshot, "groupName", "chatName", "conversationName");
|
||||
putIfPresent(obj, snapshot, "senderName", "senderNickName", "operatorName", "creatorName");
|
||||
@@ -417,11 +925,16 @@ public final class TngMoneyPacketHook {
|
||||
continue;
|
||||
}
|
||||
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 (keyLower.equals("activityid") || keyLower.contains("packet")
|
||||
|| keyLower.contains("mmp") || keyLower.contains("order")
|
||||
|| keyLower.contains("biz") || keyLower.equals("packetcode")) {
|
||||
// 已有 UUID 形态的 activityId 时不覆盖
|
||||
if (TextUtils.isEmpty(snapshot.packetId) && !looksLikeMoneyJson(val)) {
|
||||
snapshot.packetId = val;
|
||||
} else if (!TextUtils.isEmpty(snapshot.packetId)
|
||||
&& keyLower.equals("activityid")
|
||||
&& looksLikeActivityId(val)) {
|
||||
snapshot.packetId = val;
|
||||
}
|
||||
} else if (keyLower.contains("group") || keyLower.contains("chat")
|
||||
|| keyLower.contains("conversation")) {
|
||||
@@ -447,6 +960,14 @@ public final class TngMoneyPacketHook {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean looksLikeActivityId(String val) {
|
||||
if (TextUtils.isEmpty(val)) {
|
||||
return false;
|
||||
}
|
||||
// b8c37a58-bb2c-4aa2-83ef-f15466b9211e
|
||||
return val.length() >= 32 && val.indexOf('-') > 0;
|
||||
}
|
||||
|
||||
private static boolean looksLikeClaimRow(JSONObject obj) {
|
||||
return parseClaimLine(obj) != null;
|
||||
}
|
||||
@@ -683,6 +1204,43 @@ public final class TngMoneyPacketHook {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean shouldForwardPacket(MmpSnapshot snapshot) {
|
||||
String packetId = snapshot.packetId;
|
||||
if (TextUtils.isEmpty(packetId)) {
|
||||
packetId = "fp" + Integer.toHexString(snapshot.dedupKey().hashCode());
|
||||
snapshot.packetId = packetId;
|
||||
}
|
||||
String claimsFp = snapshot.claimsFingerprint();
|
||||
long now = System.currentTimeMillis();
|
||||
synchronized (RECENT_PACKET_AT) {
|
||||
Long lastAt = RECENT_PACKET_AT.get(packetId);
|
||||
String lastClaims = RECENT_PACKET_CLAIMS.get(packetId);
|
||||
if (lastAt != null && lastClaims != null
|
||||
&& claimsFp.equals(lastClaims)
|
||||
&& now - lastAt < PACKET_DEDUP_MS) {
|
||||
XposedBridge.log(TAG + " skip dup activityId=" + packetId
|
||||
+ " ageMs=" + (now - lastAt));
|
||||
return false;
|
||||
}
|
||||
RECENT_PACKET_AT.put(packetId, now);
|
||||
RECENT_PACKET_CLAIMS.put(packetId, claimsFp);
|
||||
// 顺带清理过期项,防止无限涨
|
||||
if (RECENT_PACKET_AT.size() > DEDUP_SIZE) {
|
||||
java.util.Iterator<java.util.Map.Entry<String, Long>> it =
|
||||
RECENT_PACKET_AT.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
java.util.Map.Entry<String, Long> e = it.next();
|
||||
if (now - e.getValue() > PACKET_DEDUP_MS * 3) {
|
||||
String k = e.getKey();
|
||||
it.remove();
|
||||
RECENT_PACKET_CLAIMS.remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean remember(String key) {
|
||||
if (TextUtils.isEmpty(key)) {
|
||||
return false;
|
||||
@@ -734,6 +1292,12 @@ public final class TngMoneyPacketHook {
|
||||
String dedupKey() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(TextUtils.isEmpty(packetId) ? "?" : packetId).append('|');
|
||||
sb.append(claimsFingerprint());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String claimsFingerprint() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (ClaimLine line : claims) {
|
||||
sb.append(line.nickname).append('=').append(line.amount).append(';');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user