// 微信工作手机 SDK v9-final - 全量Frida RPC接口 // 修复: Java.choose异步问题 -> 改用全局初始化+同步等待 // 修复: chatroom表列名(owner->roomowner), ContactLabel表, 朋友圈DB路径 // 账号:游条姐 (wxid_5g37snchpv8e22) 'use strict'; var _dbs = {}; var _dbsReady = false; var _initCount = 0; // ==================== DB初始化(脚本加载时立即执行)==================== function initAllDBs() { _initCount++; Java.choose('com.tencent.wcdb.database.SQLiteDatabase', { onMatch: function(db) { try { var p = db.getPath(); if (!p) return; if (p.indexOf('EnMicroMsg') > -1 && p.indexOf('EnMicroMsg.db') > -1) _dbs['main'] = db; else if (p.indexOf('SnsMicroMsg') > -1) _dbs['sns'] = db; else if (p.indexOf('sns_star_info') > -1) _dbs['sns_star'] = db; else if (p.indexOf('WalletCoreDB-2') > -1) _dbs['wallet'] = db; else if (p.indexOf('WalletPay') > -1) _dbs['wallet2'] = db; else if (p.indexOf('FinderMM029') > -1) _dbs['finder'] = db; else if (p.indexOf('FinderMessage006') > -1) _dbs['finder2'] = db; else if (p.indexOf('FinderAccounts_01') > -1) _dbs['finder3'] = db; else if (p.indexOf('enFavorite') > -1) _dbs['favorite'] = db; else if (p.indexOf('FTS5IndexMicroMsg') > -1) _dbs['fts5'] = db; else if (p.indexOf('AppBrandComm') > -1) _dbs['appbrand'] = db; else if (p.indexOf('WxFileIndex') > -1) _dbs['wxfile'] = db; var key = '_path_' + p.split('/').pop().replace('.db',''); _dbs[key] = db; } catch(e) {} }, onComplete: function() { _dbsReady = true; var dbNames = Object.keys(_dbs).filter(function(k) { return !k.startsWith('_path_'); }); send('[*] WeChat SDK v9-final loaded. DBs: ' + dbNames.join(', ')); } }); } // 脚本加载时立即初始化 Java.perform(function() { initAllDBs(); }); function getDB(name) { if (!_dbs[name]) { // 重新尝试初始化(可能DB刚被加载) initAllDBs(); // 等待一小段时间 var start = Date.now(); while (!_dbs[name] && Date.now() - start < 2000) { // 忙等待最多2秒 } } return _dbs[name] || null; } function execSQL(db, sql, args) { if (!db) return {error: 'DB not available', rows: [], count: 0}; try { var cursor; if (args && args.length > 0) { var jArgs = Java.array('java.lang.String', args.map(function(a) { return a ? String(a) : null; })); cursor = db.rawQuery(sql, jArgs); } else { cursor = db.rawQuery(sql, null); } var cols = []; var colCount = cursor.getColumnCount(); for (var i = 0; i < colCount; i++) cols.push(cursor.getColumnName(i)); var rows = []; while (cursor.moveToNext()) { var row = {}; for (var j = 0; j < colCount; j++) { try { row[cols[j]] = cursor.getString(j); } catch(e) { row[cols[j]] = null; } } rows.push(row); } cursor.close(); return {columns: cols, rows: rows, count: rows.length}; } catch(e) { return {error: e.toString(), rows: [], count: 0}; } } function writeSQL(db, sql) { if (!db) return {error: 'DB not available'}; try { db.execSQL(sql); return {success: true}; } catch(e) { return {error: e.toString()}; } } // ==================== RPC接口 ==================== rpc.exports = { // ===== 系统 ===== ping: function() { var dbStatus = {}; ['main','sns','sns_star','wallet','finder','favorite','fts5','appbrand'].forEach(function(n) { dbStatus[n] = !!_dbs[n]; }); return {status: 'ok', version: 'v9-final', timestamp: Date.now(), dbs: dbStatus}; }, initDbs: function() { initAllDBs(); var status = {}; ['main','sns','sns_star','wallet','wallet2','finder','finder2','finder3','favorite','fts5','appbrand'].forEach(function(n) { status[n] = !!_dbs[n]; }); return {status: status, total_dbs: Object.keys(_dbs).length}; }, // ===== 账号 ===== getAccountInfo: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var wxid = execSQL(db, "SELECT value FROM userinfo WHERE id=2"); var nick = execSQL(db, "SELECT value FROM userinfo WHERE id=6"); var avatar = execSQL(db, "SELECT value FROM userinfo WHERE id=4"); var phone = execSQL(db, "SELECT value FROM userinfo WHERE id=42"); var region = execSQL(db, "SELECT value FROM userinfo WHERE id=44"); var sex = execSQL(db, "SELECT value FROM userinfo WHERE id=12"); return { wxid: wxid.rows[0] ? wxid.rows[0].value : null, nickname: nick.rows[0] ? nick.rows[0].value : null, avatar: avatar.rows[0] ? avatar.rows[0].value : null, phone: phone.rows[0] ? phone.rows[0].value : null, region: region.rows[0] ? region.rows[0].value : null, sex: sex.rows[0] ? sex.rows[0].value : null, }; }, // ===== 联系人 ===== getContacts: function(limit, offset) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 100; offset = parseInt(offset) || 0; return execSQL(db, "SELECT username, alias, conRemark, nickname, type, contactLabelIds " + "FROM rcontact WHERE type=3 ORDER BY rowid LIMIT " + limit + " OFFSET " + offset ); }, getContactCount: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var r1 = execSQL(db, "SELECT COUNT(*) as friends FROM rcontact WHERE type=3"); var r2 = execSQL(db, "SELECT COUNT(*) as total FROM rcontact"); return { friends: r1.rows[0] ? parseInt(r1.rows[0].friends) : 0, total: r2.rows[0] ? parseInt(r2.rows[0].total) : 0 }; }, getContactDetail: function(wxid) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; return execSQL(db, "SELECT * FROM rcontact WHERE username='" + wxid + "' LIMIT 1"); }, searchContacts: function(keyword) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; return execSQL(db, "SELECT username, alias, conRemark, nickName, type FROM rcontact " + "WHERE (nickName LIKE '%" + keyword + "%' OR conRemark LIKE '%" + keyword + "%' OR alias LIKE '%" + keyword + "%') " + "AND type NOT IN (0,4) LIMIT 50" ); }, // ===== 标签 ===== getLabels: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; // 先尝试ContactLabel表 var r = execSQL(db, "SELECT labelId, labelName, createTime FROM ContactLabel ORDER BY labelId"); if (r.count > 0) return r; // 备用:从rcontact的contactLabelIds字段提取唯一ID var ids = execSQL(db, "SELECT DISTINCT contactLabelIds FROM rcontact WHERE contactLabelIds != '' AND contactLabelIds IS NOT NULL LIMIT 200"); var labelSet = {}; ids.rows.forEach(function(row) { var parts = (row.contactLabelIds || '').split(','); parts.forEach(function(id) { if (id) labelSet[id.trim()] = true; }); }); var rows = Object.keys(labelSet).map(function(id) { return {labelId: id, labelName: 'Label_' + id}; }); return {columns: ['labelId', 'labelName'], rows: rows, count: rows.length}; }, getLabelContacts: function(labelId) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; return execSQL(db, "SELECT username, nickName, conRemark, contactLabelIds FROM rcontact " + "WHERE contactLabelIds LIKE '%" + labelId + "%' AND type=3 LIMIT 200" ); }, // ===== 统计 ===== getAllStats: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var friends = execSQL(db, "SELECT COUNT(*) as n FROM rcontact WHERE type=3"); var groups = execSQL(db, "SELECT COUNT(*) as n FROM chatroom"); var labels = execSQL(db, "SELECT COUNT(*) as n FROM ContactLabel"); var msgs = execSQL(db, "SELECT COUNT(*) as n FROM message"); var imgs = execSQL(db, "SELECT COUNT(*) as n FROM ImgInfo2"); var videos = execSQL(db, "SELECT COUNT(*) as n FROM videoinfo2"); var files = execSQL(db, "SELECT COUNT(*) as n FROM FileMsgInfo"); var labels = execSQL(db, "SELECT COUNT(*) as n FROM ContactLabel"); return { friends: friends.rows[0] ? parseInt(friends.rows[0].n) : 0, groups: groups.rows[0] ? parseInt(groups.rows[0].n) : 0, messages: msgs.rows[0] ? parseInt(msgs.rows[0].n) : 0, images: imgs.rows[0] ? parseInt(imgs.rows[0].n) : 0, videos: videos.rows[0] ? parseInt(videos.rows[0].n) : 0, files: files.rows[0] ? parseInt(files.rows[0].n) : 0, labels: labels.rows[0] ? parseInt(labels.rows[0].n) : 0 }; }, // ===== 消息 ===== getRecentMessages: function(hours) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; hours = parseInt(hours) || 24; var since = Date.now() - hours * 3600 * 1000; return execSQL(db, "SELECT msgId, type, content, talker, createTime, isSend FROM message " + "WHERE createTime > " + since + " ORDER BY createTime DESC LIMIT 200" ); }, getMessages: function(talker, limit) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 50; return execSQL(db, "SELECT msgId, type, content, talker, createTime, isSend FROM message " + "WHERE talker='" + talker + "' ORDER BY createTime DESC LIMIT " + limit ); }, sendMessage: function(toUser, content, type) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; type = parseInt(type) || 1; var msgId = Date.now(); var createTime = Date.now(); var sql = "INSERT INTO message (msgId, type, status, isSend, content, talker, createTime, imgPath, reserved, lvbuffer, transContent, flag, bizClientMsgId, bizChatId, bizChatUserId, msgSeq, historyId) " + "VALUES (" + msgId + ", " + type + ", 3, 1, '" + content.replace(/'/g, "''") + "', '" + toUser + "', " + createTime + ", '', 0, NULL, '', 0, '', 0, '', 0, 0)"; var r = writeSQL(db, sql); if (r.success) { var verify = execSQL(db, "SELECT msgId, content, talker, createTime FROM message WHERE msgId=" + msgId); return {success: true, msgId: msgId, toUser: toUser, content: content, db_verified: verify.count > 0, note: "Written to local DB. For real network send, WeChat 8.0.56 network class name needed."}; } return {success: false, error: r.error}; }, massMessage: function(targets, content) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var results = []; var successCount = 0; (targets || []).forEach(function(toUser) { var msgId = Date.now() + Math.floor(Math.random() * 10000); var createTime = Date.now(); var sql = "INSERT INTO message (msgId, type, status, isSend, content, talker, createTime, imgPath, reserved, lvbuffer, transContent, flag, bizClientMsgId, bizChatId, bizChatUserId, msgSeq, historyId) " + "VALUES (" + msgId + ", 1, 3, 1, '" + content.replace(/'/g, "''") + "', '" + toUser + "', " + createTime + ", '', 0, NULL, '', 0, '', 0, '', 0, 0)"; var r = writeSQL(db, sql); if (r.success) { successCount++; results.push({toUser: toUser, msgId: msgId, success: true}); } else { results.push({toUser: toUser, success: false, error: r.error}); } }); return {total: targets.length, success: successCount, results: results}; }, // ===== 群组 ===== getGroups: function(limit) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 50; // 先查chatroom表的列名 var cols = execSQL(db, "PRAGMA table_info(chatroom)"); var colNames = cols.rows.map(function(r) { return r.name; }); var selectCols = ['chatroomname']; if (colNames.indexOf('memberCount') > -1) selectCols.push('memberCount'); if (colNames.indexOf('roomowner') > -1) selectCols.push('roomowner'); if (colNames.indexOf('announcement') > -1) selectCols.push('announcement'); if (colNames.indexOf('selfDisplayName') > -1) selectCols.push('selfDisplayName'); if (colNames.indexOf('memberlist') > -1) selectCols.push('memberlist'); if (colNames.indexOf('NickName') > -1) selectCols.push('NickName'); return execSQL(db, "SELECT " + selectCols.join(', ') + " FROM chatroom ORDER BY rowid DESC LIMIT " + limit); }, getGroupCount: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; return execSQL(db, "SELECT COUNT(*) as total FROM chatroom"); }, getGroupDetail: function(chatroomId) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; return execSQL(db, "SELECT * FROM chatroom WHERE chatroomname='" + chatroomId + "' LIMIT 1"); }, getGroupMembers: function(chatroomId) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var group = execSQL(db, "SELECT memberlist, roomowner, chatroomnotice, selfDisplayName FROM chatroom WHERE chatroomname='" + chatroomId + "'"); if (!group.rows[0]) return {error: 'Group not found', rows: [], count: 0}; var g = group.rows[0]; var members = (g.memberlist || '').split(';').filter(function(m) { return m.trim(); }); return { chatroomId: chatroomId, owner: g.roomowner, announcement: g.chatroomnotice, selfDisplayName: g.selfDisplayName, memberCount: members.length, members: members.slice(0, 100), rows: members.slice(0, 100).map(function(m) { return {wxid: m}; }), count: members.length }; }, setGroupAnnouncement: function(chatroomId, announcement) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var sql = "UPDATE chatroom SET chatroomnotice='" + announcement.replace(/'/g, "''") + "' WHERE chatroomname='" + chatroomId + "'"; return writeSQL(db, sql); }, setGroupNickname: function(chatroomId, nickname) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var sql = "UPDATE chatroom SET selfDisplayName='" + nickname.replace(/'/g, "''") + "' WHERE chatroomname='" + chatroomId + "'"; return writeSQL(db, sql); }, // ===== 朋友圈 ===== getSnsDbInfo: function() { initAllDBs(); // 重新尝试获取DB return { sns: {available: !!_dbs['sns'], path: 'SnsMicroMsg.db'}, sns_star: {available: !!_dbs['sns_star'], path: 'sns_star_info.db'} }; }, getMoments: function(limit) { var db = _dbs['sns'] || _dbs['sns_star']; if (!db) { initAllDBs(); db = _dbs['sns'] || _dbs['sns_star']; } if (!db) return {error: 'SNS DB not loaded. Need to open WeChat moments first.', rows: [], count: 0}; limit = parseInt(limit) || 20; // 尝试不同的表名 var tables = ['SnsInfo', 'StarSnsInfo', 'sns_info']; for (var i = 0; i < tables.length; i++) { var r = execSQL(db, "SELECT * FROM " + tables[i] + " ORDER BY createTime DESC LIMIT " + limit); if (!r.error) return r; } return {error: 'No SNS table found', rows: [], count: 0}; }, postMoment: function(content, imgPaths) { var db = _dbs['sns'] || _dbs['sns_star']; if (!db) { initAllDBs(); db = _dbs['sns'] || _dbs['sns_star']; } if (!db) return {error: 'SNS DB not loaded. Open WeChat moments first.', success: false}; var snsId = Date.now(); var createTime = Math.floor(Date.now() / 1000); // 尝试写入StarSnsInfo var sql = "INSERT INTO StarSnsInfo (snsId, userName, createTime, content, localFlag, type, sourceType) " + "VALUES (" + snsId + ", 'wxid_5g37snchpv8e22', " + createTime + ", '" + content.replace(/'/g, "''") + "', 1, 1, 0)"; var r = writeSQL(db, sql); if (r.success) return {success: true, snsId: snsId, content: content, note: 'Written to local SNS DB.'}; // 尝试SnsInfo sql = "INSERT INTO SnsInfo (snsId, userName, createTime, content, localFlag, type) " + "VALUES (" + snsId + ", 'wxid_5g37snchpv8e22', " + createTime + ", '" + content.replace(/'/g, "''") + "', 1, 1)"; r = writeSQL(db, sql); if (r.success) return {success: true, snsId: snsId, content: content, note: 'Written to SnsInfo.'}; return {success: false, error: r.error}; }, // ===== 视频号 ===== getFinderInfo: function() { var db = getDB('finder'); if (!db) return {error: 'Finder DB not available'}; var tables = execSQL(db, "SELECT name FROM sqlite_master WHERE type='table'"); var result = {tables: tables.rows.map(function(r) { return r.name; })}; // 读取FinderFeedItem var feeds = execSQL(db, "SELECT * FROM FinderFeedItem LIMIT 5"); if (!feeds.error) result.feeds = feeds; return result; }, getFinderContacts: function(limit) { var db = getDB('finder'); if (!db) return {error: 'Finder DB not available', rows: [], count: 0}; limit = parseInt(limit) || 20; return execSQL(db, "SELECT * FROM FinderMsgContactStorage LIMIT " + limit); }, getFinderAccounts: function() { var db = getDB('finder3'); if (!db) return {error: 'FinderAccounts DB not available'}; var tables = execSQL(db, "SELECT name FROM sqlite_master WHERE type='table'"); var result = {tables: tables.rows.map(function(r) { return r.name; })}; var accounts = execSQL(db, "SELECT username, nickname, signature, avatarUrl FROM FinderAccount LIMIT 10"); if (!accounts.error) result.accounts = accounts; return result; }, // ===== 收藏 ===== getFavorites: function(limit) { var db = getDB('fts5') || getDB('favorite'); if (!db) return {error: 'Favorite DB not available', rows: [], count: 0}; limit = parseInt(limit) || 20; // 尝试不同表名 var tables = ['FTS5MetaFavorite', 'favorite_item', 'FavoriteItem']; for (var i = 0; i < tables.length; i++) { var r = execSQL(db, "SELECT * FROM " + tables[i] + " LIMIT " + limit); if (!r.error) return r; } // 列出所有表 var allTables = execSQL(db, "SELECT name FROM sqlite_master WHERE type='table'"); return {error: 'No favorite table found', available_tables: allTables.rows.map(function(r){return r.name;}), rows: [], count: 0}; }, // ===== 钱包/支付 ===== getWalletInfo: function() { var db = getDB('wallet'); if (!db) return {error: 'Wallet DB not available'}; var tables = execSQL(db, "SELECT name FROM sqlite_master WHERE type='table'"); return {tables: tables.rows.map(function(r) { return r.name; })}; }, getWalletLedger: function(limit) { var db = getDB('wallet'); if (!db) return {error: 'Wallet DB not available', rows: [], count: 0}; limit = parseInt(limit) || 20; var r = execSQL(db, "SELECT * FROM WalletLedgerRecord ORDER BY rowid DESC LIMIT " + limit); return r; }, getLuckyMoney: function(limit) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 20; return execSQL(db, "SELECT * FROM WalletLuckyMoney ORDER BY rowid DESC LIMIT " + limit); }, // ===== 媒体 ===== getImageCount: function() { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; return execSQL(db, "SELECT COUNT(*) as total FROM ImgInfo2"); }, getVideoList: function(limit) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 10; return execSQL(db, "SELECT * FROM videoinfo2 ORDER BY rowid DESC LIMIT " + limit); }, getFileList: function(limit) { var db = getDB('main'); if (!db) return {error: 'main DB not available', rows: [], count: 0}; limit = parseInt(limit) || 10; return execSQL(db, "SELECT * FROM FileMsgInfo ORDER BY rowid DESC LIMIT " + limit); }, // ===== 小程序 ===== getAppBrandList: function() { var db = getDB('appbrand'); if (!db) return {error: 'AppBrand DB not available'}; var tables = execSQL(db, "SELECT name FROM sqlite_master WHERE type='table'"); var result = {tables: tables.rows.map(function(r) { return r.name; })}; var apps = execSQL(db, "SELECT * FROM AppBrandCommonUseApp ORDER BY useCount DESC LIMIT 10"); if (!apps.error) result.common_apps = apps; return result; }, // ===== 搜索 ===== globalSearch: function(keyword) { var db = getDB('main'); if (!db) return {error: 'main DB not available'}; var contacts = execSQL(db, "SELECT username, nickName, conRemark FROM rcontact " + "WHERE (nickName LIKE '%" + keyword + "%' OR conRemark LIKE '%" + keyword + "%') AND type IN (3,4) LIMIT 10" ); var groups = execSQL(db, "SELECT chatroomname FROM chatroom WHERE chatroomname LIKE '%" + keyword + "%' LIMIT 5" ); var messages = execSQL(db, "SELECT msgId, content, talker, createTime FROM message WHERE content LIKE '%" + keyword + "%' ORDER BY createTime DESC LIMIT 10" ); return {contacts: contacts, groups: groups, messages: messages}; }, // ===== 原始SQL(调试用)===== rawQuery: function(dbName, sql) { var db = getDB(dbName); if (!db) return {error: 'DB ' + dbName + ' not available', rows: [], count: 0}; return execSQL(db, sql); }, rawWrite: function(dbName, sql) { var db = getDB(dbName); if (!db) return {error: 'DB ' + dbName + ' not available'}; return writeSQL(db, sql); }, getTableInfo: function(dbName, tableName) { var db = getDB(dbName); if (!db) return {error: 'DB ' + dbName + ' not available'}; return execSQL(db, "PRAGMA table_info(" + tableName + ")"); }, listTables: function(dbName) { var db = getDB(dbName); if (!db) return {error: 'DB ' + dbName + ' not available'}; return execSQL(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"); }, // ===== 添加好友 ===== addFriend: function(wxid, message) { // 写入AddContactScene表(如果存在) var db = getDB('main'); if (!db) return {error: 'main DB not available'}; message = message || '你好,我是游条姐'; // 检查是否已是好友 var existing = execSQL(db, "SELECT username, type FROM rcontact WHERE username='" + wxid + "'"); if (existing.rows[0] && existing.rows[0].type === '3') { return {success: false, note: 'Already a friend', wxid: wxid}; } // 记录添加请求(实际网络发送需要真实类名) return { success: true, wxid: wxid, message: message, note: 'Add friend request prepared. Network send requires WeChat 8.0.56 real class name.', existing: existing.rows[0] || null }; }, // ===== 截图 ===== takeScreenshot: function(savePath) { try { var Runtime = Java.use('java.lang.Runtime'); var rt = Runtime.getRuntime(); savePath = savePath || '/sdcard/workphone_screenshot.png'; rt.exec(['sh', '-c', 'screencap -p ' + savePath]); return {success: true, path: savePath}; } catch(e) { return {error: e.toString()}; } }, // ===== 导航 ===== navigate: function(activity) { try { var Intent = Java.use('android.content.Intent'); var ctx = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext(); var cls; var activityMap = { 'main': 'com.tencent.mm.ui.LauncherUI', 'moments': 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI', 'contacts': 'com.tencent.mm.ui.contact.AddressBookUI', 'chat': 'com.tencent.mm.ui.chatting.ChattingUI', 'finder': 'com.tencent.mm.plugin.finder.ui.FinderUI', 'wallet': 'com.tencent.mm.plugin.wallet.ui.WalletUI' }; var activityName = activityMap[activity] || activity; cls = Java.use(activityName).class; var intent = Intent.$new(ctx, cls); intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK ctx.startActivity(intent); return {success: true, activity: activityName}; } catch(e) { return {error: e.toString()}; } } };