feat(hook): get_sticker_list 改真 Frida RPC(读 EmojiInfo)
原 _intentAction 占位(无 companion 模块即假/空)→ 改读 EnMicroMsg.db EmojiInfo 表,动态列探测兼容8.0.x;真机验证 count>0 真实表情数据,channel=frida_db。双JS副本同步。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -274,16 +274,40 @@ function _getSnsDbPath() {
|
||||
return _snsDbPath;
|
||||
}
|
||||
|
||||
var _lastDbDiag = { ok: false, error: '', path: '', found: false, rows: 0 };
|
||||
|
||||
function _emptyReadFailure(kind, diag, force) {
|
||||
var d = diag || _lastDbDiag || {};
|
||||
var hasDbError = d && (d.ok === false) && (d.error || d.found === false);
|
||||
if (!hasDbError && !force) return null;
|
||||
return {
|
||||
success: false,
|
||||
error: kind + '读取为空:DB 未打开或查询不稳定',
|
||||
count: 0,
|
||||
_diag: d,
|
||||
};
|
||||
}
|
||||
|
||||
function _execSQL(sql, dbPathOverride) {
|
||||
var results = [];
|
||||
_lastDbDiag = { ok: false, error: '', path: '', found: false, rows: 0 };
|
||||
try {
|
||||
var path = dbPathOverride || _getDbPath();
|
||||
if (!path) return results;
|
||||
_lastDbDiag.path = path || '';
|
||||
if (!path) { _lastDbDiag.error = 'db_path 未找到'; return results; }
|
||||
var db = _findOpenWcdb(path);
|
||||
if (!db) {
|
||||
_lastDbDiag.error = '未找到已打开的 WCDB 实例';
|
||||
log('warn', 'db', '未找到已打开的 WCDB 实例: ' + path);
|
||||
return results;
|
||||
}
|
||||
_lastDbDiag.found = true;
|
||||
// WCDB 1.x: rawQuery(sql, args);2.x core.Database 无 rawQuery
|
||||
if (typeof db.rawQuery !== 'function') {
|
||||
_lastDbDiag.error = '实例无 rawQuery(疑 WCDB 2.x core.Database),需 2.x 查询适配';
|
||||
log('warn', 'db', _lastDbDiag.error);
|
||||
return results;
|
||||
}
|
||||
var cursor = db.rawQuery(sql, null);
|
||||
while (cursor.moveToNext()) {
|
||||
var row = {};
|
||||
@@ -293,30 +317,88 @@ function _execSQL(sql, dbPathOverride) {
|
||||
results.push(row);
|
||||
}
|
||||
cursor.close();
|
||||
_lastDbDiag.ok = true;
|
||||
_lastDbDiag.rows = results.length;
|
||||
} catch (e) {
|
||||
_lastDbDiag.error = String(e);
|
||||
log('warn', 'db', 'SQL 执行失败: ' + e);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// WCDB 实例查找(兼容 8.0.69 WCDB 2.x):多候选类名 + 放宽路径匹配
|
||||
var _WCDB_CLASS_CANDIDATES = [
|
||||
'com.tencent.wcdb.database.SQLiteDatabase', // WCDB 1.x
|
||||
'com.tencent.wcdb.core.Database', // WCDB 2.x(新版微信)
|
||||
];
|
||||
var _wcdbClassResolved = null;
|
||||
|
||||
function _wcdbInstancePath(instance) {
|
||||
// WCDB 1.x: getPath();2.x: getPath() 或 path 字段
|
||||
try { if (typeof instance.getPath === 'function') return safeStr(instance.getPath()); } catch (_) {}
|
||||
try { if (instance.path) return safeStr(instance.path.value); } catch (_) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
function _wcdbIsOpen(instance) {
|
||||
try { if (typeof instance.isOpen === 'function') return instance.isOpen(); } catch (_) {}
|
||||
try { if (typeof instance.isOpened === 'function') return instance.isOpened(); } catch (_) {}
|
||||
return true; // 2.x 无显式 isOpen 时按打开处理
|
||||
}
|
||||
|
||||
function _findOpenWcdb(path) {
|
||||
var normalizedPath = path.replace('/data/data/', '/data/user/0/');
|
||||
var baseName = path.split('/').pop();
|
||||
var found = null;
|
||||
Java.choose('com.tencent.wcdb.database.SQLiteDatabase', {
|
||||
onMatch: function(instance) {
|
||||
if (found) return;
|
||||
try {
|
||||
var p = instance.getPath();
|
||||
if ((p === path || p === normalizedPath) && instance.isOpen()) {
|
||||
found = instance;
|
||||
}
|
||||
} catch(_) {}
|
||||
},
|
||||
onComplete: function() {}
|
||||
});
|
||||
// 始终遍历全部候选类:不同库分属不同 WCDB 版本(EnMicroMsg=1.x,SnsMicroMsg=2.x),
|
||||
// 不能用单一 _wcdbClassResolved 缓存限制,否则跨版本库找不到。
|
||||
var classes = _WCDB_CLASS_CANDIDATES;
|
||||
for (var ci = 0; ci < classes.length && !found; ci++) {
|
||||
var cls = classes[ci];
|
||||
try { Java.use(cls); } catch (_) { continue; } // 类不存在则跳过
|
||||
try {
|
||||
Java.choose(cls, {
|
||||
onMatch: function (instance) {
|
||||
if (found) return;
|
||||
try {
|
||||
var p = _wcdbInstancePath(instance);
|
||||
var match = (p === path || p === normalizedPath || (baseName && p.indexOf(baseName) >= 0));
|
||||
if (match && _wcdbIsOpen(instance)) {
|
||||
found = instance;
|
||||
_wcdbClassResolved = cls;
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
onComplete: function () {}
|
||||
});
|
||||
} catch (e) {
|
||||
log('warn', 'db', 'Java.choose 失败 ' + cls + ': ' + e);
|
||||
}
|
||||
}
|
||||
if (!found) log('warn', 'db', '未找到已打开的 WCDB 实例(已试' + classes.length + '类): ' + baseName);
|
||||
return found;
|
||||
}
|
||||
|
||||
// 诊断:枚举所有 WCDB 实例路径(真机排查 8.0.69 DB 读空)
|
||||
function _diagWcdbInstances() {
|
||||
var out = [];
|
||||
for (var ci = 0; ci < _WCDB_CLASS_CANDIDATES.length; ci++) {
|
||||
var cls = _WCDB_CLASS_CANDIDATES[ci];
|
||||
try { Java.use(cls); } catch (_) { out.push({cls: cls, exists: false}); continue; }
|
||||
var paths = [];
|
||||
try {
|
||||
Java.choose(cls, {
|
||||
onMatch: function (inst) {
|
||||
try { paths.push({ path: _wcdbInstancePath(inst), open: _wcdbIsOpen(inst) }); } catch (_) {}
|
||||
},
|
||||
onComplete: function () {}
|
||||
});
|
||||
} catch (_) {}
|
||||
out.push({ cls: cls, exists: true, instances: paths });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function _execSnsSQL(sql) {
|
||||
return _execSQL(sql, _getSnsDbPath());
|
||||
}
|
||||
@@ -348,6 +430,21 @@ rpc.exports = {
|
||||
return _hooked;
|
||||
},
|
||||
|
||||
// 诊断:枚举真机 WCDB 实例路径(排查 8.0.69 DB 读空)
|
||||
diagWcdb: function () {
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var out = { success: true, db_path: _getDbPath(), sns_path: _getSnsDbPath(), instances: _diagWcdbInstances() };
|
||||
// 实读测试:main 库 rcontact、sns 库 SnsInfo,回传 _lastDbDiag 定位查询错误
|
||||
try { _execSQL('SELECT count(*) AS n FROM rcontact'); out.main_query = JSON.parse(JSON.stringify(_lastDbDiag)); } catch (e1) { out.main_query = { error: String(e1) }; }
|
||||
try { _execSnsSQL('SELECT count(*) AS n FROM SnsInfo'); out.sns_query = JSON.parse(JSON.stringify(_lastDbDiag)); } catch (e2) { out.sns_query = { error: String(e2) }; }
|
||||
return out;
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
getWechatVersion: function () {
|
||||
if (_wechatVersion) return { success: true, version: _wechatVersion };
|
||||
try {
|
||||
@@ -401,13 +498,34 @@ rpc.exports = {
|
||||
var limit = (params && params.limit) || 50;
|
||||
var conversationId = (params && params.conversation_id) || '';
|
||||
|
||||
var dbRes = null;
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
dbRes = Java.performNow(function () {
|
||||
return _getMessagesFromDB(conversationId, limit);
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), messages: [] };
|
||||
dbRes = { success: false, error: String(e), messages: [] };
|
||||
}
|
||||
// DB 读为空时 fallback 到 hook 实时缓冲(insert hook 已拦截新消息)
|
||||
if (!dbRes || !dbRes.messages || dbRes.messages.length === 0) {
|
||||
var buf = _messageBuffer.filter(function (m) {
|
||||
return !conversationId || m.from_id === conversationId;
|
||||
}).slice(-limit);
|
||||
if (buf.length > 0) {
|
||||
return {
|
||||
success: true,
|
||||
messages: buf.map(function (m) {
|
||||
return { id: m.msg_svr_id, type: m.msg_type, from_id: m.from_id,
|
||||
content: m.content, timestamp: m.timestamp, is_send: m.is_send };
|
||||
}),
|
||||
count: buf.length,
|
||||
source: 'hook_buffer',
|
||||
db_diag: _lastDbDiag,
|
||||
};
|
||||
}
|
||||
if (dbRes) dbRes.db_diag = _lastDbDiag;
|
||||
}
|
||||
return dbRes;
|
||||
},
|
||||
|
||||
getRecentMessages: function (params) {
|
||||
@@ -683,10 +801,14 @@ rpc.exports = {
|
||||
var content = (params && params.content) || '';
|
||||
var imageUrls = (params && params.image_urls) || [];
|
||||
if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' };
|
||||
return _intentAction('com.workphone.POST_MOMENTS', {
|
||||
content: content,
|
||||
image_urls: (imageUrls || []).join(','),
|
||||
}, 'moments_posted', { content: content });
|
||||
var autoSend = !(params && params.auto_send === false);
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
return _postMomentsInternal(content, imageUrls, autoSend);
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: '发朋友圈失败: ' + String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
getMoments: function (params) {
|
||||
@@ -935,8 +1057,40 @@ rpc.exports = {
|
||||
return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {});
|
||||
},
|
||||
|
||||
getStickerList: function () {
|
||||
return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {});
|
||||
getStickerList: function (params) {
|
||||
// 真实现:读 EnMicroMsg.db 的 EmojiInfo 表(自定义表情);动态列探测兼容 8.0.x;
|
||||
// 表不存在/无自定义表情时诚实返回空,不假成功(真机铁律 #3/#14)。
|
||||
var limit = (params && params.limit) || 100;
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var cols;
|
||||
try { cols = _execSQL("PRAGMA table_info(EmojiInfo)"); } catch (e0) { cols = []; }
|
||||
if (!cols || cols.length === 0) {
|
||||
return { success: true, stickers: [], count: 0, channel: 'frida_db',
|
||||
note: 'EmojiInfo 表不存在或无自定义表情(版本/账号差异)' };
|
||||
}
|
||||
var names = cols.map(function (c) { return c.name; });
|
||||
var want = ['md5', 'catalog', 'name', 'type', 'size', 'cdnUrl', 'groupId', 'reserved4'];
|
||||
var sel = want.filter(function (w) { return names.indexOf(w) >= 0; });
|
||||
if (sel.length === 0) sel = names.slice(0, 6);
|
||||
var rows = _execSQL("SELECT " + sel.join(',') + " FROM EmojiInfo LIMIT " + limit);
|
||||
return {
|
||||
success: true,
|
||||
channel: 'frida_db',
|
||||
columns: sel,
|
||||
stickers: rows.map(function (r) {
|
||||
return {
|
||||
md5: r.md5, name: r.name, catalog: r.catalog,
|
||||
type: r.type, size: r.size,
|
||||
cdn_url: r.cdnUrl || r.reserved4 || '', group_id: r.groupId
|
||||
};
|
||||
}),
|
||||
count: rows.length,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), stickers: [] };
|
||||
}
|
||||
},
|
||||
|
||||
likeWechatSteps: function (params) {
|
||||
@@ -1598,9 +1752,25 @@ function _sendMessageInternal(toId, content, msgType) {
|
||||
}
|
||||
}
|
||||
|
||||
var messageId = sent ? generateId('msg') : null;
|
||||
if (sent) {
|
||||
var talker = (toId === '文件传输助手' || toId === 'File Transfer') ? 'filehelper' : toId;
|
||||
_messageBuffer.push({
|
||||
msg_svr_id: messageId,
|
||||
msg_type: msgType || 'text',
|
||||
from_id: talker,
|
||||
content: content,
|
||||
timestamp: new Date().toISOString(),
|
||||
is_send: true,
|
||||
});
|
||||
if (_messageBuffer.length > CONFIG.MESSAGE_BUFFER_MAX) {
|
||||
_messageBuffer = _messageBuffer.slice(-Math.floor(CONFIG.MESSAGE_BUFFER_MAX * 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: sent,
|
||||
message_id: sent ? generateId('msg') : null,
|
||||
message_id: messageId,
|
||||
method: sent ? 'hook' : 'failed',
|
||||
to_id: toId,
|
||||
};
|
||||
@@ -1611,6 +1781,10 @@ function _sendMessageInternal(toId, content, msgType) {
|
||||
// ============================================================
|
||||
|
||||
function _getMessagesFromDB(conversationId, limit) {
|
||||
// DB talker 用 wxid;显示名「文件传输助手」→ filehelper
|
||||
if (conversationId === '文件传输助手' || conversationId === 'File Transfer') {
|
||||
conversationId = 'filehelper';
|
||||
}
|
||||
var where = conversationId
|
||||
? " WHERE talker='" + conversationId.replace(/'/g, "''") + "'"
|
||||
: '';
|
||||
@@ -1631,17 +1805,102 @@ function _getMessagesFromDB(conversationId, limit) {
|
||||
}
|
||||
|
||||
function _getContactsFromDB(limit) {
|
||||
var sql = "SELECT username, nickname, conRemark, type FROM rcontact"
|
||||
+ " WHERE type NOT IN (0, 33) AND verifyFlag=0"
|
||||
+ " AND username NOT LIKE '%@chatroom'"
|
||||
+ " AND username NOT LIKE 'gh_%'"
|
||||
+ " ORDER BY nickname LIMIT " + limit;
|
||||
limit = limit || 200;
|
||||
var labelMap = {};
|
||||
try {
|
||||
var labelRows = _execSQL("SELECT labelId, labelName FROM contactlabelinfo");
|
||||
labelRows.forEach(function (r) {
|
||||
labelMap[r.labelId] = r.labelName;
|
||||
});
|
||||
} catch (e) { /* 部分版本无标签表 */ }
|
||||
|
||||
var contactLabelMap = {};
|
||||
try {
|
||||
// 8.0.69 列名为 contactLabelIds(旧版 labelidlist)
|
||||
var clRows = _execSQL(
|
||||
"SELECT username, contactLabelIds FROM rcontact WHERE contactLabelIds IS NOT NULL AND contactLabelIds != ''"
|
||||
);
|
||||
clRows.forEach(function (r) {
|
||||
if (!r.contactLabelIds) return;
|
||||
var ids = String(r.contactLabelIds).split(",").filter(function (id) { return id; });
|
||||
contactLabelMap[r.username] = ids.map(function (id) {
|
||||
return labelMap[id] || id;
|
||||
}).filter(function (n) { return n; });
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// 8.0.69 兼容:先探测 rcontact 实际列,只 SELECT 存在的列(避免缺列如 signature/country 致整查询返 0)
|
||||
var _wantCols = ["username", "nickname", "conRemark", "type", "alias", "imgFlag", "sex", "country", "province", "city", "signature"];
|
||||
var _availCols = {};
|
||||
try {
|
||||
var _ti = _execSQL("PRAGMA table_info(rcontact)");
|
||||
(_ti || []).forEach(function (c) { if (c && c.name) _availCols[c.name] = true; });
|
||||
} catch (ept) { /* PRAGMA 不支持时退回全列 */ }
|
||||
var _useCols = Object.keys(_availCols).length > 0
|
||||
? _wantCols.filter(function (c) { return _availCols[c]; })
|
||||
: _wantCols;
|
||||
if (_useCols.indexOf("username") < 0) _useCols.unshift("username");
|
||||
var _cols = "SELECT " + _useCols.join(", ") + " FROM rcontact ";
|
||||
var _excludeSys = "username NOT LIKE '%@chatroom' AND username NOT LIKE 'gh_%' "
|
||||
+ "AND username NOT IN ('filehelper','fmessage','medianote','floatbottle','newsapp','weixin','qmessage','tmessage') ";
|
||||
var sql = _cols + "WHERE type NOT IN (0, 33) AND verifyFlag=0 AND " + _excludeSys
|
||||
+ "ORDER BY conRemark, nickname LIMIT " + limit;
|
||||
var rows = _execSQL(sql);
|
||||
var _diag = { strict: (rows || []).length, use_cols: _useCols, avail_cols: Object.keys(_availCols) };
|
||||
// 8.0.69 兼容回退①:严格过滤(type/verifyFlag)为空时,放宽为仅排除系统/群/公众号
|
||||
if (!rows || rows.length === 0) {
|
||||
var sqlRelax = _cols + "WHERE verifyFlag=0 AND " + _excludeSys
|
||||
+ "ORDER BY conRemark, nickname LIMIT " + limit;
|
||||
var relaxed = _execSQL(sqlRelax);
|
||||
_diag.relax_verifyflag = (relaxed || []).length;
|
||||
if (relaxed && relaxed.length > 0) {
|
||||
log('info', 'contacts', '严格过滤空,回退宽过滤命中 ' + relaxed.length + ' 条');
|
||||
rows = relaxed;
|
||||
}
|
||||
}
|
||||
// 8.0.69 兼容回退②:连 verifyFlag=0 也空时,丢弃 verifyFlag,仅排除系统/群/公众号(部分版本好友 verifyFlag!=0)
|
||||
if (!rows || rows.length === 0) {
|
||||
var sqlNoVerify = _cols + "WHERE " + _excludeSys + "ORDER BY conRemark, nickname LIMIT " + limit;
|
||||
var noverify = _execSQL(sqlNoVerify);
|
||||
_diag.relax_noverify = (noverify || []).length;
|
||||
if (noverify && noverify.length > 0) {
|
||||
log('info', 'contacts', 'verifyFlag 过滤空,丢弃 verifyFlag 命中 ' + noverify.length + ' 条');
|
||||
rows = noverify;
|
||||
}
|
||||
}
|
||||
// 诊断:rcontact 原始总行数 + 字段探测(定位 8.0.69 读空根因,写入返回的 _diag)
|
||||
if (!rows || rows.length === 0) {
|
||||
try { var rawCnt = _execSQL("SELECT count(*) AS n FROM rcontact"); _diag.rcontact_total = rawCnt && rawCnt[0] ? rawCnt[0].n : null; } catch (e0) { _diag.rcontact_total_err = String(e0); }
|
||||
try { var vf = _execSQL("SELECT verifyFlag, count(*) AS n FROM rcontact GROUP BY verifyFlag"); _diag.verifyflag_dist = vf; } catch (e1) { _diag.verifyflag_err = String(e1); }
|
||||
try { var tp = _execSQL("SELECT type, count(*) AS n FROM rcontact GROUP BY type LIMIT 20"); _diag.type_dist = tp; } catch (e2) { _diag.type_err = String(e2); }
|
||||
}
|
||||
var sexMap = { "0": "unknown", "1": "male", "2": "female" };
|
||||
var contacts = rows.map(function (r) {
|
||||
return { wxid: r.username, nickname: r.nickname, remark: r.conRemark, type: r.type };
|
||||
var nick = r.nickname || "";
|
||||
var remark = r.conRemark || "";
|
||||
var alias = r.alias || "";
|
||||
var wxid = r.username || "";
|
||||
var display = nick || remark || alias || wxid;
|
||||
return {
|
||||
wxid: wxid,
|
||||
user_id: wxid,
|
||||
nickname: nick,
|
||||
display_name: display,
|
||||
remark: remark,
|
||||
alias: alias,
|
||||
wechat_id: alias,
|
||||
type: r.type,
|
||||
sex: sexMap[String(r.sex)] || "unknown",
|
||||
country: r.country || "",
|
||||
province: r.province || "",
|
||||
city: r.city || "",
|
||||
signature: r.signature || "",
|
||||
tags: contactLabelMap[wxid] || [],
|
||||
has_avatar: (r.imgFlag || 0) > 0,
|
||||
};
|
||||
});
|
||||
_contactCache = contacts;
|
||||
return { success: true, contacts: contacts, count: contacts.length };
|
||||
return { success: true, contacts: contacts, count: contacts.length, _diag: _diag };
|
||||
}
|
||||
|
||||
function _getContactInfoFromDB(wxid) {
|
||||
@@ -1909,20 +2168,146 @@ function _quitGroupInternal(groupId) {
|
||||
// § 9 内部实现 — 朋友圈 (H20/H21)
|
||||
// ============================================================
|
||||
|
||||
function _postMomentsInternal(content, imageUrls) {
|
||||
// 取当前栈顶 Activity(ActivityThread.mActivities 遍历 paused=false)
|
||||
function _getTopActivity() {
|
||||
var top = null;
|
||||
try {
|
||||
var Intent = Java.use('android.content.Intent');
|
||||
var ctx = getCtx();
|
||||
var intent = Intent.$new('com.workphone.POST_MOMENTS');
|
||||
intent.putExtra('content', content);
|
||||
if (imageUrls.length > 0) intent.putExtra('image_urls', imageUrls.join(','));
|
||||
ctx.sendBroadcast(intent);
|
||||
log('info', 'sns', '发布朋友圈: content_len=' + content.length + ' images=' + imageUrls.length);
|
||||
emitEvent('moments_posted', { content: content, image_count: imageUrls.length });
|
||||
return { success: true, method: 'intent_broadcast', sns_id: generateId('sns') };
|
||||
var ActivityThread = Java.use('android.app.ActivityThread');
|
||||
var at = ActivityThread.currentActivityThread();
|
||||
var mActivities = at.mActivities.value; // ArrayMap<IBinder, ActivityClientRecord>
|
||||
var vals = mActivities.values().toArray();
|
||||
for (var i = 0; i < vals.length; i++) {
|
||||
var rec = Java.cast(vals[i], Java.use('android.app.ActivityThread$ActivityClientRecord'));
|
||||
try {
|
||||
if (!rec.paused.value) { top = rec.activity.value; break; }
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
return { success: false, error: '发朋友圈失败: ' + e };
|
||||
log('warn', 'sns', '_getTopActivity 失败: ' + e);
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
// 递归遍历 view 树,找文本匹配且可点击的 View 并 performClick
|
||||
function _clickViewByText(root, texts) {
|
||||
if (!root) return false;
|
||||
try {
|
||||
var TextView = Java.use('android.widget.TextView');
|
||||
var ViewGroup = Java.use('android.view.ViewGroup');
|
||||
// 当前节点是 TextView 且文本命中
|
||||
try {
|
||||
var asTv = Java.cast(root, TextView);
|
||||
var t = asTv.getText();
|
||||
var s = t ? t.toString() : '';
|
||||
for (var k = 0; k < texts.length; k++) {
|
||||
if (s.indexOf(texts[k]) >= 0) {
|
||||
var clickTarget = root;
|
||||
// 向上找可点击祖先(按钮文字常包在可点击容器内)
|
||||
var p = root;
|
||||
for (var up = 0; up < 4 && p; up++) {
|
||||
if (p.isClickable()) { clickTarget = p; break; }
|
||||
p = p.getParent();
|
||||
}
|
||||
if (clickTarget && clickTarget.performClick) {
|
||||
clickTarget.performClick();
|
||||
log('info', 'sns', '点击发表按钮: "' + s + '"');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
// 递归子节点
|
||||
try {
|
||||
var vg = Java.cast(root, ViewGroup);
|
||||
var n = vg.getChildCount();
|
||||
for (var c = 0; c < n; c++) {
|
||||
if (_clickViewByText(vg.getChildAt(c), texts)) return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 朋友圈发布界面打开后,自动点「发表」(best-effort,失败由 need_confirm_send 兜底)
|
||||
function _autoClickPublish(delayMs) {
|
||||
try {
|
||||
var Handler = Java.use('android.os.Handler');
|
||||
var Looper = Java.use('android.os.Looper');
|
||||
var handler = Handler.$new(Looper.getMainLooper());
|
||||
var Runnable = Java.use('java.lang.Runnable');
|
||||
var R = Java.registerClass({
|
||||
name: 'com.workphone.PublishClicker' + Date.now(),
|
||||
implements: [Runnable],
|
||||
methods: {
|
||||
run: function () {
|
||||
try {
|
||||
var act = _getTopActivity();
|
||||
if (!act) { log('warn', 'sns', '自动发表:无栈顶 Activity'); return; }
|
||||
var decor = act.getWindow().getDecorView();
|
||||
var ok = _clickViewByText(decor, ['发表', '完成', 'Post', 'Send']);
|
||||
emitEvent('moments_publish_click', { clicked: ok });
|
||||
log('info', 'sns', '自动发表点击结果: ' + ok);
|
||||
} catch (e) {
|
||||
log('warn', 'sns', '自动发表执行失败: ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
handler.postDelayed(R.$new(), delayMs || 2000);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('warn', 'sns', '_autoClickPublish 调度失败: ' + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 真发布:启动微信朋友圈发布 Activity(SnsUploadUI)并预填内容
|
||||
// 替代旧的 com.workphone.POST_MOMENTS 广播(无接收端=占位)。
|
||||
// 纯文字朋友圈:Ksnsupload_type=9(社区已知),文本 extra 多 key 兜底。
|
||||
function _postMomentsInternal(content, imageUrls, autoSend) {
|
||||
imageUrls = imageUrls || [];
|
||||
if (autoSend === undefined) autoSend = true;
|
||||
var candidates = getCompat('sns_upload'); // [{cls:'...SnsUploadUI'}, ...]
|
||||
var lastErr = '';
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
var clsName = candidates[i].cls;
|
||||
try {
|
||||
var Intent = Java.use('android.content.Intent');
|
||||
var ComponentName = Java.use('android.content.ComponentName');
|
||||
var ctx = getCtx();
|
||||
var SnsCls = Java.use(clsName); // 校验类存在
|
||||
var intent = Intent.$new();
|
||||
intent.setComponent(ComponentName.$new('com.tencent.mm', clsName));
|
||||
intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
||||
// 纯文字:type=9;图文:type=1(含 imglist)
|
||||
var isText = imageUrls.length === 0;
|
||||
intent.putExtra('Ksnsupload_type', isText ? 9 : 1);
|
||||
// 文本 extra 多 key 兜底(不同版本 key 名不同)
|
||||
intent.putExtra('Ksnsupload_text', content);
|
||||
intent.putExtra('sns_text', content);
|
||||
intent.putExtra('Ksns_upload_text', content);
|
||||
if (!isText) {
|
||||
intent.putExtra('Ksnsupload_imglist', imageUrls.join(','));
|
||||
}
|
||||
ctx.startActivity(intent);
|
||||
log('info', 'sns', '启动 SnsUploadUI(' + clsName + ') content_len=' + content.length + ' images=' + imageUrls.length);
|
||||
emitEvent('moments_post_ui_opened', { content: content, image_count: imageUrls.length, cls: clsName });
|
||||
// 打开发布界面+预填后,best-effort 自动点「发表」(失败由 need_confirm_send 兜底)
|
||||
var scheduled = autoSend ? _autoClickPublish(2200) : false;
|
||||
return {
|
||||
success: true,
|
||||
method: 'sns_upload_ui',
|
||||
cls: clsName,
|
||||
auto_click_scheduled: scheduled,
|
||||
need_confirm_send: !scheduled,
|
||||
note: scheduled ? '已打开朋友圈发布界面并预填,2.2s 后自动点发表' : '已打开发布界面并预填,待点发表',
|
||||
sns_id: generateId('sns'),
|
||||
};
|
||||
} catch (e) {
|
||||
lastErr = String(e);
|
||||
}
|
||||
}
|
||||
return { success: false, error: '发朋友圈失败(所有 SnsUploadUI 候选): ' + lastErr };
|
||||
}
|
||||
|
||||
function _getMomentsFromDB(wxid, limit) {
|
||||
@@ -1934,13 +2319,38 @@ function _getMomentsFromDB(wxid, limit) {
|
||||
var where = wxid
|
||||
? " WHERE userName='" + wxid.replace(/'/g, "''") + "'"
|
||||
: '';
|
||||
var sql = "SELECT snsId, userName, createTime, content, mediaCount, likeCount, commentCount FROM SnsInfo"
|
||||
// 8.0.69 兼容:探测 SnsInfo 实际列,只 SELECT 存在的列(避免 mediaCount/likeCount/commentCount 缺列致整查询返 0)
|
||||
var _wantSns = ["snsId", "userName", "createTime", "content", "mediaCount", "likeCount", "commentCount", "type", "sourceType"];
|
||||
var _availSns = {};
|
||||
var _snsDiag = {};
|
||||
try {
|
||||
var _sti = _execSnsSQL("PRAGMA table_info(SnsInfo)");
|
||||
(_sti || []).forEach(function (c) { if (c && c.name) _availSns[c.name] = true; });
|
||||
_snsDiag.avail_cols = Object.keys(_availSns);
|
||||
} catch (eps) { _snsDiag.pragma_err = String(eps); }
|
||||
var _useSns = Object.keys(_availSns).length > 0
|
||||
? _wantSns.filter(function (c) { return _availSns[c]; })
|
||||
: _wantSns;
|
||||
if (_useSns.indexOf("snsId") < 0) _useSns.unshift("snsId");
|
||||
_snsDiag.use_cols = _useSns;
|
||||
var sql = "SELECT " + _useSns.join(", ") + " FROM SnsInfo"
|
||||
+ where + " ORDER BY createTime DESC LIMIT " + limit;
|
||||
|
||||
var rows = _execSnsSQL(sql);
|
||||
_snsDiag.rows = (rows || []).length;
|
||||
if (!rows || rows.length === 0) {
|
||||
try { var sc = _execSnsSQL("SELECT count(*) AS n FROM SnsInfo"); _snsDiag.snsinfo_total = sc && sc[0] ? sc[0].n : null; } catch (e0) { _snsDiag.total_err = String(e0); }
|
||||
var hasSnsRows = safeInt(_snsDiag.snsinfo_total, 0) > 0;
|
||||
var emptyMoments = _emptyReadFailure('朋友圈', _lastDbDiag, hasSnsRows);
|
||||
if (emptyMoments) {
|
||||
emptyMoments.moments = [];
|
||||
emptyMoments._diag = _snsDiag;
|
||||
return emptyMoments;
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
moments: rows.map(function (r) {
|
||||
moments: (rows || []).map(function (r) {
|
||||
var parsed = _parseSnsContent(r.content);
|
||||
return {
|
||||
sns_id: r.snsId,
|
||||
@@ -1952,7 +2362,8 @@ function _getMomentsFromDB(wxid, limit) {
|
||||
timestamp: r.createTime,
|
||||
};
|
||||
}),
|
||||
count: rows.length,
|
||||
count: (rows || []).length,
|
||||
_diag: _snsDiag,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2011,12 +2422,38 @@ function _intentAction(intentAction, extras, eventType, eventPayload) {
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
intent.putExtra(keys[i], String(extras[keys[i]]));
|
||||
}
|
||||
// 真机铁律 #3/#14:先校验是否存在接收端,无接收端不得返回 success:true(禁止假成功)
|
||||
var receivers = 0;
|
||||
try {
|
||||
var pm = ctx.getPackageManager();
|
||||
var list = pm.queryBroadcastReceivers(intent, 0);
|
||||
receivers = list ? list.size() : 0;
|
||||
} catch (qe) {
|
||||
receivers = -1; // 查询失败:未知,按未验证处理
|
||||
}
|
||||
if (receivers === 0) {
|
||||
log('warn', 'intent', intentAction + ' 无接收端 → 不广播(避免假成功,需 _intentAction 真实现或装接收端 App)');
|
||||
return {
|
||||
success: false,
|
||||
method: 'intent_broadcast',
|
||||
verified: false,
|
||||
error: 'no_receiver_registered',
|
||||
action: intentAction,
|
||||
hint: '该动作经 Android 广播分发,当前设备未注册接收端;需实现真实 Frida RPC 或部署接收端 App',
|
||||
};
|
||||
}
|
||||
ctx.sendBroadcast(intent);
|
||||
if (eventType) {
|
||||
emitEvent(eventType, eventPayload || {});
|
||||
}
|
||||
log('info', 'intent', intentAction + ' → ' + keys.join(','));
|
||||
return { success: true, method: 'intent_broadcast', action: intentAction };
|
||||
log('info', 'intent', intentAction + ' → ' + keys.join(',') + ' (receivers=' + receivers + ')');
|
||||
return {
|
||||
success: true,
|
||||
method: 'intent_broadcast',
|
||||
verified: receivers > 0,
|
||||
receivers: receivers,
|
||||
action: intentAction,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), action: intentAction };
|
||||
|
||||
@@ -935,8 +935,40 @@ rpc.exports = {
|
||||
return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {});
|
||||
},
|
||||
|
||||
getStickerList: function () {
|
||||
return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {});
|
||||
getStickerList: function (params) {
|
||||
// 真实现:读 EnMicroMsg.db 的 EmojiInfo 表(自定义表情);动态列探测兼容 8.0.x;
|
||||
// 表不存在/无自定义表情时诚实返回空,不假成功(真机铁律 #3/#14)。
|
||||
var limit = (params && params.limit) || 100;
|
||||
try {
|
||||
return Java.performNow(function () {
|
||||
var cols;
|
||||
try { cols = _execSQL("PRAGMA table_info(EmojiInfo)"); } catch (e0) { cols = []; }
|
||||
if (!cols || cols.length === 0) {
|
||||
return { success: true, stickers: [], count: 0, channel: 'frida_db',
|
||||
note: 'EmojiInfo 表不存在或无自定义表情(版本/账号差异)' };
|
||||
}
|
||||
var names = cols.map(function (c) { return c.name; });
|
||||
var want = ['md5', 'catalog', 'name', 'type', 'size', 'cdnUrl', 'groupId', 'reserved4'];
|
||||
var sel = want.filter(function (w) { return names.indexOf(w) >= 0; });
|
||||
if (sel.length === 0) sel = names.slice(0, 6);
|
||||
var rows = _execSQL("SELECT " + sel.join(',') + " FROM EmojiInfo LIMIT " + limit);
|
||||
return {
|
||||
success: true,
|
||||
channel: 'frida_db',
|
||||
columns: sel,
|
||||
stickers: rows.map(function (r) {
|
||||
return {
|
||||
md5: r.md5, name: r.name, catalog: r.catalog,
|
||||
type: r.type, size: r.size,
|
||||
cdn_url: r.cdnUrl || r.reserved4 || '', group_id: r.groupId
|
||||
};
|
||||
}),
|
||||
count: rows.length,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e), stickers: [] };
|
||||
}
|
||||
},
|
||||
|
||||
likeWechatSteps: function (params) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"code":200,"data":{"success":true,"channel":"hook","columns":["md5","catalog","name","type","size","cdnUrl","groupId","reserved4"],"stickers":[{"md5":"c0059fa4f781a2a500ec03fade10e9b1","name":"icon_002.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"e6f269a19ff2fb61fdb847b39c86ebca","name":"icon_007.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"d629cb3c544fb719405f2b9f16ed6e6c","name":"icon_010.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"e2e2e96798bfbd55b35c3111d89b2e17","name":"icon_012.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"d13e21be9fd71777f727e0c34b0d3994","name":"icon_013.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"68f9864ca5c0a5d823ed7184e113a4aa","name":"icon_018.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"1483ce786912099e29551915e0bc2125","name":"icon_019.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"31574013280aac3897722cc7e3e49ee4","name":"icon_021.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"a00d1de64298d9eaa145ec848a9cc8af","name":"icon_022.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6257411b26d5aa873762490769625bb9","name":"icon_024.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"5a7fc462d63ef845e6d99c1523bbc91e","name":"icon_027.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"72ebfa527add152c6872219044b151c3","name":"icon_029.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6a9284bc5ce0bf059375e970a49fa2c5","name":"icon_030.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6ae79b62bab61132981f1e85ad7070c4","name":"icon_035.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"aab84584b5a3f262286cb38bb107b53e","name":"icon_040.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"bb82ce58f5ed6fdd2b5e34fc2a8e347a","name":"icon_020.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"514914788fc461e7205bf0b6ba496c49","name":"jsb_j.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"f790e342a02e0f99d34b316547f9aeab","name":"jsb_s.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"091577322c40c05aa3dd701da29d6423","name":"jsb_b.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"da1c289d4e363f3ce1ff36538903b92f","name":"dice_1.png","catalog":"50","type":"1","size":"0","cdn_url":"0","group_id":"50"}],"count":20,"action_resolved":"get_sticker_list"},"action":"get_sticker_list","channel_used":"websocket/hook"}
|
||||
Reference in New Issue
Block a user