├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── main.js
├── package-lock.json
├── package.json
├── project.json
├── src
├── constant.js
├── control-map.js
├── details-op.js
├── get-location-image.js
├── get-location.js
├── get-ocr-location.js
├── get-share-link.js
├── get-some-can-see.js
├── save-media.js
├── save-to-db.js
├── start.js
├── transform-date.js
└── utils.js
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | test
4 | main.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | },
5 | extends: 'airbnb-base',
6 | globals: {
7 | Atomics: 'readonly',
8 | SharedArrayBuffer: 'readonly',
9 | },
10 | parserOptions: {
11 | ecmaVersion: 2018,
12 | ecmaFeatures: {
13 | jsx: true,
14 | },
15 | },
16 | rules: {
17 | 'no-console': 'off',
18 | 'no-undef': 'off',
19 | 'prefer-destructuring': 'off',
20 | 'no-plusplus': 'off',
21 | 'no-loop-func': 'off',
22 | 'no-alert': 'off'
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | test
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # co-wechat-life-export
2 |
3 | 一个无需 root,用于导出朋友圈内容的 [Auto.js Pro](https://pro.autojs.org) 脚本项目。目前仅适用于微信最新版 7.0.4。
4 |
5 |
6 |
7 |
8 |
9 | 
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'ui';
2 |
3 | const start = require('./dist/start.js');
4 | const db = require('./dist/save-to-db.js');
5 | const constant = require('./dist/constant.js');
6 |
7 | console.setGlobalLogConfig({
8 | file: constant.appDir + '/log.txt'
9 | });
10 |
11 | ui.layout(
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | // var items = [
44 | // {runSeq: 1, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'},
45 | // {runSeq: 2, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'},
46 | // {runSeq: 3, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'},
47 | // {runSeq: 4, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'},
48 | // {runSeq: 5, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'},
49 | // {runSeq: 6, lifeStartTime: "2019/5/14 14:17", lifeEndTime: "2019/5/14 14:19", runStartTime: "2019/5/14 14:20", runEndTime: "2019/5/14 14:24", lifeCount: '4'}
50 | // ];
51 | const items = db.getHistory();
52 |
53 | ui.list.setDataSource(items);
54 |
55 | (function initForm() {
56 | const st = new Date('2011/1/21');
57 | const et = new Date();
58 | ui.sy.setText(st.getFullYear() + '');
59 | ui.sm.setText((st.getMonth() + 1) + '');
60 | ui.sd.setText(st.getDate() + '');
61 | ui.sh.setText(st.getHours() + '');
62 | ui.sm2.setText(st.getMinutes() + '');
63 |
64 | ui.ey.setText(et.getFullYear() + '');
65 | ui.em.setText((et.getMonth() + 1) + '');
66 | ui.ed.setText(et.getDate() + '');
67 | ui.eh.setText(et.getHours() + '');
68 | ui.em2.setText(et.getMinutes() + '');
69 |
70 | })();
71 |
72 | ui.startButton.click(() => {
73 | const st = new Date(ui.sy.getText() + "/" + ui.sm.getText() + "/" + ui.sd.getText() + " " + ui.sh.getText() + ":" + ui.sm2.getText()).getTime();
74 | const et = new Date(ui.ey.getText() + "/" + ui.em.getText() + "/" + ui.ed.getText() + " " + ui.eh.getText() + ":" + ui.em2.getText()).getTime();
75 | if (isNaN(st)) {
76 | toastLog('起始时间填写错误')
77 | return;
78 | }
79 | if (isNaN(et)) {
80 | toastLog('结束时间填写错误')
81 | return;
82 | }
83 | threads.start(start(st, et));
84 | });
85 |
86 | ui.list.on("item_bind", function(itemView, itemHolder){
87 | itemView.deleteItem.on("click", function(){
88 | confirm("确定删除吗").then(value=>{
89 | //当点击确定后会执行这里, value为true或false, 表示点击"确定"或"取消"
90 | if (value) {
91 | let item = itemHolder.item;
92 | const deleteResult = db.deleteHistory(item.runSeq);
93 | if (deleteResult > 0) {
94 | items.splice(itemHolder.position, 1);
95 | toastLog('删除成功');
96 | } else {
97 | toastLog('删除失败');
98 | }
99 | }
100 | });
101 | });
102 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "co-wechat-life-export",
3 | "version": "1.0.0",
4 | "description": "朋友圈导出",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "webpack --watch --config webpack.config.js --mode=development",
8 | "build": "webpack --config webpack.config.js --mode=production"
9 | },
10 | "author": "Coande",
11 | "devDependencies": {
12 | "@babel/core": "^7.4.4",
13 | "@babel/preset-env": "^7.4.4",
14 | "babel-loader": "^8.0.5",
15 | "eslint": "^5.16.0",
16 | "eslint-config-airbnb-base": "^13.1.0",
17 | "eslint-loader": "^2.1.2",
18 | "eslint-plugin-import": "^2.17.2",
19 | "webpack": "^4.31.0",
20 | "webpack-cli": "^3.3.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets": [],
3 | "build": {
4 | "build_number": 0,
5 | "build_time": 0,
6 | "release": false
7 | },
8 | "encryptLevel": 0,
9 | "useFeatures": [],
10 | "launchConfig": {
11 | "displaySplash": true,
12 | "hideLogs": false,
13 | "splashText": "Powered by Auto.js Pro",
14 | "stableMode": false
15 | },
16 | "name": "朋友圈导出",
17 | "mainScriptFile": "main.js",
18 | "ignore": [
19 | "build",
20 | "node_modules",
21 | "src",
22 | "test",
23 | ".eslintignore",
24 | ".eslintrc.js",
25 | ".gitignore",
26 | "package-lock.json",
27 | "package.json",
28 | "README.md",
29 | "webpack.config.js"
30 | ],
31 | "optimization": {
32 | "removeOpenCv": false,
33 | "unusedResources": false
34 | },
35 | "packageName": "com.e12e.co.wechat.life.export",
36 | "scripts": {},
37 | "versionName": "1.1.0",
38 | "versionCode": 2
39 | }
--------------------------------------------------------------------------------
/src/constant.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | wechatMediaDir: '/sdcard/tencent/MicroMsg/WeiXin/',
3 | appDir: '/sdcard/com.e12e.co.wechat.life.export/',
4 | };
5 |
--------------------------------------------------------------------------------
/src/control-map.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // 朋友圈列表
3 | listViewId: 'eii',
4 | // 朋友圈 item 城市名称
5 | city: 'ee',
6 | // listItemId: 'm5',
7 | // 图片类 左侧图片图标
8 | pictureIcon: 'eh2',
9 | // 图片类 左侧图标 右侧文字,包裹容器
10 | // pictureWrapper: 'ekp',
11 | // 分享类 链接上面一行文字
12 | shareComment: 'ld',
13 | // // 分享类 链接左边 icon
14 | // shareIcon: 'd11',
15 | // 分享类 链接右边文字
16 | shareText: 'd13',
17 | // 分享类 上面一行评论,下面分享内容,包裹容器
18 | shareWrapper: 'a7z',
19 | // 图片浏览界面 右下角赞数和评论数图标包裹容器
20 | countWrapper: 'ej1',
21 | // 详情界面 抱歉,无法发送
22 | failText: 'ehk',
23 | // 详情界面 地点
24 | locationText: 'ehx',
25 | // 详情界面 日期
26 | dateText: 'ehz',
27 | // 详情界面 部分人可见
28 | someCanSee: 'ei2',
29 | // 详情界面 来源
30 | fromText: 'ei3',
31 | // 详情界面 list容器
32 | detailsWrapper: 'ehm',
33 | // 详情界面 视频容器
34 | videoWrapper: 'ap9',
35 |
36 | // 视频浏览 视频容器
37 | videoPlayerWrapper: 'aey',
38 | // 图片浏览 图片容器
39 | picturePlayerWrapper: 'ejn',
40 |
41 | // 网页浏览时菜单
42 | broswerMoreButton: 'kj',
43 |
44 | // 网页浏览时菜单 菜单条目容器
45 | browserMenuWrapper: 'd97',
46 |
47 | // 列表底部横线
48 | bottomLine: 'ai5',
49 |
50 | // 部分好友可见 好友列表
51 | someCanSeeList: 'n3',
52 | // 部分好友可见 好友名称
53 | someCanSeeItemName: 'nr',
54 | };
55 |
--------------------------------------------------------------------------------
/src/details-op.js:
--------------------------------------------------------------------------------
1 | const controlMap = require('./control-map.js');
2 | const saveMedia = require('./save-media.js');
3 | const getShareLink = require('./get-share-link.js');
4 | const locationObj = require('./get-location-image');
5 | const getSomeCanSee = require('./get-some-can-see.js');
6 | const constant = require('./constant.js');
7 | const utils = require('./utils.js');
8 | const db = require('./save-to-db.js');
9 | const transformDate = require('./transform-date.js');
10 |
11 | // 滚动到顶部
12 | function scrollToFirstRow() {
13 | const firstChild = id(controlMap.detailsWrapper)
14 | .findOnce()
15 | .children()
16 | .get(0);
17 | if (firstChild.row() !== 0) {
18 | scrollUp();
19 | sleep(500);
20 | scrollToFirstRow();
21 | }
22 | }
23 |
24 | function backToList(lifeType) {
25 | if (lifeType === 'media') {
26 | back();
27 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsGalleryUI');
28 | }
29 | back();
30 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsUserUI');
31 | }
32 |
33 | module.exports = (extInfo) => {
34 | let lifeType = '';
35 | // 判断是否图片浏览界面
36 | const countWrapper = id(controlMap.countWrapper).findOnce();
37 | if (countWrapper) {
38 | // console.log('当前是图片/视频浏览界面');
39 | // 是图片浏览界面,需要点击才能进入详情界面
40 | countWrapper.click();
41 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsCommentDetailUI');
42 | lifeType = 'media';
43 | }
44 |
45 | sleep(2000);
46 |
47 | // 有可能先公开后设置私密,提示会在底部
48 | const privateSee = text('私密照片不能评论').findOnce();
49 | let isPrivateSee = false;
50 | if (privateSee) {
51 | isPrivateSee = true;
52 | console.log('当前内容仅自己可见');
53 | }
54 |
55 | scrollToFirstRow();
56 |
57 | const dateText = id(controlMap.dateText).findOnce();
58 | if (!dateText) {
59 | toastLog('找不到日期控件');
60 | throw new Error('找不到日期控件');
61 | }
62 |
63 | const dateTextString = dateText.text();
64 | const transformDateRes = transformDate(dateTextString);
65 | console.log('时间:==', dateTextString, '==', transformDateRes);
66 |
67 | if (transformDateRes < extInfo.filterLifeStartTime
68 | || transformDateRes > extInfo.filterLifeEndTime) {
69 | if (transformDateRes < extInfo.filterLifeStartTime) {
70 | return {
71 | isFinish: true,
72 | };
73 | }
74 | backToList(lifeType);
75 | return {};
76 | }
77 |
78 | // 保证能够截图到地址
79 | const localtionText = id(controlMap.locationText).findOnce();
80 | if (
81 | localtionText
82 | && (localtionText.bounds().top > device.height
83 | || localtionText.bounds().height() < 0
84 | || localtionText.bounds().height() === 0)
85 | ) {
86 | scrollUp();
87 | sleep(500);
88 | }
89 |
90 | const failText = id(controlMap.failText).findOnce();
91 | if (failText) {
92 | db.insert({
93 | row: extInfo.row,
94 | createdAt: new Date().getTime(),
95 | });
96 | backToList(lifeType);
97 | return {};
98 | }
99 |
100 | // 开始获取数据
101 | console.log('===================================>>');
102 | const shareComment = id(controlMap.shareComment).findOnce();
103 | let shareCommentString = '';
104 | if (shareComment) {
105 | shareCommentString = shareComment.text();
106 | console.log('文字内容:', shareCommentString);
107 | }
108 | const shareText = id(controlMap.shareText).findOnce();
109 | let shareTextString = '';
110 | if (shareText) {
111 | shareTextString = shareText.text();
112 | console.log('分享名称:', shareTextString);
113 | }
114 |
115 | // 获取不到地点信息
116 | const locationImage = locationObj.get();
117 | // const locationTextCom = id(controlMap.locationText).findOnce();
118 | // let locationImage = '';
119 | // let locationDetail = '';
120 | // if (locationTextCom) {
121 | // // 地点比较难获取
122 | // // locationImage = locationTextCom.text();
123 | // // console.log('地点:', locationTextCom.text());
124 | // // 获取经纬度(需要借助谷歌地图com.google.android.apps.maps)
125 | // // locationDetail = getLocation();
126 | // }
127 |
128 | const fromText = id(controlMap.fromText).findOnce();
129 | let fromTextString = '';
130 | if (fromText) {
131 | fromTextString = fromText.text();
132 | console.log('来源:', fromText.text());
133 | }
134 |
135 | let shareLink = '';
136 | if (fromTextString !== '收藏') {
137 | // 点击进入分享内容详情
138 | if (shareText) {
139 | utils.superClick(shareText);
140 | shareLink = getShareLink();
141 | }
142 | if (shareLink) {
143 | console.log('分享链接地址:', shareLink);
144 | }
145 | }
146 |
147 | const someCanSee = getSomeCanSee();
148 | if (someCanSee) {
149 | console.log('当前内容限制了可见性');
150 | }
151 |
152 | const pictureFileNameList = saveMedia.savePicture();
153 | if (pictureFileNameList.length) {
154 | console.log('保存图片的文件名为:', pictureFileNameList);
155 | // 移动文件
156 | files.ensureDir(`${constant.appDir}/media/picture/`);
157 | pictureFileNameList.forEach((name) => {
158 | files.move(`${constant.wechatMediaDir}/${name}`, `${constant.appDir}/media/picture/${name}`);
159 | });
160 | }
161 |
162 | const videoFileName = saveMedia.saveVideo();
163 | if (videoFileName) {
164 | console.log('保存视频的文件名为:', videoFileName);
165 | files.ensureDir(`${constant.appDir}/media/video/`);
166 | files.move(
167 | `${constant.wechatMediaDir}/${videoFileName}`,
168 | `${constant.appDir}/media/video/${videoFileName}`,
169 | );
170 | }
171 |
172 | // 保存到数据库
173 | db.insert({
174 | row: extInfo.row,
175 | desc: shareCommentString,
176 | picture: pictureFileNameList.length ? pictureFileNameList.join(',') : '',
177 | video: videoFileName,
178 | shareTitle: shareTextString,
179 | shareLink,
180 | shareFrom: fromTextString,
181 | sendTime: transformDate(dateTextString),
182 | sendLocation: '',
183 | sendLocationShow: locationImage ? locationImage.fileName : '',
184 | sendLocationOcr: locationImage ? locationImage.ocrText : '',
185 | someCanSeeType: someCanSee ? someCanSee.type : '',
186 | someCanSeeList: someCanSee ? JSON.stringify(someCanSee.list) : '',
187 | isPrivate: isPrivateSee ? 1 : 0,
188 | createdAt: new Date().getTime(),
189 | });
190 |
191 | console.log('<<===================================');
192 |
193 | backToList(lifeType);
194 | return {};
195 | };
196 |
--------------------------------------------------------------------------------
/src/get-location-image.js:
--------------------------------------------------------------------------------
1 | const constant = require('./constant.js');
2 | const getOcrLocation = require('./get-ocr-location.js');
3 | const controlMap = require('./control-map.js');
4 |
5 | module.exports = (() => ({
6 | get() {
7 | const locationCom = id(controlMap.locationText).findOnce();
8 | if (locationCom) {
9 | // 截图
10 | const img = captureScreen();
11 | const locationComRect = locationCom.bounds();
12 |
13 | const clip = images.clip(
14 | img,
15 | locationComRect.left,
16 | locationComRect.top,
17 | locationComRect.width(),
18 | locationComRect.height(),
19 | );
20 | const fileName = `location-${new Date().getTime()}.png`;
21 | const targetPath = `${constant.appDir}/location/${fileName}`;
22 | files.ensureDir(targetPath);
23 | const ocrText = getOcrLocation(clip);
24 | images.save(clip, targetPath);
25 | clip.recycle();
26 | return {
27 | fileName,
28 | ocrText,
29 | };
30 | }
31 | return '';
32 | },
33 | }))();
34 |
--------------------------------------------------------------------------------
/src/get-location.js:
--------------------------------------------------------------------------------
1 | const controlMap = require('./control-map.js');
2 |
3 | module.exports = () => {
4 | // 获取经纬度(需要借助谷歌地图com.google.android.apps.maps)
5 | const googleMapsName = getAppName('com.tencent.mobileqq');
6 | if (googleMapsName) {
7 | const locationTextCom = id(controlMap.locationText).findOnce();
8 | locationTextCom.click();
9 | // TODO: 涉及到网页操作,有空再搞
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/get-ocr-location.js:
--------------------------------------------------------------------------------
1 | module.exports = (() => {
2 | const ocrPackage = 'org.autojs.plugin.ocr';
3 | const ocrPackageName = getAppName(ocrPackage);
4 | let ocr;
5 | if (ocrPackageName) {
6 | const OCR = plugins.load(ocrPackage);
7 | ocr = new OCR();
8 | }
9 | return (img) => {
10 | if (ocr) {
11 | const res = ocr.ocrImage(img);
12 | return res.text;
13 | }
14 | return '';
15 | };
16 | })();
17 |
--------------------------------------------------------------------------------
/src/get-share-link.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils.js');
2 | const controlMap = require('./control-map.js');
3 |
4 | module.exports = () => {
5 | id(controlMap.broswerMoreButton).waitFor();
6 | sleep(1500);
7 | // 可能需要微信登录
8 | if (text('允许').findOnce()) {
9 | text('允许').findOnce().click();
10 | }
11 | const moreOP = id(controlMap.broswerMoreButton)
12 | .untilFind()
13 | .get(0);
14 | const clickResult = utils.superClick(moreOP);
15 | if (!clickResult) {
16 | console.log('点击结果:', clickResult);
17 | }
18 | id(controlMap.browserMenuWrapper).waitFor();
19 |
20 | let shareLink = '';
21 | sleep(1000);
22 | if (!text('复制链接').findOnce()) {
23 | // 页面打不开了
24 | shareLink = '网页无法继续访问';
25 | } else {
26 | text('复制链接').waitFor();
27 | sleep(1500);
28 | const copyButton = text('复制链接')
29 | .untilFind()
30 | .get(0);
31 | utils.superClick(copyButton);
32 | shareLink = getClip();
33 | // 有可能部分选项延迟加载,导致再次弹出菜单
34 | // const copyButton2 = text('复制链接').findOnce();
35 | // if (copyButton2) {
36 | // back();
37 | // sleep(500);
38 | // }
39 | // // 点一次屏幕以保证清掉菜单
40 | // click(device.width / 2, device.height / 4);
41 | // sleep(500);
42 | }
43 | back();
44 | sleep(1000);
45 |
46 | if (currentActivity() !== 'com.tencent.mm.plugin.sns.ui.SnsCommentDetailUI') {
47 | back();
48 | sleep(1000);
49 | }
50 | return shareLink;
51 | };
52 |
--------------------------------------------------------------------------------
/src/get-some-can-see.js:
--------------------------------------------------------------------------------
1 | const controlMap = require('./control-map.js');
2 |
3 | module.exports = () => {
4 | const someCanSee = id(controlMap.someCanSee).findOnce();
5 | if (someCanSee) {
6 | someCanSee.click();
7 | const titleCom = textMatches(/^该照片.?可见的朋友$/)
8 | .untilFind()
9 | .get(0);
10 |
11 | const itemNameList = [];
12 | let reachEndTimes = 0;
13 | let lastRow;
14 | // 连续三次到底判断为真的到底了
15 | while (reachEndTimes !== 3) {
16 | const someCanSeeListCom = id(controlMap.someCanSeeList)
17 | .untilFind()
18 | .get(0);
19 | const itemList = someCanSeeListCom.children();
20 | // eslint-disable-next-line no-loop-func
21 | let hasNew = false;
22 | itemList.forEach((item) => {
23 | const currentRow = item.row();
24 | if (currentRow < lastRow || currentRow === lastRow) {
25 | return;
26 | }
27 | hasNew = true;
28 | lastRow = currentRow;
29 | const itemNameCom = item.findOne(id(controlMap.someCanSeeItemName));
30 | itemNameList.push(itemNameCom.text());
31 | });
32 | if (!hasNew) {
33 | reachEndTimes++;
34 | } else {
35 | reachEndTimes = 0;
36 | }
37 | scrollDown();
38 | sleep(500);
39 | }
40 | if (titleCom) {
41 | back();
42 | sleep(500);
43 | return {
44 | type: titleCom.text(),
45 | list: itemNameList,
46 | };
47 | }
48 | }
49 | return '';
50 | };
51 |
--------------------------------------------------------------------------------
/src/save-media.js:
--------------------------------------------------------------------------------
1 | const constant = require('./constant.js');
2 | const controlMap = require('./control-map.js');
3 | const utils = require('./utils.js');
4 |
5 | function doSaveMedia(mediaCom, mediaType) {
6 | let currentLatestFileName = '';
7 | mediaCom.click();
8 | if (mediaType === 'picture') {
9 | // console.log('等待图片浏览activity');
10 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsBrowseUI');
11 | // id(controlMap.picturePlayerWrapper).waitFor();
12 | } else {
13 | // console.log('等待视频浏览activity');
14 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsOnlineVideoActivity');
15 | // id(controlMap.videoPlayerWrapper).waitFor();
16 | }
17 | sleep(1000);
18 | const longClickResult = press(device.width / 2, device.height / 2, 600);
19 | if (!longClickResult) {
20 | console.error('长按结果:', longClickResult);
21 | }
22 | // 先扫一遍文件夹的内容
23 | const shellResult = shell(`ls -t ${constant.wechatMediaDir}`);
24 | const latestFileName = shellResult.result.split('\n')[0];
25 | const saveMediaMenuName = mediaType === 'picture' ? '保存图片' : '保存视频';
26 | const savePicCom = text(saveMediaMenuName)
27 | .untilFind()
28 | .get(0);
29 | const clickResult = utils.superClick(savePicCom);
30 | if (!clickResult) {
31 | console.log('点击结果:', clickResult);
32 | }
33 | let saveFlag = false;
34 | while (!saveFlag) {
35 | // 再扫一遍以确定是否保存成功
36 | const shellResult2 = shell(`ls -t ${constant.wechatMediaDir}`);
37 | currentLatestFileName = shellResult2.result.split('\n')[0];
38 | if (currentLatestFileName !== latestFileName) {
39 | saveFlag = true;
40 | }
41 | sleep(500);
42 | }
43 | sleep(2500);
44 | if (text('识别图中二维码').findOnce()) {
45 | back();
46 | sleep(500);
47 | }
48 | back();
49 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsCommentDetailUI');
50 | return currentLatestFileName;
51 | }
52 |
53 | module.exports = {
54 | savePicture() {
55 | const saveFileNameList = [];
56 | const pictureList = desc('图片').find();
57 | if (!pictureList.empty()) {
58 | pictureList.forEach((picture) => {
59 | const saveFileName = doSaveMedia(picture, 'picture');
60 | saveFileNameList.push(saveFileName);
61 | });
62 | }
63 | return saveFileNameList;
64 | },
65 | saveVideo() {
66 | let saveFileName = '';
67 | const videoCom = id(controlMap.videoWrapper).findOnce();
68 | if (videoCom) {
69 | saveFileName = doSaveMedia(videoCom, 'video');
70 | }
71 | return saveFileName;
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/src/save-to-db.js:
--------------------------------------------------------------------------------
1 | const constant = require('./constant.js');
2 |
3 | function numberPadding(n) {
4 | if ((`${n}`).length < 2) {
5 | return `0${n}`;
6 | }
7 | return n;
8 | }
9 |
10 | const dateFormat = (timestamp) => {
11 | const date = new Date(parseInt(timestamp, 10));
12 | return `${date.getFullYear()}/${numberPadding(date.getMonth() + 1)}/${numberPadding(date.getDate())} ${numberPadding(date.getHours())}:${numberPadding(date.getMinutes())}`;
13 | };
14 |
15 | const dbWrapper = (() => {
16 | // console.log('打开/创建数据库实例');
17 | const db = sqlite.open(
18 | `${constant.appDir}/data.db`,
19 | { version: 1 },
20 | {
21 | onOpen(dbInstant) {
22 | dbInstant.execSQL(
23 | // eslint-disable-next-line no-multi-str
24 | '\
25 | CREATE TABLE IF NOT EXISTS wechatLife(\
26 | `id` INTEGER PRIMARY KEY AUTOINCREMENT,\
27 | `row` INTEGER NOT NULL,\
28 | `desc` TEXT,\
29 | `picture` TEXT,\
30 | `video` TEXT,\
31 | `shareTitle` TEXT,\
32 | `shareLink` TEXT,\
33 | `shareFrom` TEXT,\
34 | `sendTime` TEXT,\
35 | `sendLocation` TEXT,\
36 | `sendLocationShow` TEXT,\
37 | `sendLocationOcr` TEXT,\
38 | `someCanSeeType` TEXT,\
39 | `someCanSeeList` TEXT,\
40 | `isPrivate` INTEGER,\
41 | `runSeq` INTEGER,\
42 | `createdAt` TEXT\
43 | )\
44 | ',
45 | );
46 | },
47 | },
48 | );
49 | // 获取 runSeq,进行递增
50 | const maxRunSeq = db.rawQuery('SELECT MAX(runSeq) as maxRunSeq FROM wechatLife', null).single().maxRunSeq;
51 | let currentRunSeq;
52 | if (!Number.isNaN(maxRunSeq) && maxRunSeq) {
53 | currentRunSeq = parseInt(maxRunSeq, 10) + 1;
54 | } else {
55 | currentRunSeq = 1;
56 | }
57 | return {
58 | getDB() {
59 | return db;
60 | },
61 | insert(data) {
62 | // const insertResult = db.insert('wechatLife', {
63 | // row: 1,
64 | // desc: '测试测试',
65 | // picture: '["a.png", "b.png"]',
66 | // video: 'c.mp4',
67 | // shareTitle: '测试链接标题',
68 | // shareLink: 'https://e12e.com',
69 | // shareFrom: 'co',
70 | // sendTime: '2019年5月10日',
71 | // sendLocation: '珠海',
72 | // sendLocationShow: 'd.png',
73 | // isSomeCanSee: 1,
74 | // isPrivate: 0,
75 | // createdAt: '0000000000'
76 | // });
77 | // eslint-disable-next-line no-param-reassign
78 | data.runSeq = currentRunSeq;
79 | console.info('将要插入:\n', data);
80 | const insertResult = db.insert('wechatLife', data);
81 | console.log('insertResult:', insertResult);
82 | },
83 | getHistory() {
84 | const cursor = db.rawQuery('SELECT MIN(sendTime) as lifeStartTime,MAX(sendTime) as lifeEndTime,MIN(createdAt) as runStartTime,MAX(createdAt) as runEndTime,runSeq,COUNT(runSeq) as lifeCount FROM wechatLife GROUP BY runSeq', null);
85 | const resultList = [];
86 | while (cursor.moveToNext()) {
87 | const res = cursor.pick();
88 | resultList.push({
89 | runSeq: res.runSeq,
90 | lifeStartTime: dateFormat(res.lifeStartTime),
91 | lifeEndTime: dateFormat(res.lifeEndTime),
92 | runStartTime: dateFormat(res.runStartTime),
93 | runEndTime: dateFormat(res.runEndTime),
94 | lifeCount: res.lifeCount,
95 | });
96 | }
97 | // 记得关闭cursor
98 | cursor.close();
99 | return resultList;
100 | },
101 | deleteHistory(runSeq) {
102 | const res = db.delete('wechatLife', 'runSeq = ?', [runSeq]);
103 | console.log('删除结果:', res);
104 | return res;
105 | },
106 | close() {
107 | db.close();
108 | },
109 | };
110 | })();
111 |
112 | module.exports = dbWrapper;
113 |
114 | // const insertResult = dbWrapper.insert({
115 | // row: 1,
116 | // desc: '测试测试',
117 | // picture: '["a.png", "b.png"]',
118 | // video: 'c.mp4',
119 | // shareTitle: '测试链接标题',
120 | // shareLink: 'https://e12e.com',
121 | // shareFrom: 'co',
122 | // sendTime: '2019年5月10日',
123 | // sendLocation: '珠海',
124 | // sendLocationShow: 'd.png',
125 | // isSomeCanSee: 1,
126 | // isPrivate: 0,
127 | // createdAt: '0000000000'
128 | // });
129 |
--------------------------------------------------------------------------------
/src/start.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils.js');
2 | const detailsOP = require('./details-op.js');
3 | const controlMap = require('./control-map.js');
4 | const db = require('./save-to-db.js');
5 | const constant = require('./constant.js');
6 |
7 | function getIsReachEnd() {
8 | if (id(controlMap.bottomLine).findOnce()) {
9 | return true;
10 | }
11 | return false;
12 | }
13 |
14 | module.exports = (filterLifeStartTime, filterLifeEndTime) => () => {
15 | // 请求截图
16 | if (!requestScreenCapture()) {
17 | toastLog('请求截图失败,需要截图权限');
18 | exit();
19 | }
20 |
21 | try {
22 | auto();
23 | } catch (error) {
24 | toastLog('请先打开无障碍再试');
25 | exit();
26 | }
27 |
28 | ui.run(() => {
29 | dialogs.alert('提示', '请保持当前软件后台运行,并切换微信,打开朋友圈个人页面');
30 | });
31 |
32 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsUserUI');
33 |
34 | let currentIndex;
35 | let reachEndType = -1;
36 | while (reachEndType < 1) {
37 | // 朋友圈的每一个内容条目
38 | const list = id(controlMap.listViewId)
39 | .findOnce()
40 | .children();
41 |
42 | // eslint-disable-next-line no-loop-func
43 | list.forEach((item) => {
44 | if (reachEndType > 1 || reachEndType === 1) {
45 | return;
46 | }
47 | const currentRow = item.row();
48 | if (currentRow < currentIndex || currentRow === currentIndex) {
49 | // console.log('当前item为重复item:', currentRow);
50 | return;
51 | }
52 | currentIndex = currentRow;
53 | const pictureIcon = item.findOne(id(controlMap.pictureIcon));
54 | const shareWrapper = item.findOne(id(controlMap.shareWrapper));
55 | const foundObj = pictureIcon || shareWrapper;
56 | if (foundObj) {
57 | const clickResult = utils.superClick(foundObj);
58 |
59 | if (clickResult) {
60 | if (pictureIcon) {
61 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsGalleryUI');
62 | } else {
63 | waitForActivity('com.tencent.mm.plugin.sns.ui.SnsCommentDetailUI');
64 | }
65 | const res = detailsOP({
66 | row: currentRow,
67 | filterLifeStartTime,
68 | filterLifeEndTime,
69 | });
70 | if (res.isFinish) {
71 | reachEndType = 1;
72 | }
73 | } else {
74 | console.error('第', currentIndex, '个item点击结果:', clickResult);
75 | }
76 | } else {
77 | console.log('第', currentIndex, '个item不是目标item');
78 | }
79 | });
80 |
81 | const lastItem = list.get(list.size() - 1);
82 | sleep(500);
83 | swipe(0, lastItem.bounds().top, 0, 0, 2000);
84 | sleep(500);
85 | if (getIsReachEnd()) {
86 | reachEndType += 1;
87 | }
88 | }
89 | db.close();
90 | ui.run(() => {
91 | alert('提示', `恭喜,导出完毕!导出的数据位于:${constant.appDir}`);
92 | toastLog(`恭喜,导出完毕!导出的数据位于:${constant.appDir}`);
93 | });
94 | };
95 |
--------------------------------------------------------------------------------
/src/transform-date.js:
--------------------------------------------------------------------------------
1 | module.exports = (dateString) => {
2 | // 22:02
3 | const reg1 = /^\s*(\d+):(\d+)\s*$/;
4 | const execRes1 = reg1.exec(dateString);
5 | // 昨天 17:18
6 | const reg2 = /^\s*昨天\s(\d+):(\d+)\s*$/;
7 | const execRes2 = reg2.exec(dateString);
8 | // 2019年5月11日 07:11
9 | const reg3 = /^\s*(\d+)年(\d+)月(\d+)日 (\d+):(\d+)\s*$/;
10 | const execRes3 = reg3.exec(dateString);
11 | const now = new Date();
12 | const year = now.getFullYear();
13 | const month = now.getMonth() + 1;
14 | const date = now.getDate();
15 | if (execRes1) {
16 | return new Date(`${year}/${month}/${date} ${execRes1[1]}:${execRes1[2]}`).getTime();
17 | } if (execRes2) {
18 | const yesterday = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
19 | const yesterdayYear = yesterday.getFullYear();
20 | const yesterdayMonth = yesterday.getMonth() + 1;
21 | const yesterdayDate = yesterday.getDate();
22 | return new Date(`${yesterdayYear}/${yesterdayMonth}/${yesterdayDate} ${execRes2[1]}:${execRes2[2]}`).getTime();
23 | }
24 | return new Date(`${execRes3[1]}/${execRes3[2]}/${execRes3[3]} ${execRes3[4]}:${execRes3[5]}`).getTime();
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /**
3 | * 找到指定属性的祖先节点
4 | * @param {Object} obj 给定的 UiObject
5 | * @param {Object} attr 需要查找的存在特定属性的对象
6 | * @return {Object} 找到的祖先节点 UiObject
7 | */
8 | findParentByAttr(obj, attr) {
9 | const p = obj.parent();
10 | if (!p) {
11 | return null;
12 | }
13 | const keys = Object.keys(attr);
14 | let diffFlag = false;
15 | keys.forEach((key) => {
16 | if (attr[key] !== p[key]()) {
17 | diffFlag = true;
18 | }
19 | });
20 | if (diffFlag) {
21 | return this.findParentByAttr(p, attr);
22 | }
23 | return p;
24 | },
25 | superClick(obj) {
26 | if (obj.clickable()) {
27 | return obj.click();
28 | }
29 | const clickableParent = this.findParentByAttr(obj, {
30 | clickable: true,
31 | });
32 | return clickableParent.click();
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: {
5 | start: './src/start.js',
6 | 'save-to-db': './src/save-to-db.js',
7 | constant: './src/constant.js',
8 | },
9 | output: {
10 | path: path.resolve(__dirname, 'dist'),
11 | filename: '[name].js',
12 | libraryTarget: 'commonjs2',
13 | },
14 | module: {
15 | rules: [
16 | {
17 | enforce: 'pre',
18 | test: /\.js$/,
19 | exclude: /node_modules/,
20 | loader: 'eslint-loader',
21 | },
22 | {
23 | test: /\.m?js$/,
24 | exclude: /(node_modules|bower_components|main.js)/,
25 | use: {
26 | loader: 'babel-loader',
27 | options: {
28 | presets: ['@babel/preset-env'],
29 | },
30 | },
31 | },
32 | ],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------