235 Commits
Author SHA1 Message Date
Yuxin Wu ca9fc96b98 Update README 2026-01-09 20:06:18 +08:00
CK ZhangandYuxin Wu 30eef32ae6 Implement reply quote UI for HTML export (#107)
* render: improve reply quote UI

* render: address PR review comments

* res: avoid masking errors in img decode
2026-01-09 20:03:21 +08:00
CK ZhangandGitHub 11d60a7487 Fix Python 3.13 compatibility (#106)
* compat: support Python 3.13 (imghdr removal)

* compat: drop unnecessary future annotations import
2026-01-01 19:17:47 +08:00
Chang Qian (Fred)andGitHub 928b92ade1 To make it compatible with user (not userdebug) builds, use adb shell su instead of adb root whenever possible (#102) 2025-09-12 23:11:41 -07:00
643bdd8984 Improve stability of WS connection with WXGF decoder app (#101)
* In case TYPE_LINK is missing a title, use url as title to avoid aborting

* Retry WS connection on broken pipe

* Update render.py

* Update wxgf.py

---------

Co-authored-by: Yuxin Wu <[email protected]>
2025-07-05 19:02:00 -07:00
Chang Qian (Fred)andGitHub a38c12fe4b Fix dump-audio (#103) 2025-07-05 18:51:15 -07:00
Yuxin Wu a0d362d9f6 Move frida.md 2025-02-03 10:37:35 -08:00
Yuxin Wu eedaa9c9e0 support downloading avatar 2025-01-30 14:30:04 -08:00
Yuxin Wu e1a1b0b946 remove ensure_unicode 2025-01-30 12:05:48 -08:00
Yuxin Wu b271469b94 support wxgf emoji 2025-01-30 12:07:31 -08:00
Yuxin Wu c782325cfa improve rendering of various types 2025-01-29 01:47:42 -08:00
Yuxin Wu 837a9ef2f7 update builtin emojis 2025-01-28 23:47:52 -08:00
Yuxin Wu a508e29003 update readme 2025-01-28 19:24:29 -08:00
Yuxin Wu 83bbb0a1fd add android app 2025-01-28 19:00:47 -08:00
Yuxin Wu 1e754f229a add wxgf client code 2025-01-28 06:11:40 -08:00
Yuxin Wu 6d9e4a00a9 fix utf8 decode error 2025-01-22 04:25:01 -08:00
Yuxin Wu 20708ef755 update 2025-01-05 17:03:38 +08:00
JakeandGitHub cb76a41edc Update README.md (#99)
old link doesn't work anymore.
2024-08-13 16:33:02 -07:00
Yuxin WuandGitHub 4e7593f3b2 Update README.md 2021-12-13 16:46:57 -08:00
Yuxin Wu 3e86880913 update docs 2021-04-19 01:22:36 -07:00
52fb966dc5 🌈style(view): fix body scroll (#84)
Co-authored-by: lqzh <[email protected]>
2021-04-13 15:58:53 -07:00
lqzhgoodandGitHub a841e342cb Update requirements.txt (#82)
add `numpy` module
2021-04-13 00:19:28 -07:00
Yuxin Wu 5b7d293b06 update docs about sfs/avatar.index 2021-02-02 11:00:48 -08:00
Yuxin Wu 541f6aa5ee update 2021-01-07 19:12:20 -08:00
Yuxin Wu b818b6e2aa catch decryption error (#72) 2020-10-03 04:38:19 -07:00
Yuxin Wu 6f86622c93 use tempfile for audio decoding 2020-07-17 12:26:25 -07:00
Kai JiaandGitHub f61b9d59b7 Fix a typo in README (#66) 2020-07-06 10:11:34 -07:00
Yuxin Wu 78fffc6d33 update readme 2020-07-02 17:00:46 -07:00
Yuxin Wu 8b5a28c39c update readme 2020-07-01 01:26:15 -07:00
Yuxin Wu f2ba97117c better error message 2020-06-30 23:31:56 -07:00
Yuxin Wu 7eb7c60a81 remove "internal_emoji" 2020-06-30 23:16:19 -07:00
Yuxin Wu b12cd93f0d decrypt local emoji 2020-06-30 23:09:46 -07:00
Yuxin Wu e314b9d815 check md5 of local emoji as well 2020-06-29 11:21:07 -07:00
Yuxin Wu e46378aa25 use encrypturl if cdnurl does not produce correct md5 2020-06-29 11:07:44 -07:00
Yuxin Wu 64e5e5eb3f aes-decrypt emoji from encrypturl 2020-06-28 02:57:14 -07:00
Yuxin Wu 1f1a28e605 check for two avatar storage 2020-06-28 00:33:07 -07:00
Yuxin Wu eeba4b7e6c fix old avatar storage 2020-06-28 00:12:39 -07:00
Yuxin Wu f6acd0b0bf move common/ into wechat/ 2020-06-27 20:34:05 -07:00
Yuxin Wu 973993723b support both chatid and nickname (fix #51) 2020-06-27 20:04:34 -07:00
Yuxin Wu 55784bd1bf avoid assertion for type 49 (close #52) 2020-06-27 19:56:28 -07:00
Yuxin Wu 7571334eca update readme 2020-06-27 19:45:42 -07:00
Yuxin Wu bfb2d0c914 add missing file 2020-06-27 19:32:21 -07:00
Yuxin Wu aa9a1c9129 compatibility with an old database file 2020-06-27 19:00:43 -07:00
Yuxin Wu 86224a8151 update 2020-06-27 17:34:52 -07:00
Yuxin Wu 1395351755 support latest smileys 2020-06-27 17:31:35 -07:00
Yuxin Wu 628bc024c8 move libchat to legacy 2020-06-27 16:34:35 -07:00
Yuxin Wu d6d2aaea55 handle emoji thumbnail 2020-06-27 16:32:07 -07:00
Yuxin Wu 4dc13a16f7 add support for video file 2020-06-27 15:54:03 -07:00
Yuxin Wu 95826392df rewrite with python3 2020-06-27 15:21:17 -07:00
Yuxin Wu 503a43c953 update 2020-06-27 02:21:57 -07:00
Yuxin Wu 0f3fd34b54 update 2020-06-27 01:49:45 -07:00
Yuxin Wu 0f97ecc9f0 rewrite decryption logic 2020-06-27 01:45:15 -07:00
Yuxin Wu a82bd281a7 sqlcipher compatibilty 2020-06-26 19:01:18 -07:00
Yuxin Wu 249b2cf229 update in parser 2020-06-26 16:53:20 -07:00
Yuxin Wu 7fc6632907 remove my emails 2018-08-26 01:21:45 -07:00
Yuxin Wu e5240137ee update docs 2017-11-29 04:04:22 -08:00
Yuxin Wu ed21c3b630 update docs 2017-11-29 04:02:49 -08:00
Yuxin Wu 8e68c59893 use emoji cover for failures 2017-11-29 03:55:09 -08:00
Yuxin Wu b82bcc7673 fix avatar rendering 2017-11-29 03:26:40 -08:00
Yuxin Wu 5893e7c6e6 update docs 2017-11-29 02:33:15 -08:00
futurewrgandYuxin Wu 20ae09848b Add support for new avatar storage (#50)
* Add support for new avatar storage,modify the README and android-interact.sh

* Show the nickname for chatroom, fix the IOError in avatar.py

* style change
2017-11-29 01:34:51 -08:00
Yuxin WuandGitHub 9e776ca38e fix dependencies (fix #49) 2017-10-14 23:55:42 -07:00
Yuxin Wu f8ede162c0 use userid instead of nickname as identifiers everywhere. (#39) 2017-03-02 17:46:30 +08:00
Yuxin Wu b2abfa7309 update README (#39) 2017-03-02 12:09:30 +08:00
Yuxin Wu 9f1ae9363d bug fix on chatroom msg handling 2017-02-09 13:24:37 +08:00
Yuxin Wu 613f856662 update readme 2016-10-12 14:03:10 -04:00
Yuxin Wu 4ee06766fd bug fix 2016-10-12 13:53:45 -04:00
Yuxin Wu 06217dbfb8 bugfix 2016-10-12 13:53:11 -04:00
Yuxin WuandGitHub 748cccb745 Update README.md 2016-10-11 12:11:07 -04:00
Yuxin Wu 54dc3156dd update readme 2016-10-10 01:19:59 -04:00
Yuxin Wu c4c20633ea update readme 2016-10-10 01:19:21 -04:00
Yuxin Wu 9a45dfaf35 bug fix 2016-09-06 02:01:32 -04:00
Jakukyo FrielandYuxin Wu b4d374cbe2 Refine README: (#31)
* Add commands to install dependencies on debian.

* README: mention `requests` in dependencies.

Thanks @h82k to point out this in #29

* README: fix broken link.

* Improve instructions on uncompress emoji.cache.

See #29

* README: remove unnecessary unpack command.

> unpacking emoji should not be necessary.
> I think things should work without running emoji-cache-tool.py.
> That tool is mainly for me to see what's in the cache.

-- @ppwwyyxx in #29

* README: fix `dump-html.py` command line.

See #29
2016-07-26 16:56:38 -07:00
Yuxin Wu 29115b104f see if readme looks better 2016-07-02 22:54:39 -07:00
Yuxin Wu 3731443254 avoid using cover, and use compressed emoji.cache 2016-06-18 23:37:04 -07:00
Yuxin Wu 11081b2ba8 script improvements 2016-06-18 22:24:20 -07:00
Yuxin Wu 49a4187b46 update readme 2016-06-18 10:16:41 -07:00
Yuxin Wu ac958c32db add help 2016-06-18 10:12:41 -07:00
Yuxin Wu 1f2f7560bc emoji cache downloadd 2016-06-18 10:04:53 -07:00
Yuxin Wu 91ac3126f8 support red envelope 2016-06-17 23:20:08 -07:00
Yuxin Wu 212caa37cf request emoji from cdnUrl 2016-06-17 23:13:39 -07:00
Yuxin Wu a8777b9c1b use static cover for emojis 2016-06-01 11:22:34 -07:00
Yuxin Wu 3c28406935 fix printing 2016-04-29 11:14:42 -04:00
Yuxin Wu cae58fb5ec Merge pull request #27 from myGod0rz/patch-1
Update plot-num-msg-by-time.py
2016-04-23 12:50:39 -04:00
myGod0rz 260ae7ed28 Update plot-num-msg-by-time.py
from wechat.util import ......
from common.textutil import ensure_unicode
2016-04-23 18:07:01 +08:00
Yuxin Wu e644246311 dump audio 2016-04-21 18:35:38 -04:00
Yuxin Wu 268544183d update script with argparse 2016-04-21 18:13:06 -04:00
Yuxin Wu 1135c81cc5 update donate button 2016-04-16 00:26:49 -04:00
Yuxin Wu 23a1c09797 Merge pull request #25 from xrf/readme
Update README.md
2016-03-18 22:02:54 -04:00
Fei Yuan 876b41e3eb Update README.md
Clarify a few things and improve the formatting.
2016-03-18 22:01:28 -04:00
Yuxin Wu 1e5f15f506 Merge pull request #24 from xrf/compatibility
Allow android-interact.sh to be run in another directory
2016-03-18 12:07:19 -04:00
Yuxin Wu 2a3d37dd98 Merge pull request #23 from xrf/iphonesubinfo
Fallback to 'service call' to obtain IMEI
2016-03-18 10:07:20 -04:00
Fei Yuan fd64ea52d7 Allow android-interact.sh to be run in another directory
In order to source compatibility.sh, the working directory must be
changed to the directory of the script.  But finding the script's
directory requires $REALPATH, which is defined in compatibility.sh.

To break the cycle, the script now makes use of Python's
os.path.realpath.
2016-03-18 03:08:10 -04:00
Fei Yuan 5eb6e71515 Fallback to 'service call' to obtain IMEI
Uses the solution from http://stackoverflow.com/a/32293148

Tested and works on Android 6.0.1 on Nexus 4.

Fixes #9.
2016-03-18 02:52:38 -04:00
Yuxin Wu fcde19dc23 update 2016-02-08 10:38:38 -05:00
Yuxin Wu f3956fa50a list chats 2016-01-21 00:53:53 -05:00
Yuxin Wu 1cfc8b707b update avatar parsing 2016-01-21 00:16:04 -05:00
Yuxin Wu 28c7d3d599 group message with proper handling of user name 2016-01-20 14:12:14 -05:00
Yuxin Wu 683bdc30d0 Update README.md 2016-01-14 19:43:29 -05:00
Yuxin Wu 85061e9bd5 Update README.md 2016-01-10 01:27:20 -05:00
Yuxin Wu 76962d0ac1 Delete DOES_IT_WORK.md 2016-01-10 01:26:44 -05:00
Yuxin Wu b0b8a3e867 Update DOES_IT_WORK.md 2016-01-10 01:20:04 -05:00
Yuxin Wu 3f80e56365 Update DOES_IT_WORK.md 2016-01-10 01:16:47 -05:00
Yuxin Wu 6ab40e92ff add does-it-work 2016-01-10 01:14:22 -05:00
Yuxin Wu 0875e4d97a use empty avatar in failure 2016-01-08 21:38:21 -05:00
ymy513 86619339a5 Welcome 2016-01-05 00:10:28 -05:00
ppwwyyxx 4d13d287d0 add gitattr 2015-11-21 22:04:49 -05:00
ppwwyyxx 42ee381c7e fix when contacts are deleted 2015-11-09 10:48:41 -05:00
ppwwyyxx 13e3d456e8 use newer version of sqlcipher 2015-11-03 15:17:56 -05:00
Yuxin Wu ffcfa9237a Merge pull request #15 from SwordYork/master
find correct avatar.block.xxxxx file (maybe)
2015-10-09 23:13:58 -04:00
Sword York b6066065ed find correct avatar.block.xxxxx file (maybe) 2015-10-10 11:01:19 +08:00
ppwwyyxx 62e66b8769 update README 2015-10-09 15:15:35 -04:00
ppwwyyxx 3f40bec3ef clean-up legacy code 2015-10-09 15:07:28 -04:00
ppwwyyxx 26115c48df update scripts for avatar code 2015-10-09 14:54:19 -04:00
Yuxin Wu 82240095b6 Merge pull request #14 from SwordYork/master
Avatar support for new version wechat
2015-10-09 13:36:49 -04:00
Sword York ab1ea56242 more detail usage 2015-10-09 15:16:50 +08:00
Sword York 4eae9cd7c1 fix bug, add avt_db support 2015-10-09 10:21:41 +08:00
Sword York c156a3142b read avatar from sfs/avatar.block.0000x 2015-10-08 18:16:01 +08:00
ppwwyyxx 5ca0e4e90e more logging in android-interact 2015-08-30 09:22:04 -04:00
ppwwyyxx 88003355c2 use root in adb 2015-08-27 20:24:08 -04:00
ppwwyyxx ce9e2550d0 fix donation 2015-08-23 11:19:46 -04:00
ppwwyyxx 5f176e3bd1 add donation 2015-08-23 11:04:14 -04:00
ppwwyyxx 7015793abe fix import bug 2015-08-23 10:27:10 -04:00
ppwwyyxx f27476fa1c add a chinese title 2015-06-25 21:20:43 +08:00
ppwwyyxx 022e439cbe move utils to common 2015-06-18 00:07:15 +08:00
ppwwyyxx f0dc8fb3af fix rename of a script 2015-06-17 10:10:26 +08:00
ppwwyyxx 79fbe7889f update readme 2015-06-17 10:07:18 +08:00
ppwwyyxx 3b16f40ec2 fix readme 2015-06-16 23:54:45 +08:00
ppwwyyxx d00c0e5cc1 add silk and corresponding code, for wechat 6.0+ 2015-06-16 23:52:27 +08:00
ppwwyyxx 9cc70cec79 make jquery loaded first 2015-06-16 23:47:53 +08:00
Yuxin Wu 7457e23829 Merge pull request #11 from ppwwyyxx/revert-7-html-archive-format
Revert "Changed the formatting of the generated HTML"
2015-06-16 23:46:26 +08:00
Yuxin Wu 25092daa46 Revert "Changed the formatting of the generated HTML" 2015-06-16 23:45:13 +08:00
ppwwyyxx e6208102e4 add silk parsing code 2015-06-16 23:17:43 +08:00
ppwwyyxx 15c8fd6168 rename scripts 2015-06-16 22:25:18 +08:00
ppwwyyxx b3ac340072 move sqlcipher to third-party 2015-06-16 22:20:39 +08:00
Yuxin Wu 85247692c6 Merge pull request #6 from trammel/docfix
Updated README.md to add links to rooted adb and additional required programs
2015-05-25 17:42:15 +08:00
Yuxin Wu ed851845fa Merge pull request #7 from trammel/html-archive-format
Changed the formatting of the generated HTML
2015-05-25 17:29:32 +08:00
ppwwyyxx c60296c6dd bug fix 2015-05-25 15:23:06 +08:00
ppwwyyxx f399643bb9 some small fix.. 2015-05-25 15:17:17 +08:00
Yuxin Wu c6044221d7 Merge pull request #8 from chaserhkj/chaserhkj
Use XML parsers to parse messages, fixes various bugs
2015-04-28 13:54:12 +08:00
KangJing Huang (Chaserhkj) 3358a89faf Use XML parsers to parse messages 2015-04-27 20:03:24 +08:00
Jonathon Padfield b3acacddfd Accidentally used spaces instead of tabs in markdown list item. 2015-04-27 16:03:40 +08:00
Yuxin Wu 06b7e631b3 Merge pull request #5 from trammel/unknown-contact
Added more meaningful error message when the user provides an unknown contact
2015-04-27 14:23:34 +08:00
Yuxin Wu 7348dabbb3 Merge pull request #4 from trammel/smiley-unicode
Fixed smiley unicode parsing to work on Python 2.7, Mac OSX
2015-04-27 14:04:34 +08:00
Jonathon Padfield 41feb7c180 Changed the formatting of the generated HTML, to not have a scrollable chat/style box, but instead a single page of text. It's more easily printable, and searchable. Also added full YYYY-MM-DD date times, because messages can go back years. 2015-04-27 13:29:56 +08:00
Jonathon Padfield 7492366d85 Added more meaningful error message when the user provides an unknown contact name. 2015-04-27 13:29:22 +08:00
Jonathon Padfield ca6334e48e Fixed smiley unicode parsing to work on Python 2.7, Mac OSX 2015-04-27 13:28:45 +08:00
Jonathon Padfield 8ae9a81283 Updated README.md to add links to rooted adb (required on some devices to pull files) and additional required programs 2015-04-27 13:28:00 +08:00
ppwwyyxx cdb0272770 add encryption in todo 2015-04-19 17:32:54 +08:00
ppwwyyxx 987d8bda78 some small change 2015-04-12 21:10:11 +08:00
ppwwyyxx 5cc0cefb2b msg get through sqlite 2015-03-28 00:09:12 +08:00
ppwwyyxx 99faea5637 msg to libchat seems to work 2015-03-25 23:15:22 +08:00
ppwwyyxx 4572cb045e rename 2015-03-25 17:47:31 +08:00
ppwwyyxx 9b3dbb2665 add libchat db 2015-03-25 16:44:12 +08:00
ppwwyyxx e7b41487db add libchat 2015-03-25 15:41:29 +08:00
Yuxin Wu b1dd50e08a Merge pull request #3 from chaserhkj/chaserhkj
Added a stat report script.
2015-03-08 10:54:39 +08:00
HuangKangJing 869bc15e1f Added a stat report script. 2015-03-07 21:22:57 +08:00
Yuxin Wu b85b515137 Merge pull request #2 from chaserhkj/chaserhkj
Fix 2 problems
2015-03-02 11:40:53 +08:00
HuangKangJing c8c64d9142 Image is in PIL.Image now on most platforms 2015-03-02 11:20:28 +08:00
HuangKangJing 9656bb3ffc dill should be added to dependencies 2015-03-02 11:19:59 +08:00
ppwwyyxx a60fa569a9 bug fix 2015-02-02 13:32:11 +08:00
Yuxin Wu 142caa80fc Merge pull request #1 from vuryleo/master
Add some bsd compatibility & darwin version of sqlcipher
2015-01-20 14:58:30 +08:00
Vury Leo 49ff512437 Add darwin version of sqlcipher v2.1.1 2015-01-20 12:03:44 +08:00
Vury Leo ca63c5a190 Add bsd compatibility 2015-01-19 14:35:40 +08:00
ppwwyyxx 7c5a4a3df8 add future 2015-01-12 00:19:11 +08:00
ppwwyyxx 651c5dda1c change order or example 2015-01-12 00:09:51 +08:00
ppwwyyxx 9c7ec015ab add example 2015-01-12 00:09:00 +08:00
ppwwyyxx 69fb2c5dc9 smaller size by avoid repeating image and unnecessary smiley css 2015-01-11 23:43:56 +08:00
Yuxin Wu 442a3fc6fc add license 2015-01-11 21:57:58 +08:00
ppwwyyxx 34a3a7accc add note about sqlcipher 2015-01-11 21:53:42 +08:00
ppwwyyxx 26e2881f66 fix dirname check 2015-01-09 22:43:24 +08:00
ppwwyyxx 0baca87c3f fix todos in readme 2015-01-09 22:32:11 +08:00
ppwwyyxx 7468f7db35 better logger, locate voice message 2015-01-09 22:29:01 +08:00
ppwwyyxx 8eea048660 not required 2015-01-09 12:11:23 +08:00
ppwwyyxx 6476338832 resource dir 2015-01-09 12:09:53 +08:00
ppwwyyxx 39dfcf677b add adb in readme 2015-01-09 12:08:22 +08:00
ppwwyyxx b35d96418b add automatic db decrypt 2015-01-09 12:06:09 +08:00
ppwwyyxx 7e28d5e221 change decrypted db name 2015-01-09 12:03:16 +08:00
ppwwyyxx cddca228b0 add screenshots 2015-01-09 11:57:12 +08:00
ppwwyyxx c2c9de8fcb speed up voice rendering 2015-01-08 00:53:16 +08:00
ppwwyyxx add87ef670 fix some efficient issue 2015-01-08 00:25:09 +08:00
ppwwyyxx 1a655c37e7 fix 1048625 2015-01-07 23:46:08 +08:00
ppwwyyxx 29cb4037e2 readme and prompt 2015-01-07 22:19:17 +08:00
ppwwyyxx c4b497145c add video TODO 2015-01-07 22:03:56 +08:00
ppwwyyxx a3f3cedf7a group chat 2015-01-07 22:02:53 +08:00
ppwwyyxx 555ba90930 update readme 2015-01-07 21:13:30 +08:00
ppwwyyxx f477db0c84 rename template.html 2015-01-03 00:24:52 +08:00
ppwwyyxx b892e519c1 refactor avatar css 2015-01-02 23:24:14 +08:00
ppwwyyxx 138ebf63ee fix bug in android script 2014-12-31 23:28:29 +08:00
ppwwyyxx 93b68015a7 some bug fix 2014-12-27 00:18:42 +08:00
ppwwyyxx 91c8b63875 32bit support 2014-12-26 00:17:23 +08:00
ppwwyyxx f35e4248f6 add pysox, 2x faster 2014-12-25 23:20:35 +08:00
ppwwyyxx c23caa9ad8 bug fix 2014-12-25 10:18:51 +08:00
ppwwyyxx 9cb981749a various bug fixes 2014-12-25 10:04:54 +08:00
ppwwyyxx 8c17349a2c fix readme 2014-12-24 23:25:27 +08:00
ppwwyyxx 9888506420 update readme 2014-12-24 22:54:22 +08:00
ppwwyyxx bcaa7dcae6 multiple html output 2014-12-23 00:03:40 +08:00
ppwwyyxx 7ee52e32f5 bug fix in time-slice 2014-12-22 23:07:36 +08:00
ppwwyyxx f03eb9b5bd add time in html 2014-12-22 22:49:33 +08:00
ppwwyyxx e32547421a fix missing internal emoji 2014-12-22 22:00:20 +08:00
ppwwyyxx b8f7a15213 minor fix 2014-12-22 16:53:23 +08:00
ppwwyyxx d03b353173 add internal emoji files 2014-12-22 09:57:39 +08:00
ppwwyyxx 937fa8b88f add internal emoji 2014-12-22 09:49:30 +08:00
ppwwyyxx e2d2f5166f minor fixes.. 2014-12-21 23:54:19 +08:00
ppwwyyxx 4065a9bbac some minor fixes 2014-12-21 23:35:54 +08:00
ppwwyyxx ea0204fad7 fix DOM parsing 2014-12-21 21:54:26 +08:00
ppwwyyxx 4dd5c3ac36 fix missing file 2014-12-21 21:23:44 +08:00
ppwwyyxx 0622f64f16 fix emoji size 2014-12-21 21:15:51 +08:00
ppwwyyxx c41bdf3815 emoji works 2014-12-20 20:30:46 +08:00
ppwwyyxx 0d219538a6 emoji -> smiley 2014-12-20 17:55:46 +08:00
ppwwyyxx 5b97d3d95f some bug fixes 2014-12-20 15:47:55 +08:00
ppwwyyxx 4bf6d00000 emoji done 2014-12-17 00:09:28 +08:00
ppwwyyxx 378ad73751 a lot of emoji file 2014-12-16 23:51:14 +08:00
ppwwyyxx f91316a9fa emoji works for only some format... 2014-12-15 22:40:42 +08:00
ppwwyyxx 4b847d0c3b add emoji parse 2014-12-14 01:57:44 +08:00
ppwwyyxx 0e89e304c3 bigimg fancybox work 2014-12-13 00:39:10 +08:00
ppwwyyxx 6ba264130c fix title hight 2014-12-12 23:42:29 +08:00
ppwwyyxx 74c58a3b9c add logger 2014-12-12 23:15:59 +08:00
ppwwyyxx 543c79dfc1 image thumbnail 2014-11-23 23:12:07 +08:00
ppwwyyxx 9c5b9afa12 voice works! 2014-11-23 18:04:22 +08:00
ppwwyyxx fb05d2722f voice works 2014-11-23 17:53:00 +08:00
ppwwyyxx 4006bd39df update readme 2014-11-23 16:45:10 +08:00
ppwwyyxx d614eaf454 avatar done 2014-11-23 16:38:04 +08:00
ppwwyyxx 5f39a09da3 update readme 2014-11-22 23:57:48 +08:00
ppwwyyxx 3fcfd43921 generate msg-only html 2014-11-22 23:52:50 +08:00
ppwwyyxx b15c0f0d79 change file name 2014-11-22 22:33:02 +08:00
ppwwyyxx 9793382814 add Render 2014-11-22 22:30:10 +08:00
ppwwyyxx b7d97d4cb2 add Res.py 2014-11-22 21:59:59 +08:00
ppwwyyxx 680ee0e962 video/links 2014-11-22 08:26:10 +08:00
ppwwyyxx 35dbf6fcda plt by hour 2014-11-21 15:57:44 +08:00
ppwwyyxx a65c9280b5 plot 2014-11-21 15:19:35 +08:00
ppwwyyxx 9b55b7a303 add todo 2014-11-21 14:41:19 +08:00
ppwwyyxx 436717da10 add readme and driver 2014-11-21 14:37:18 +08:00
ppwwyyxx 83040b8a8d msg can work 2014-11-21 14:09:09 +08:00
ppwwyyxx 68c0e92aac init 2014-11-21 13:09:01 +08:00
199 changed files with 4709 additions and 1064 deletions
+6
View File
@@ -3,3 +3,9 @@
output/ output/
resource resource
*.db *.db
*.db.decoded
*2015*
*2017*
2020*
avatar.index
emoji.cache
+56 -77
View File
@@ -1,118 +1,97 @@
## Dump WeChat Messages from Android ## Render WeChat Messages from Android
## 导出安卓微信聊天数据 ## 安卓微信消息记录分析工具
WeChat(微信), as the most popular mobile IM app in China, doesn't give users any method to export well-formatted history message. WeChat, as the most popular mobile IM app in China, doesn't provide any methods to read its message history.
This tool can parse and export WeChat messages on a rooted android phone. Customers are not able to analyze their own chat messages or interact with other data analysis tools.
Right now it can dump messages in text-only mode, or generate a single-file html containing voice messages, images, emoji, etc. We provide this tool that can parse WeChat messages from a rooted android phone.
This is necessary to provide interoperability between WeChat messages and other message analysis tools.
As examples, we provide sample scripts to obtain statistics of the message history and
render the messages into self-contained html files including voice messages, images, emojis, videos, etc.
Users can also write custom programs based on this tool to manage their chat messages.
__NEWS__: WeChat 6.0+ uses silk to encode audio. The code is updated. The tool is last verified to work with latest version of WeChat on 2025/01/01.
If the tool works for you, please take a moment to add your phone/OS to [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
__NEWS__: WeChat 6.3 uses a new avatar storage. The code is updated. ## How to use:
If this tools works for you, please take a moment to __add your phone/OS to__ [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
If it doesn't work, please leave an issue together with your phone/OS/wechat version.
### How to use:
#### Dependencies: #### Dependencies:
+ python-PIL + adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash.
+ [PyQuery](https://pypi.python.org/pypi/pyquery/1.2.1) + Python >= 3.8
+ [pysox](https://pypi.python.org/pypi/pysox/0.3.6.alpha) + sox (command line tools)
+ [pysqlcipher](https://pypi.python.org/pypi/pysqlcipher) + Silk audio decoder (included; build it with `./third-party/compile_silk.sh`)
+ numpy + Other python dependencies: `pip install -r requirements.txt`.
+ csscompressor (suggested, optional)
+ adb and rooted android phone connected to a Linux/Mac OS.
+ Silk audio decoder (included; just run `./third-party/compile_silk.sh`)
+ gnu-sed
#### Get Necessary Data: #### Get Necessary Data:
Note that commands involving `./android-interact.sh` are meant to be run on the computer. 1. Pull database file and (for older WeChat versions) avatar index:
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
+ (Requires Linux or Mac) Get the decrypted WeChat database and the avatar index: + Manual:
+ Automatic: `./android-interact.sh db-decrypt` + Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device.
+ Requires rooted adb. If the OS distribution does not come with adb support, you can download an app such as https://play.google.com/store/apps/details?id=eu.chainfire.adbd It should be a 32-character-long name consisting of hexadecimal digits.
+ Manual: + Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device.
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device. It should be a 32-character-long name consisting of hexadecimal digits. 2. Decode `EnMicroMsg.db`. We do not provide instructions to do that.
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/{EnMicroMsg.db,sfs/avatar.index}` from the device, possible ways are: 3. Copy the unencrypted WeChat user resource directory `/data/data/com.tencent.mm/MicroMsg/${userid}/{avatar,emoji,image2,sfs,video,voice2}` from the phone to the `resource` directory:
+ `./android-interact.sh db`
+ Use your rooted file system manager app
+ Get WeChat uin (an integer), possible ways are:
+ `./android-interact.sh uin`, which pulls the value from `/data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml`
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
+ Get your phone IMEI number (a positive integer), possible ways are:
+ `./android-interact.sh imei`
+ Call `*#06#` on your phone
+ Find IMEI in system settings
+ Decrypt database, will produce `decrypted.db`:
```
./decrypt-db.py <path to EnMicroMsg.db> <imei> <uin>
```
NOTE: you may need to try different ways to getting imei & uin,
because things behave differently on different phones.
Also, if the decryption doesn't work with pysqlcipher, maybe try the version of sqlcipher in `legacy`.
+ Copy the WeChat user resource directory `/mnt/sdcard/tencent/MicroMsg/${userid}/{emoji,image2,sfs,video,voice2}` from the phone's SD card to the `resource` directory:
+ `./android-interact.sh res` + `./android-interact.sh res`
+ You might need to tweak `RES_DIR` in the script if the default doesn't work + Change `RES_DIR` in the script if the location of these directories is different on your phone.
+ This can take a __long__ time. Some ways to do this faster: For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
+ If there's enough free space on the SD card, you can combine all the files via `busybox tar` without compression in the `adb shell`, use `adb pull` to copy the `tar` archive to the computer, and then extract it. BusyBox is needed as the Android system's `tar` may choke on long paths. + This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive,
+ Alternatively, you can use pipes. This is slower, but doesn't require any free space on the SD card: `busybox tar` is recommended as the Android system's `tar` may choke on long paths.
+ In the end, we need a `resource` directory with the following subdir: `avatar,emoji,image2,sfs,video,voice2`.
```sh 4. (Optional) Install and start a WXGF decoder server on an android device. Without this, certain WXGF images will not be rendered or will be rendered in low resolution.
# copy MicroMsg to the current directory See [WXGFDecoder](WXGFDecoder) for instructions.
adb shell 'cd /mnt/sdcard/tencent &&
busybox tar czf - MicroMsg 2>/dev/null | busybox base64' | 4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2)
base64 -di | tar xzf - and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering.
```
wget -c https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2
tar xf emoji.cache.tar.bz2
#### Run: #### Run:
+ Parse and dump text messages of __every__ chat (requires `decrypted.db`): + Parse and dump text messages of __every__ chat (requires decoded database):
``` ```
./dump-msg.py decrypted.db output_dir ./dump-msg.py decoded.db output_dir
``` ```
+ List all chats (requires `decrypted.db`): + List all chats (required decoded database):
``` ```
./list-chats.py decrypted.db ./list-chats.py decoded.db
``` ```
+ Generate statistical report on text messages (requires `output_dir` from `./dump-msg.py`): + Generate statistics report on text messages (requires `output_dir` from `./dump-msg.py`):
``` ```
./count-message.sh output_dir ./count-message.sh output_dir
``` ```
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires `decrypted.db`, `avatar.index`, and `resource`): + Dump messages of one contact to html, containing voice messages, emojis, and images (requires decoded database and `resource`):
``` ```
./dump-html.py decrypted.db avatar.index resource "<contact_name>" output.html ./dump-html.py "<contact_display_name>"
``` ```
* The output file is `output.html`. Check `./dump-html.py -h` to use different input/output paths.
* Add `--wxgf-server ws://xx.xx.xx.xx:xxxx` to use a WXGF decoder server.
### Examples: ### Examples:
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
Screenshots of generated html: Screenshots of generated html:
![byvoid](https://github.com/ppwwyyxx/wechat-dump/raw/master/screenshots/byvoid.jpg) ![byvoid](https://github.com/ppwwyyxx/wechat-dump/raw/master/screenshots/byvoid.jpg)
### TODO List See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
+ Search by uid/username
+ Faster way to copy a directory from android (I don't know..). ### TODO List (help needed!)
+ Fix rare unhandled types: > 10000 and < 0 * After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 -
+ Better user experiences... see `grep 'TODO' wechat -R` they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis.
+ more easy-to-use for non-programmers (GUI?) * Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu).
* Fix rare unhandled message types: > 10000 and < 0
* Better user experiences... see `grep 'TODO' wechat -R`
### Donate! ### Donate!
Paypal:
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted"> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted">
<img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" /> <img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" />
</a> </a>
+11
View File
@@ -0,0 +1,11 @@
*.iml
.gradle
local.properties
.idea
app/release
.DS_Store
build
captures
.externalNativeBuild
.cxx
local.properties
+29
View File
@@ -0,0 +1,29 @@
# WXGF Decoder
This is an android app that decodes wechat's proprietary "WXGF/WXAM" image format.
## Build & Install
Open this project directory using android studio, and build against an ARMv8 device.
Pre-built APKs can be downloaded at [releases](https://github.com/ppwwyyxx/wechat-dump/releases).
Install the APK to your android phone.
Note that this app uses native libs, therefore requires an ARMv8 compatible device to run.
The native libs are extracted from the WeChat v7.0.22 APK downloaded from [here](https://www.apkmirror.com/apk/wechat-tencent/wechat/wechat-7-0-22-release/wechat-7-0-22-android-apk-download/)
## Use
<p align="center">
<img src="screenshot.jpg" alt="screenshot" width="400">
</p>
1. (Optional) Configure the port number if needed, or leave it to default.
2. Click "Start Server"
3. Use the server address `ws://xx.xx.xx.xx:xxxx` to decode
* Make sure this address is accessible from the computer that runs `wechat-dump`.
For example, connect them into the same Wifi.
4. Once a client is connected and decoding request is received, logs will be printed in the app.
+1
View File
@@ -0,0 +1 @@
build
+55
View File
@@ -0,0 +1,55 @@
import com.android.build.api.dsl.Packaging
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "com.ppwwyyxx.wxgfdecoder"
compileSdk = 35
defaultConfig {
applicationId = "com.ppwwyyxx.wxgfdecoder"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
viewBinding = true
}
packagingOptions {
pickFirst( "jniLibs/arm64-v8a/libvoipCodec.so")
}
}
dependencies {
implementation("org.java-websocket:Java-WebSocket:1.5.4")
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.navigation.fragment)
implementation(libs.navigation.ui)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:extractNativeLibs="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WXAMConverter"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,227 @@
package com.ppwwyyxx.wxgfdecoder;
import android.content.Context;
import android.os.Build;
import android.os.PowerManager;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.EditText;
import com.tencent.mm.plugin.gif.MMWXGFJNI;
import androidx.appcompat.app.AppCompatActivity;
import com.ppwwyyxx.wxgfdecoder.databinding.ActivityMainBinding;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.List;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private Button startButton;
private TextView hostnameTextView, portTextView, logTextView;
private EditText portEditText;
private WebSocketServer server;
static {
System.loadLibrary("wechatcommon");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Keep screen on
getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// If the app is running on Android 8.0 or higher, request for ignoring battery optimizations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (!powerManager.isIgnoringBatteryOptimizations(getPackageName())) {
// TODO: request for ignoring battery optimizations
}
}
MMWXGFJNI.initialize(getApplicationInfo().nativeLibraryDir);
// Set up UI
startButton = findViewById(R.id.start_server_button);
hostnameTextView = findViewById(R.id.text_view_hostname);
portTextView = findViewById(R.id.text_view_port);
portEditText = findViewById(R.id.edit_text_port);
logTextView = findViewById(R.id.log_text_area);
logTextView.setText("");
startButton.setOnClickListener(v -> {
try {
startWebSocketServer();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// Run a unit test:
testWxgfDecoder();
}
private void startWebSocketServer() throws InterruptedException {
if (server != null) {
server.stop();
server = null;
}
int port;
try {
port = Integer.parseInt(portEditText.getText().toString());
} catch (NumberFormatException e) {
port = 8080;
}
try {
server = new WebSocketServer(new InetSocketAddress(port)) {
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
printLog("new connection from " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
printLog("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
}
@Override
public void onMessage( WebSocket conn, ByteBuffer message ) {
byte[] msg = message.array();
printLog("received binary message from " + conn.getRemoteSocketAddress() + ": size=" + msg.length
+ " -- " + Arrays.toString(Arrays.copyOfRange(msg, 0, 10)));
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(msg);
if (res != null) {
printLog("After decoding: size=" + res.length + " -- " +
Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
ByteBuffer response = ByteBuffer.wrap(res);
conn.send(response); // the response gif
} else {
printLog("Failed to decode.");
conn.send("FAILED".getBytes());
}
}
@Override
public void onMessage( WebSocket conn, String message) {
printLog("Unexpected! received text message from " + conn.getRemoteSocketAddress() + ": " + message);
}
@Override
public void onError(WebSocket conn, Exception ex) {
printLog("an error occurred on connection " + conn + ":" + ex);
}
@Override
public void onStart() {
try {
String hostName = getLocalIpAddress(true);
int port = server.getPort();
printLog("server started successfully at ws://" + hostName + ":" + port);
runOnUiThread(() -> { // update UI in UI thread
hostnameTextView.setText("Hostname: " + hostName);
portTextView.setText("Port: " + String.valueOf(port));
});
} catch (Exception e) {
e.printStackTrace();
}
}
};
server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
private void printLog(String log) {
System.out.println(log);
runOnUiThread(() -> {
String currentLog = logTextView.getText().toString();
logTextView.setText(currentLog + "\n" + log);
});
}
public String getLocalIpAddress(boolean useIPv4) throws SocketException {
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface intf : interfaces) {
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
for (InetAddress addr : addrs) {
if (!addr.isLoopbackAddress()) {
String sAddr = addr.getHostAddress();
boolean isIPv4 = sAddr.indexOf(':')<0;
if (useIPv4) {
if (isIPv4)
return sAddr;
} else {
return sAddr;
}
}
} }
return "localhost";
}
// Methods below are for debugging the decoder:
private void testWxgfDecoder() {
byte[] wxgf = null;
try {
wxgf = readBinary(this);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("Unit test: wxgf size=" + wxgf.length);
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(wxgf);
System.out.println("Unit test: res size=" + res.length + " -- " + Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
try {
writeFile(this, "test.jpg", res);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (wxgf.length != 64367 || res.length != 705188) {
throw new RuntimeException("test failed");
}
}
public static byte[] readBinary(Context context) throws IOException {
InputStream inputStream = context.getResources().openRawResource(R.raw.test_wxgf); // R.raw.image refers to your image.jpg file
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
inputStream.close(); // Close the input stream
return byteArrayOutputStream.toByteArray();
}
public static void writeFile(Context context, String filename, byte[] data) throws IOException {
File file = new File(context.getExternalFilesDir(null), filename); // Use null for the type argument to get the root external files directory
System.out.println("Writing file to: " + file.getAbsolutePath());
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(data);
outputStream.close();
}
}
@@ -0,0 +1,37 @@
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentDashboardBinding;
public class DashboardFragment extends Fragment {
private FragmentDashboardBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
DashboardViewModel dashboardViewModel =
new ViewModelProvider(this).get(DashboardViewModel.class);
binding = FragmentDashboardBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView textView = binding.textDashboard;
dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
@@ -0,0 +1,19 @@
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class DashboardViewModel extends ViewModel {
private final MutableLiveData<String> mText;
public DashboardViewModel() {
mText = new MutableLiveData<>();
mText.setValue("This is dashboard fragment");
}
public LiveData<String> getText() {
return mText;
}
}
@@ -0,0 +1,34 @@
package com.ppwwyyxx.wxgfdecoder.ui.home;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentHomeBinding;
public class HomeFragment extends Fragment {
private FragmentHomeBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
HomeViewModel homeViewModel =
new ViewModelProvider(this).get(HomeViewModel.class);
binding = FragmentHomeBinding.inflate(inflater, container, false);
View root = binding.getRoot();
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
@@ -0,0 +1,19 @@
package com.ppwwyyxx.wxgfdecoder.ui.home;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class HomeViewModel extends ViewModel {
private final MutableLiveData<String> mText;
public HomeViewModel() {
mText = new MutableLiveData<>();
mText.setValue("This is home fragment");
}
public LiveData<String> getText() {
return mText;
}
}
@@ -0,0 +1,37 @@
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentNotificationsBinding;
public class NotificationsFragment extends Fragment {
private FragmentNotificationsBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
NotificationsViewModel notificationsViewModel =
new ViewModelProvider(this).get(NotificationsViewModel.class);
binding = FragmentNotificationsBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView textView = binding.textNotifications;
notificationsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
@@ -0,0 +1,19 @@
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class NotificationsViewModel extends ViewModel {
private final MutableLiveData<String> mText;
public NotificationsViewModel() {
mText = new MutableLiveData<>();
mText.setValue("This is notifications fragment");
}
public LiveData<String> getText() {
return mText;
}
}
@@ -0,0 +1,14 @@
package com.tencent.mm.plugin.fts.jni;
public class FTSJNIUtils {
public static native int countTokens(String str);
public static native int icuInitialize(String str);
public static native String icuTokenizer(String str);
public static native void nativeInitFts(long j, byte[] bArr);
public static native int stringCompareUtfBinary(String str, String str2);
}
@@ -0,0 +1,101 @@
//File:
package com.tencent.mm.plugin.gif;
import android.graphics.Bitmap;
import java.io.File;
/*
*import com.tencent.matrix.trace.core.AppMethodBeat;
*import com.tencent.mm.loader.p558j.C11743b;
*import com.tencent.mm.sdk.platformtools.C52656ad;
*/
/* renamed from: com.tencent.mm.plugin.gif.MMWXGFJNI */
public class MMWXGFJNI {
public static final int PIC_TYPE_JPG = 0;
public static final int PIC_TYPE_PNG = 1;
private static final String TAG = "MicroMsg.JNI.MMWXGF";
static int mECode;
public static boolean mIsInit;
public static native int nativeAV2Gif(String str, String str2);
public static native int nativeAV2Gif(byte[] bArr, byte[] bArr2);
public static native int nativeAddGifEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
public static native int nativeAddWxAMEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
public static native int nativeDecodeBufferFrame(long j, byte[] bArr, int i, Bitmap bitmap, int[] iArr);
public static native int nativeDecodeBufferHeader(long j, byte[] bArr, int i);
public static native byte[] nativeFinishGifEncode(long j);
public static native byte[] nativeFinishWxAMEncode(long j);
public static native int nativeGetOption(long j, byte[] bArr, int i, int[] iArr);
public static native int nativeInit(String str);
public static native long nativeInitGifEncode(int i, int i2, long j);
public static native long nativeInitWxAMDecoder();
public static native long nativeInitWxAMEncoder(int i, int i2, long j, int i3);
public static native boolean nativeIsWXGF(byte[] bArr, int i);
public static native int nativePic2Wxam(String str, String str2, int i, int i2);
public static native int nativePic2WxamWithWH(String str, String str2, int i, int i2, int i3, int i4);
public static native int nativePic2WxamWithWH(String str, byte[] bArr, int i, int i2, int i3, int i4, int i5, int i6, int i7);
public static native int nativeRewindBuffer(long j);
public static native int nativeUninit(long j);
public static native int nativeWxam2Pic(String str, String str2);
public static native byte[] nativeWxam2PicBuf(byte[] bArr);
public static native byte[] nativeWxamToGif(byte[] bArr);
public static void initialize(String nativeDir) {
// init wxgf decoder
String soFilePath = nativeDir + "/libvoipCodec.so";
File soFile = new File(soFilePath);
if (!soFile.exists())
throw new RuntimeException("missing so file " + soFilePath);
int initSuccess = nativeInit(soFilePath); // return -1 if missing so
if (initSuccess < 0)
throw new RuntimeException("nativeInit failed " + initSuccess);
mIsInit = true;
}
/* // Original code below:
*static {
* AppMethodBeat.m13231i(104681);
* mIsInit = false;
* mECode = -1;
* C52656ad.m88211i(TAG, "static MMWXGFJNI");
* if (!mIsInit) {
* String str = C11743b.ahY() + "lib/libvoipCodec.so";
* int nativeInit = nativeInit(str);
* mECode = nativeInit;
* mIsInit = nativeInit >= 0;
* C52656ad.m88212i(TAG, "native init MMWXGF mECode:%d result:%b :%s", Integer.valueOf(mECode), Boolean.valueOf(mIsInit), str);
* AppMethodBeat.m13232o(104681);
* return;
* }
* C52656ad.m88205d(TAG, "MMWXGF has init.");
* AppMethodBeat.m13232o(104681);
*}
*/
public static int getErrorCode() {
return mECode;
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent">
<Space
android:layout_width="match_parent"
android:layout_height="48dp" />
<EditText
android:id="@+id/edit_text_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Port Number (default 8080)"
android:inputType="number" />
<TextView
android:id="@+id/text_view_hostname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Hostname: " />
<TextView
android:id="@+id/text_view_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Port: " />
<Button
android:id="@+id/start_server_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Server" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_weight="1">
<TextView
android:id="@+id/log_text_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical" />
</ScrollView>
</LinearLayout>
<!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
<!-- android:id="@+id/nav_view"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="0dp"-->
<!-- android:layout_marginEnd="0dp"-->
<!-- android:layout_gravity="bottom"-->
<!-- android:background="?android:attr/windowBackground"-->
<!-- app:menu="@menu/bottom_nav_menu" />-->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.dashboard.DashboardFragment">
<TextView
android:id="@+id/text_dashboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.notifications.NotificationsFragment">
<TextView
android:id="@+id/text_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
</menu>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.ppwwyyxx.wxgfdecoder.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.ppwwyyxx.wxgfdecoder.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.ppwwyyxx.wxgfdecoder.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
</navigation>
Binary file not shown.
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
@@ -0,0 +1,6 @@
<resources>
<string name="app_name">WXGF Decoder</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
</resources>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
+4
View File
@@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
+21
View File
@@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+30
View File
@@ -0,0 +1,30 @@
[versions]
agp = "8.8.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.10.0"
constraintlayout = "2.1.4"
lifecycleLivedataKtx = "2.6.1"
lifecycleViewmodelKtx = "2.6.1"
navigationFragment = "2.6.0"
navigationUi = "2.6.0"
media3Common = "1.5.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Wed Jan 22 21:22:48 PST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

+23
View File
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "WXGFDecoder"
include(":app")
+59 -79
View File
@@ -1,48 +1,21 @@
#!/bin/bash #!/bin/bash
# File: android-interact.sh # File: android-interact.sh
# Date: Fri Jun 26 10:38:07 2015 +0800
# Author: Yuxin Wu <[email protected]>
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"` PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
PROG_DIR=`dirname "$PROG_NAME"` PROG_DIR=`dirname "$PROG_NAME"`
cd "$PROG_DIR" cd "$PROG_DIR"
source compatibility.sh
# Please check that your path is the same, since this might be different among devices # Please check that your path is the same, since this might be different among devices
RES_DIR="/mnt/sdcard/tencent/MicroMsg" # RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
MM_DIR="/data/data/com.tencent.mm" RES_DIR="/data/data/com.tencent.mm/MicroMsg"
echo "Starting rooted adb server..." if [[ $1 == "db" || $1 == "res" ]]; then
adb root
if [[ $1 == "uin" ]]; then
adb pull $MM_DIR/shared_prefs/system_config_prefs.xml 2>/dev/null
uin=$($GREP 'default_uin' system_config_prefs.xml | $GREP -o 'value="\-?[0-9]*' | cut -c 8-)
[[ -n $uin ]] || {
>&2 echo "Failed to get wechat uin. You can try other methods, or report a bug."
exit 1
}
rm system_config_prefs.xml
echo "Got wechat uin: $uin"
elif [[ $1 == "imei" ]]; then
imei=$(adb shell dumpsys iphonesubinfo | $GREP 'Device ID' | $GREP -o '[0-9]+')
[[ -n $imei ]] || {
imei=$(adb shell service call iphonesubinfo 1 | awk -F "'" '{print $2}' | sed 's/[^0-9A-F]*//g' | tr -d '\n')
}
[[ -n $imei ]] || {
>&2 echo "Failed to get imei. You can try other methods, or report a bug."
exit 1
}
echo "Got imei: $imei"
elif [[ $1 == "db" || $1 == "res" ]]; then
echo "Looking for user dir name..." echo "Looking for user dir name..."
sleep 1 # sometimes adb complains: device not found # look for dirname which looks like md5 (32 alpha-numeric chars)
userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' \ userList=$(adb shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}')
| awk '{if (length() == 32) print}') numUser=$(echo "$userList" | wc -l)
numUser=$(echo $userList | wc -l)
# choose the first user. # choose the first user.
chooseUser=$(echo $userList | head -n1) chooseUser=$(echo "$userList" | head -n1)
[[ -n $chooseUser ]] || { [[ -n $chooseUser ]] || {
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR" >&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
exit 1 exit 1
@@ -50,54 +23,61 @@ elif [[ $1 == "db" || $1 == "res" ]]; then
echo "Found $numUser user(s). User chosen: $chooseUser" echo "Found $numUser user(s). User chosen: $chooseUser"
if [[ $1 == "res" ]]; then if [[ $1 == "res" ]]; then
echo "Pulling resources... this might take a long time, because adb sucks..." mkdir -p resource
mkdir -p resource; cd resource (
for d in image2 voice2 emoji video sfs; do cd resource || exit
mkdir -p $d; cd $d echo "Pulling resources... "
adb pull $RES_DIR/$chooseUser/$d for d in avatar image2 voice2 emoji video sfs; do
cd .. echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..."
[[ -d $d ]] || { adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null |
>&2 echo "Failed to download resource directory: $RES_DIR/$chooseUser/$d" busybox base64" | base64 -di | tar xzf - --strip-components 5
exit 1 [[ -d $d ]] && continue
}
done echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..."
cd .. adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" |
echo "Resource pulled at ./resource" base64 -di | tar xzf - --strip-components 5
echo "Total size: $(du -sh resource | cut -f1)" [[ -d $d ]] && continue
echo "Trying to download $RES_DIR/$chooseUser/$d with adb pull (slow) ..."
mkdir -p $d
(
cd $d || exit
adb root
sleep 1 # sometimes adb complains: device not found
adb pull "$RES_DIR/$chooseUser/$d"
)
[[ -d $d ]] || {
echo "Failed to download $RES_DIR/$chooseUser/$d"
}
done
echo "Resource pulled at ./resource"
echo "Total size: $(du -sh | cut -f1)"
)
else else
echo "Pulling database and avatar index file..." for f in EnMicroMsg.db sfs/avatar.index; do
adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..."
[[ -f EnMicroMsg.db ]] && \ adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null |
echo "Database successfully downloaded to EnMicroMsg.db" || { busybox base64" | base64 -di | tar xzf - --strip-components 5
>&2 echo "Failed to pull database by adb" [[ -f $f ]] && continue
exit 1
} echo "Trying to download $RES_DIR/$chooseUser/$f with tar & base64 ..."
adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index adb shell su -c "tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | base64" |
[[ -f avatar.index ]] && \ base64 -di | tar xzf - --strip-components 5
echo "Avatar index successfully downloaded to avatar.index" || { [[ -f $f ]] && continue
>&2 echo "Failed to pull avatar index by adb, are you using latest version of wechat?"
exit 1 echo "Trying to download $RES_DIR/$chooseUser/$f with adb pull..."
adb root
sleep 1
adb pull $RES_DIR/$chooseUser/$f
[[ -f $f ]] || {
echo "Failed to download $RES_DIR/$chooseUser/$f"
} }
done
echo "Database and avatar index file successfully downloaded"
fi fi
elif [[ $1 == "db-decrypt" ]]; then
echo "Getting uin..."
$0 uin | tail -n1 | $GREP -o '\-?[0-9]*' | tee /tmp/uin
echo "Getting imei..."
$0 imei | tail -n1 | $GREP -o '[0-9]*' | tee /tmp/imei
echo "Getting db..."
$0 db
echo "Decrypting db..."
imei=$(cat /tmp/imei)
uin=$(cat /tmp/uin)
if [[ -z $imei || -z $uin ]]; then
>&2 echo "Failed to get imei or uin. See README for manual methods."
exit 1
fi
./decrypt-db.py EnMicroMsg.db $imei $uin
rm /tmp/{uin,imei}
echo "Done. See decrypted.db"
else else
echo "Usage: $0 <res|db-decrypt>" echo "Usage: $0 <res|db>"
exit 1 exit 1
fi fi
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
# File: utils.py
# Date: Wed Jun 17 23:59:25 2015 +0800
# Author: Yuxin Wu <[email protected]>
import hashlib
import base64
def ensure_bin_str(s):
if type(s) == str:
return s
if type(s) == unicode:
return s.encode('utf-8')
def ensure_unicode(s):
if type(s) == str:
return s.decode('utf-8')
if type(s) == unicode:
return s
def md5(s):
m = hashlib.md5()
m.update(s)
return m.hexdigest()
def get_file_b64(fname):
data = open(fname, 'rb').read()
return base64.b64encode(data)
def safe_filename(fname):
filename = ensure_unicode(fname)
return "".join(
[c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash -e
# $File: compatibility.sh
# $Date: Tue Jun 16 22:23:36 2015 +0800
# Author: Vury Leo <i[at]vuryleo[dot]com>
if [ `uname` = 'Darwin' ]; then
GREP='grep -E'
MD5SUM='md5'
else
GREP='grep -E'
MD5SUM='md5sum'
fi
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash -e #!/bin/bash -e
# File: count-message.sh # File: count-message.sh
# Date: Sun Apr 12 21:01:01 2015 +0900 # Date: Wed Nov 29 02:32:40 2017 -0800
# Author: Kangjing Huang <[email protected]> # Author: Kangjing Huang <[email protected]>
@@ -11,7 +11,7 @@ then
fi fi
# TODO work on db directly # TODO work on db directly
echo -e "Filename\tCounts of message\tCounts of chars\tCounts of words" echo -e "Filename\t#Lines\t#Chars\t#Words"
SAVEIFS=$IFS SAVEIFS=$IFS
IFS=$(echo -en "\n\b") IFS=$(echo -en "\n\b")
@@ -23,6 +23,6 @@ do
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m) CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w) WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT" echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
done done | sort -t $'\t' -k 2 -n | column -t -s $'\t'
IFS=$SAVEIFS IFS=$SAVEIFS
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
# File: decrypt-db.py
# Author: Yuxin Wu <[email protected]>
from argparse import ArgumentParser
from pysqlcipher import dbapi2 as sqlite
from hashlib import md5
import sys
import os
DEFAULT_OUTPUT_DB_NAME = 'decrypted.db'
def get_args():
parser = ArgumentParser()
parser.add_argument('db', help='path to EnMicroMsg.db')
parser.add_argument('imei', help='15 digit IMEI of your phone')
parser.add_argument('uin', help='WeChat UIN')
parser.add_argument('--output', help='output decrypted database',
default=DEFAULT_OUTPUT_DB_NAME)
args = parser.parse_args()
return args
def get_key(imei, uin):
a = md5(imei + uin)
return a.hexdigest()[:7]
if __name__ == '__main__':
args = get_args()
output = args.output
if os.path.abspath(os.path.dirname(output)) != os.path.abspath('.'):
print "Output file must be in current directory"
sys.exit(1)
if os.path.isfile(output):
print "{} already exists. Remove? (y/n)".format(args.output),
ans = raw_input()
if ans not in ['y', 'Y']:
print "Bye!"
sys.exit()
os.unlink(output)
key = get_key(args.imei, args.uin)
print "KEY: {}".format(key)
print "Decrypt and dump database to {} ... ".format(output)
conn = sqlite.connect(args.db)
c = conn.cursor()
c.execute("PRAGMA key = '" + key + "';")
c.execute("PRAGMA cipher_use_hmac = OFF;")
c.execute("PRAGMA cipher_page_size = 1024;")
c.execute("PRAGMA kdf_iter = 4000;")
c.execute("ATTACH DATABASE '" + args.output + "' AS db KEY '';")
c.execute("SELECT sqlcipher_export('db');" )
c.execute("DETACH DATABASE db;" )
c.close()
+21 -26
View File
@@ -1,49 +1,44 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# File: dump-audio.py
# Author: Yuxin Wu <[email protected]>
import sys import sys
import base64
import argparse import argparse
from common.textutil import ensure_unicode
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from wechat.msg import TYPE_SPEAK
from wechat.res import Resource from wechat.res import Resource
from wechat.render import HTMLRender
from wechat.libchathelper import LibChatHelper
def get_args(): def get_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact') parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output mp3 dir', default='/tmp') parser.add_argument('--output', help='output mp3 dir', default='/tmp')
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database') parser.add_argument('--db', default='decoded.db', help='path to decoded database')
parser.add_argument('--res', default='resource', help='reseource directory') parser.add_argument('--res', default='resource', help='reseource directory')
args = parser.parse_args() args = parser.parse_args()
return args return args
if __name__ == '__main__': if __name__ == '__main__':
args = get_args() args = get_args()
name = ensure_unicode(args.name)
output_file = args.output
parser = WeChatDBParser(args.db) parser = WeChatDBParser(args.db)
res = Resource(args.res, '') res = Resource(parser, args.res)
if name and name in parser.msgs_by_chat: try:
msgs = parser.msgs_by_chat[name] chatid = parser.get_chat_id(args.name)
else: except KeyError:
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys()))) sys.stderr.write(u"Valid Contacts: {}\n".format('\n'.join(parser.all_chat_nicknames)))
sys.stderr.write(u"Couldn't find that contact {}.".format(name)); sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
sys.exit(1) sys.exit(1)
print "Number of Messages: ", len(msgs)
msgs = parser.msgs_by_chat[chatid]
print(f"Number of Messages for {args.name}: ", len(msgs))
assert len(msgs) > 0 assert len(msgs) > 0
libchat = LibChatHelper(parser, res) voice_msgs = [m for m in msgs if m.type == TYPE_SPEAK]
msgs = libchat.convert_msgs(msgs) for idx, m in enumerate(voice_msgs):
voices = [m.sound for m in msgs if m.sound] audio_str, duration = res.get_voice_mp3(m.imgPath)
for idx, v in enumerate(voices): audio_bytes = base64.b64decode(audio_str)
p = v.find(':') outf = f'/{args.output}/{idx:04d}-{duration:.1f}s.mp3'
v = v[p:] with open(outf, 'wb') as f:
with open('/{}/{:04d}.mp3'.format(args.output, idx), 'wb') as f: f.write(audio_bytes)
f.write(v) print(f"Audio written to {outf}")
+36 -22
View File
@@ -1,52 +1,66 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- import os
# File: dump-html.py
# Date: Wed Mar 25 17:44:20 2015 +0800
# Author: Yuxin Wu <[email protected]>
import sys import sys
import argparse import argparse
import logging
from datetime import datetime
from common.textutil import ensure_unicode
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from wechat.res import Resource from wechat.res import Resource
from wechat.render import HTMLRender from wechat.render import HTMLRender
logger = logging.getLogger("wechat")
def get_args(): def get_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact') parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output html file', default='output.html') parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html')
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database') parser.add_argument('--db', default='EnMicroMsg.db.decoded',
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file') help='path to the decoded database, e.g. EnMicroMsg.db.decoded')
parser.add_argument('--res', default='resource', help='reseource directory') parser.add_argument('--res', default='resource', help='the resource directory')
parser.add_argument('--wxgf-server', help='address of the wxgf image decoder server')
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat. Ignore for new version of wechat.')
parser.add_argument('--start', help='start time in format of YYYY-MM-DD HH:MM:SS',
type=datetime.fromisoformat)
args = parser.parse_args() args = parser.parse_args()
return args return args
if __name__ == '__main__': if __name__ == '__main__':
args = get_args() args = get_args()
name = ensure_unicode(args.name)
output_file = args.output output_file = args.output
parser = WeChatDBParser(args.db) parser = WeChatDBParser(args.db)
res = Resource(parser, args.res, args.avt)
if name and name in parser.msgs_by_chat: try:
msgs = parser.msgs_by_chat[name] chatid = parser.get_chat_id(args.name)
else: except KeyError:
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys()))) sys.stderr.write(u"Valid Contacts: {}\n".format(
sys.stderr.write(u"Couldn't find that contact {}.".format(name)); u'\n'.join(parser.all_chat_nicknames)))
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
sys.exit(1) sys.exit(1)
print "Number of Messages: ", len(msgs)
res = Resource(parser, args.res,
wxgf_server=args.wxgf_server,
avt_db=args.avt)
msgs = parser.msgs_by_chat[chatid]
logger.info(f"Number of Messages for chatid {chatid}: {len(msgs)}")
assert len(msgs) > 0 assert len(msgs) > 0
if args.start is not None:
msgs = [msg for msg in msgs if msg.createTime > args.start]
logger.info(f"Number of Messages after {args.start}: {len(msgs)}")
render = HTMLRender(parser, res) render = HTMLRender(parser, res)
htmls = render.render_msgs(msgs) htmls = render.render_msgs(msgs)
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
if len(htmls) == 1: if len(htmls) == 1:
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
print >> f, htmls[0].encode('utf-8') f.write(htmls[0])
else: else:
assert output_file.endswith(".html")
basename = output_file[:-5]
for idx, html in enumerate(htmls): for idx, html in enumerate(htmls):
with open(output_file + '.{}'.format(idx), 'w') as f: with open(basename + f'{idx:02d}.html', 'w') as f:
print >> f, html.encode('utf-8') f.write(html)
res.emoji_reader.flush_cache()
+32 -22
View File
@@ -1,30 +1,40 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: dump-msg.py
# Date: Mon May 25 15:23:05 2015 +0800
# Author: Yuxin Wu <[email protected]>
import logging
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from common.textutil import safe_filename from wechat.common.textutil import safe_filename
import sys, os import sys, os
if len(sys.argv) != 3: logger = logging.getLogger("wechat")
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
db_file = sys.argv[1] if __name__ == '__main__':
output_dir = sys.argv[2] if len(sys.argv) != 3:
try: sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0]))
os.mkdir(output_dir)
except:
pass
if not os.path.isdir(output_dir):
sys.exit("Error creating directory {}".format(output_dir))
parser = WeChatDBParser(db_file) db_file = sys.argv[1]
output_dir = sys.argv[2]
try:
os.mkdir(output_dir)
except:
pass
if not os.path.isdir(output_dir):
sys.exit("Error creating directory {}".format(output_dir))
for name, msgs in parser.msgs_by_chat.iteritems(): parser = WeChatDBParser(db_file)
print u"Writing msgs for {}".format(name)
safe_name = safe_filename(name) for chatid, msgs in parser.msgs_by_chat.items():
with open(os.path.join(output_dir, safe_name + '.txt'), 'w') as f: name = parser.contacts[chatid]
for m in msgs: if len(name) == 0:
print >> f, m logger.info(f"Chat {chatid} doesn't have a valid display name.")
name = str(id(chatid))
logger.info(f"Writing msgs for {name}")
safe_name = safe_filename(name)
outf = os.path.join(output_dir, safe_name + '.txt')
if os.path.isfile(outf):
logger.info(f"File {outf} exists! Skip contact {name}")
continue
with open(outf, 'w') as f:
for m in msgs:
f.write(str(m))
f.write("\n")
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pickle
import sys
import os
import imghdr
import base64
if __name__ == '__main__':
if len(sys.argv) != 3:
print("""\
Usage:
{} unpack output-dir
{} pack input-dir
""".format(sys.argv[0], sys.argv[0]))
sys.exit(1)
if sys.argv[1] == 'unpack':
with open('emoji.cache', 'rb') as f:
dic = pickle.load(f)
outdir = sys.argv[2]
assert os.path.isdir(outdir)
for md5, img in dic.items():
data = img[0]
if not isinstance(data, bytes):
data = data.encode('ascii')
name = os.path.join(outdir, md5 + '.' + img[1].lower())
print(name)
with open(name, 'wb') as f:
f.write(base64.decodebytes(data))
elif sys.argv[1] == 'pack':
ret = {}
indir = sys.argv[2]
files = os.listdir(indir)
for fname in files:
try:
md5, format = fname.split('.')
except:
print("Unable to parse", fname)
continue
with open(os.path.join(indir, fname), 'rb') as f:
b64 = base64.encodebytes(f.read()).decode('ascii')
ret[md5] = (b64, format)
with open('emoji.cache', 'wb') as f:
pickle.dump(ret, f)
-1
View File
@@ -1 +0,0 @@
../compatibility.sh
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash -e
# $File: compatibility.sh
# $Date: Tue Jun 16 22:23:36 2015 +0800
# Author: Vury Leo <i[at]vuryleo[dot]com>
if [ `uname` = 'Darwin' ]; then
GREP='grep -E'
MD5SUM='md5'
else
GREP='grep -E'
MD5SUM='md5sum'
fi
-47
View File
@@ -1,47 +0,0 @@
#!/bin/bash -e
# File: decrypt-db.sh
# Date: Tue Jun 16 22:23:13 2015 +0800
# Author: Yuxin Wu <[email protected]>
source compatibility.sh
MSGDB=$1
imei=$2
uin=$3
output=decrypted.db
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
echo "Usage: $0 <path to EnMicroMsg.db> <imei> <uin>"
exit 1
fi
if [[ -f $output ]]; then
echo -n "$output already exists. removed? (y/n)"
read r
[[ $r == "y" ]] && rm -v $output || exit 1
fi
KEY=$(echo -n "$imei$uin" | $MD5SUM | cut -b 1-7)
echo "KEY: $KEY"
uname | grep Darwin > /dev/null && os=darwin || os=linux
uname -m | grep x86_64 > /dev/null && version=64bit || version=32bit
echo "Use $version sqlcipher of $os."
echo "Dump decrypted database... (Don't worry about libcrypt.so version warning.)"
SQLCIPHER=./sqlcipher/$os/$version
export LD_LIBRARY_PATH=$SQLCIPHER
"$SQLCIPHER"/sqlcipher "$MSGDB" << EOF
PRAGMA key='$KEY';
PRAGMA cipher_use_hmac = off;
PRAGMA cipher_page_size = 1024;
PRAGMA kdf_iter = 4000;
ATTACH DATABASE "$output" AS db KEY "";
SELECT sqlcipher_export("db");
DETACH DATABASE db;
EOF
echo "Database successfully dumped to $output"
@@ -1,8 +1,8 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: create_table.py # File: create_table.py
# Date: Wed Mar 25 16:43:22 2015 +0800 # Date: Wed Mar 25 16:43:22 2015 +0800
# Author: Yuxin Wu <[email protected]> # Author: Yuxin Wu
import sys import sys
import os import os
@@ -10,7 +10,7 @@ import os
from libchat import SqliteLibChat from libchat import SqliteLibChat
if len(sys.argv) != 2: if len(sys.argv) != 2:
print "Usage: {} <DB file name>" print("Usage: {} <DB file name>")
sys.exit() sys.exit()
db_name = sys.argv[1] db_name = sys.argv[1]
@@ -1,8 +1,5 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: libchat.py
# Date: Sun Apr 12 21:08:51 2015 +0900
# Author: Yuxin Wu <[email protected]>
import sqlite3 import sqlite3
import os import os
from datetime import datetime from datetime import datetime
@@ -99,7 +96,7 @@ class SqliteLibChat(object):
else: else:
self.c.execute("SELECT * FROM message WHERE {}".format( self.c.execute("SELECT * FROM message WHERE {}".format(
' AND '.join(["{} = {}".format(k, v) ' AND '.join(["{} = {}".format(k, v)
for k, v in predicate.iteritems()]))) for k, v in predicate.items()])))
for row in self.c.fetchall(): for row in self.c.fetchall():
yield ChatMsg(*SqliteLibChat.postfilter(row)) yield ChatMsg(*SqliteLibChat.postfilter(row))
@@ -113,5 +110,5 @@ if __name__ == '__main__':
for k in db.iterate_all_msg(): for k in db.iterate_all_msg():
from IPython import embed; embed() from IPython import embed; embed()
print k print(k)
@@ -1,8 +1,4 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: libchathelper.py
# Date: Thu Jun 18 00:02:35 2015 +0800
# Author: Yuxin Wu <[email protected]>
import base64 import base64
from pyquery import PyQuery from pyquery import PyQuery
@@ -12,8 +8,8 @@ logger = logging.getLogger(__name__)
from libchat.libchat import SqliteLibChat, ChatMsg from libchat.libchat import SqliteLibChat, ChatMsg
from .msg import * from .msg import *
from common.timer import timing from .common.timer import timing
from common.progress import ProgressReporter from .common.progress import ProgressReporter
class LibChatHelper(object): class LibChatHelper(object):
""" Build LibChat messages from WeChat Msg""" """ Build LibChat messages from WeChat Msg"""
@@ -46,21 +42,27 @@ class LibChatHelper(object):
return img, 'jpeg' return img, 'jpeg'
elif msg.type == TYPE_EMOJI: elif msg.type == TYPE_EMOJI:
md5 = msg.imgPath md5 = msg.imgPath
emoji_img, format = self.res.get_emoji_by_md5(md5) if md5:
return emoji_img, format emoji_img, format = self.res.get_emoji_by_md5(md5)
return emoji_img, format
else:
return '', ''
elif msg.type == TYPE_CUSTOM_EMOJI: elif msg.type == TYPE_CUSTOM_EMOJI:
pq = PyQuery(msg.content) pq = PyQuery(msg.content)
md5 = pq('emoticonmd5').text() md5 = pq('emoticonmd5').text()
img, format = self.res.get_emoji(md5, None) if md5:
return img, format img, format = self.res.get_emoji(md5, None)
return img, format
else:
return '', ''
else: else:
return '', '' return '', ''
def _get_sound(self, msg): def _get_sound(self, msg):
if msg.type == TYPE_SPEAK: if msg.type == TYPE_SPEAK:
audio_str, duration = self.res.get_voice_mp3(msg.imgPath) audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
return '{}:{}'.format(duration, base64.b64decode(audio_str)) return base64.b64decode(audio_str)
return '' return b''
def _get_extra(self, msg): def _get_extra(self, msg):
ret = {} ret = {}
@@ -77,7 +79,7 @@ class LibChatHelper(object):
if img: if img:
# TODO don't use b64, directly return image content # TODO don't use b64, directly return image content
img = base64.b64decode(img) img = base64.b64decode(img)
# TODO do we need to save format? # TODO do we need to save format or voice duration?
sound = self._get_sound(msg) sound = self._get_sound(msg)
extra = self._get_extra(msg) extra = self._get_extra(msg)
-24
View File
@@ -1,24 +0,0 @@
Copyright (c) 2008, ZETETIC LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the ZETETIC LLC nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-7
View File
@@ -1,7 +0,0 @@
## NOTE
For convenience, I include the binary version of sqlcipher 2.1.1.
User can also choose to download and install sqlcipher 2.1.1 from their trusted source, such as:
https://launchpad.net/ubuntu/+source/sqlcipher/2.1.1-2
Binary file not shown.
-130
View File
@@ -1,130 +0,0 @@
#! /bin/sh
# sqlite3 - temporary wrapper script for .libs/sqlite3
# Generated by ltmain.sh (GNU libtool) 2.2.6
#
# The sqlite3 program cannot be directly executed until all the libtool
# libraries that it depends on are installed.
#
# This wrapper script should never be moved out of the build directory.
# If it is, it will not operate correctly.
# Sed substitution that helps us do robust quoting. It backslashifies
# metacharacters that are still active within double-quoted strings.
Xsed='/opt/local/bin/gsed -e 1s/^X//'
sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
# Be Bourne compatible
if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
emulate sh
NULLCMD=:
# Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
# is contrary to our usage. Disable this feature.
alias -g '${1+"$@"}'='"$@"'
setopt NO_GLOB_SUBST
else
case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
fi
BIN_SH=xpg4; export BIN_SH # for Tru64
DUALCASE=1; export DUALCASE # for MKS sh
# The HP-UX ksh and POSIX shell print the target directory to stdout
# if CDPATH is set.
(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
relink_command=""
# This environment variable determines our operation mode.
if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
# install mode needs the following variables:
generated_by_libtool_version='2.2.6'
notinst_deplibs=' libsqlite3.la'
else
# When we are sourced in execute mode, $file and $ECHO are already set.
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
ECHO="/bin/echo"
file="$0"
# Make sure echo works.
if test "X$1" = X--no-reexec; then
# Discard the --no-reexec flag, and continue.
shift
elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
# Yippee, $ECHO works!
:
else
# Restart under the correct shell, and then maybe $ECHO will work.
exec /bin/sh "$0" --no-reexec ${1+"$@"}
fi
fi
# Find the directory that this script lives in.
thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
test "x$thisdir" = "x$file" && thisdir=.
# Follow symbolic links until we get to the real thisdir.
file=`ls -ld "$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
while test -n "$file"; do
destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
# If there was a directory component, then change thisdir.
if test "x$destdir" != "x$file"; then
case "$destdir" in
[\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
*) thisdir="$thisdir/$destdir" ;;
esac
fi
file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
file=`ls -ld "$thisdir/$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
done
# Usually 'no', except on cygwin/mingw when embedded into
# the cwrapper.
WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
# special case for '.'
if test "$thisdir" = "."; then
thisdir=`pwd`
fi
# remove .libs from thisdir
case "$thisdir" in
*[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
.libs ) thisdir=. ;;
esac
fi
# Try to get the absolute directory name.
absdir=`cd "$thisdir" && pwd`
test -n "$absdir" && thisdir="$absdir"
program='sqlite3'
progdir="$thisdir/.libs"
if test -f "$progdir/$program"; then
# Add our own library path to DYLD_LIBRARY_PATH
DYLD_LIBRARY_PATH="/Users/vuryleo/Projects/sqlcipher/.libs:$DYLD_LIBRARY_PATH"
# Some systems cannot cope with colon-terminated DYLD_LIBRARY_PATH
# The second colon is a workaround for a bug in BeOS R4 sed
DYLD_LIBRARY_PATH=`$ECHO "X$DYLD_LIBRARY_PATH" | $Xsed -e 's/::*$//'`
export DYLD_LIBRARY_PATH
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
# Run the actual program with our arguments.
exec "$progdir/$program" ${1+"$@"}
$ECHO "$0: cannot exec $program $*" 1>&2
exit 1
fi
else
# The program doesn't exist.
$ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
$ECHO "This script is just a wrapper for $program." 1>&2
/bin/echo "See the libtool documentation for more information." 1>&2
exit 1
fi
fi
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: list-chats.py # File: list-chats.py
# Author: Yuxin Wu <[email protected]> # Author: Yuxin Wu <[email protected]>
@@ -6,7 +6,7 @@
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
import sys import sys
if len(sys.argv) != 2: if len(sys.argv) != 2:
print "Usage: {} db_file".format(sys.argv[0]) print("Usage: {} db_file".format(sys.argv[0]))
sys.exit(1) sys.exit(1)
db_file = sys.argv[1] db_file = sys.argv[1]
@@ -14,4 +14,4 @@ db_file = sys.argv[1]
parser = WeChatDBParser(db_file) parser = WeChatDBParser(db_file)
chats = parser.msgs_by_chat.keys() chats = parser.msgs_by_chat.keys()
for k in chats: for k in chats:
print k.encode('utf-8'), '\t', parser.contacts_rev[k].encode('utf-8') print(parser.contacts[k], '\t', k)
+28 -31
View File
@@ -1,49 +1,46 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: plot-num-msg-by-time.py
# Date: Wed Mar 25 17:44:39 2015 +0800
# Author: Yuxin Wu <[email protected]>
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from common.textutil import ensure_unicode
from datetime import timedelta, datetime from datetime import timedelta, datetime
import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import sys, os import sys
if len(sys.argv) != 3:
sys.exit("Usage: {0} <path to decrypted_database.db> <name>".format(sys.argv[0]))
db_file = sys.argv[1] if __name__ == '__main__':
name = ensure_unicode(sys.argv[2]) if len(sys.argv) != 3:
every_k_days = 2 sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0]))
parser = WeChatDBParser(db_file) db_file = sys.argv[1]
msgs = parser.msgs_by_chat[name] name = sys.argv[2]
times = [x.createTime for x in msgs] every_k_days = 2
start_time = times[0]
diffs = [(x - start_time).days for x in times]
max_day = diffs[-1]
width = 20 parser = WeChatDBParser(db_file)
numbers = range((max_day / width + 1) * width + 1)[::width] msgs = parser.msgs_by_chat[name]
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers] times = [x.createTime for x in msgs]
plt.xticks(numbers, labels) start_time = times[0]
plt.xlabel("Date") diffs = [(x - start_time).days for x in times]
plt.ylabel("Number of msgs in k days") max_day = diffs[-1]
plt.hist(diffs, bins=max_day / every_k_days)
plt.show() width = 20
numbers = range((max_day / width + 1) * width + 1)[::width]
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
plt.xticks(numbers, labels)
plt.xlabel("Date")
plt.ylabel("Number of msgs in k days")
plt.hist(diffs, bins=max_day / every_k_days)
plt.show()
# statistics by hour # statistics by hour
# I'm in a different time zone in this period: # I'm in a different time zone in this period:
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15} #TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
#def real_hour(x): #def real_hour(x):
#for k, v in TZ_DELTA.iteritems(): #for k, v in TZ_DELTA.items():
#if x > k[0] and x < k[1]: #if x > k[0] and x < k[1]:
#print x #print x
#return (x.hour + v + 24) % 24 #return (x.hour + v + 24) % 24
#return x.hour #return x.hour
#hours = [real_hour(x) for x in times] #hours = [real_hour(x) for x in times]
#plt.ylabel("Number of msgs") #plt.ylabel("Number of msgs")
#plt.xlabel("Hour in a day") #plt.xlabel("Hour in a day")
+10
View File
@@ -0,0 +1,10 @@
pyquery
javaobj-py3>=0.4.1
pycryptodome
pillow>=7.0
requests
pysqlcipher3>=1.0.3
csscompressor
numpy
ipython
websocket-client
-3
View File
@@ -1,7 +1,4 @@
#!/bin/bash -e #!/bin/bash -e
# File: compile_silk.sh
# Date: Tue Jun 16 22:26:49 2015 +0800
# Author: Yuxin Wu <[email protected]>
cd `dirname "$0"`/silk cd `dirname "$0"`/silk
make make
+1 -1
View File
@@ -47,7 +47,7 @@ ifeq (yes,$(USE_NEON))
endif endif
CFLAGS += -Wall -enable-threads -O3 CFLAGS += -Wall -O3
CFLAGS += $(call cppflags-from-defines,$(CDEFINES)) CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES)) CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import logging import logging
@@ -21,10 +21,10 @@ def set_level_color(lvl, color):
logger.addHandler(handler) logger.addHandler(handler)
set_level_color(logging.INFO, '\033[1;32m') set_level_color(logging.INFO, '\033[1;32m')
set_level_color(logging.WARN, '\033[1;31m') set_level_color(logging.WARN, '\033[1;33m')
set_level_color(logging.ERROR, '\033[1;31m') set_level_color(logging.ERROR, '\033[1;31m')
if __name__ == '__main__': if __name__ == '__main__':
logger.info("info") logger.info("info")
logger.warn("warn") logger.warning("warn")
+44 -51
View File
@@ -1,78 +1,71 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: audio.py
# Date: Fri Jun 26 10:42:41 2015 +0800
# Author: Yuxin Wu <[email protected]>
import os import os
from subprocess import PIPE, Popen, call import tempfile
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import pysox from .common.textutil import get_file_b64
from .common.procutil import subproc_succ
from common.textutil import get_file_b64
SILK_DECODER = os.path.join(os.path.dirname(__file__), SILK_DECODER = os.path.join(os.path.dirname(__file__),
'../third-party/silk/decoder') '../third-party/silk/decoder')
if not os.path.exists(SILK_DECODER):
logger.error("Silk decoder is not compiled. Please see README.md.")
raise RuntimeError()
def parse_wechat_audio_file(file_name): def parse_wechat_audio_file(file_name):
try: try:
return do_parse_wechat_audio_file(file_name) return do_parse_wechat_audio_file(file_name)
except Exception as e: except Exception as e:
logger.error("Pase audio file {} error!".format(file_name)) logger.error(f"Error when parsing audio file {file_name}: {str(e)}")
logger.error(e)
return "", 0 return "", 0
def do_parse_wechat_audio_file(file_name): def do_parse_wechat_audio_file(file_name):
""" return a mp3 base64 string, and the duration""" """ return a mp3 stored in base64 unicode string, and the duration"""
if not file_name: return "", 0 if not file_name: return "", 0
mp3_file = os.path.join('/tmp', with tempfile.TemporaryDirectory(prefix="wechatdump_audio") as temp:
os.path.basename(file_name)[:-4] + '.mp3') mp3_file = os.path.join(temp,
with open(file_name) as f: os.path.basename(file_name)[:-4] + '.mp3')
header = f.read(10) with open(file_name, 'rb') as f:
if 'AMR' in header: header = f.read(10)
# maybe this is faster than calling sox from command line? if b'AMR' in header:
infile = pysox.CSoxStream(file_name) cmd = f"sox -e signed -c 1 {file_name} {mp3_file}"
outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal()) subproc_succ(cmd)
chain = pysox.CEffectsChain(infile, outfile) cmd = f"soxi -D {mp3_file}"
chain.flow_effects() duration = float(subproc_succ(cmd))
outfile.close()
signal = infile.get_signal().get_signalinfo() # The below is python2 only. It should be equivalent to using sox from command line
duration = signal['length'] * 1.0 / signal['rate'] # import pysox
elif 'SILK' in header: # infile = pysox.CSoxStream(file_name)
raw_file = os.path.join('/tmp', # outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
os.path.basename(file_name)[:-4] + '.raw') # chain = pysox.CEffectsChain(infile, outfile)
proc = Popen('{0} {1} {2}'.format(SILK_DECODER, # chain.flow_effects()
file_name, raw_file), # outfile.close()
shell=True, stdout=PIPE, stderr=PIPE) # signal = infile.get_signal().get_signalinfo()
stdout = proc.communicate()[0] # duration = signal['length'] * 1.0 / signal['rate']
for line in stdout.split('\n'): elif b'SILK' in header:
if 'File length' in line: if not os.path.exists(SILK_DECODER):
duration = float(line[13:-3].strip()) raise RuntimeError("Silk decoder is not compiled. Please see README.md.")
break
raw_file = os.path.join(temp,
os.path.basename(file_name)[:-4] + '.raw')
cmd = '{0} {1} {2}'.format(SILK_DECODER, file_name, raw_file)
out = subproc_succ(cmd)
for line in out.split(b'\n'):
if b'File length' in line:
duration = float(line[13:-3].strip())
break
else:
raise RuntimeError("Error decoding silk audio file!" + out.decode('utf-8'))
# TODO don't know how to do this with python
subproc_succ('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(raw_file, mp3_file))
else: else:
raise RuntimeError("Error decoding silk audio file!") raise NotImplementedError("Audio file format cannot be recognized.")
# I don't know how to do this with pysox
proc = call('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(
raw_file, mp3_file), shell=True)
os.unlink(raw_file)
else:
raise NotImplementedError("Unsupported Audio Format! This is a bug!")
try:
mp3_string = get_file_b64(mp3_file) mp3_string = get_file_b64(mp3_file)
os.unlink(mp3_file)
except:
raise RuntimeError("Failed to decode audio file: {}".format(file_name))
return mp3_string, duration return mp3_string, duration
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
fname = sys.argv[1] fname = sys.argv[1]
print parse_wechat_audio_file(fname)[1] print(parse_wechat_audio_file(fname)[1])
+106 -40
View File
@@ -1,71 +1,137 @@
#!/usr/bin/env python2 #!/usr/bin/env python3
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: avatar.py
# Date: Thu Jun 18 00:02:07 2015 +0800
# Author: Yuxin Wu <[email protected]>
from PIL import Image from PIL import Image
import cStringIO import io
import glob
import os import os
import numpy as np import numpy as np
import logging import logging
import sqlite3 import sqlite3
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from common.textutil import ensure_bin_str, md5 from .common.textutil import md5
def _filename_priority(s):
if "_hd" in s and s.endswith(".png"):
return 10
else:
return 1
class AvatarReader(object): class AvatarReader(object):
def __init__(self, avt_dir, avt_db="avatar.index"): def __init__(self, res_dir, avt_db="avatar.index"):
self.avt_dir = avt_dir self.sfs_dir = os.path.join(res_dir, 'sfs')
# new location of avatar, see #50
self.avt_dir = os.path.join(res_dir, 'avatar')
if not os.path.isdir(self.avt_dir) or len(os.listdir(self.avt_dir)) == 0:
self.avt_dir = None
self.avt_db = avt_db self.avt_db = avt_db
if self.avt_db is None or not os.path.isfile(self.avt_db): self._use_avt = True
logger.warn( if self.avt_db is not None:
"Avatar database {} not found. Will not use avatar!".format(avt_db)) if len(glob.glob(os.path.join(self.sfs_dir, 'avatar*'))) == 0:
self.avt_db = None # has sfs/avatar*
self.avt_db = None
if self.avt_dir is None and self.avt_db is None:
logger.warn("Cannot find avatar storage. Will not use avatar!")
self._use_avt = False
def get_avatar_from_avtdb(self, avtid):
try:
candidates = self._search_avt_db(avtid)
candidates = sorted(candidates, key=lambda x: _filename_priority(x[0]), reverse=True)
for c in candidates:
path, offset, size = c
return self.read_img_from_block(path, offset, size)
except Exception:
pass
def get_avatar_from_avtdir(self, avtid) -> Image.Image | None:
dir1, dir2 = avtid[:2], avtid[2:4]
candidates = glob.glob(os.path.join(self.avt_dir, dir1, dir2, f"*{avtid}*"))
candidates = sorted(set(candidates), key=_filename_priority, reverse=True)
for cand in candidates:
if os.path.isdir(cand):
candidates.extend(os.path.join(cand, x) for x in os.listdir(cand))
for cand in candidates:
if os.path.isdir(cand):
continue
try:
if cand.endswith(".bm"):
return self.read_bm_file(cand)
else:
return Image.open(cand)
except Exception:
logger.exception("")
pass
def get_avatar(self, username): def get_avatar(self, username):
""" username: `username` field in db.rcontact""" """ username: `username` field in db.rcontact"""
if self.avt_db is None: return None if not self._use_avt:
username = ensure_bin_str(username)
filename = md5(username)
dir1, dir2 = filename[:2], filename[2:4]
filename = os.path.join(dir1, dir2,
"user_{}.png".format(filename))
try:
try:
pos, size = self.query_index(filename)
return self.read_img(pos, size)
except TypeError:
logger.warn("Avatar for {} not found in avatar database.".format(username))
return None
except Exception as e:
raise
print e
logger.warn("Failed to retrieve avatar!")
return None return None
avtid = md5(username.encode('utf-8'))
if self.avt_db is not None:
ret = self.get_avatar_from_avtdb(avtid)
if ret is not None:
return ret
def read_img(self, pos, size): if self.avt_dir is not None:
ret = self.get_avatar_from_avtdir(avtid)
if ret is not None:
return ret
logger.warning("Avatar file for {} not found.".format(username))
def save_avatar_to_avtdir(self, username: str, im: Image.Image):
"""Save a downloaded avatar to avtdir so it can be reused next time."""
avtid = md5(username.encode('utf-8'))
dir1, dir2 = avtid[:2], avtid[2:4]
fname = os.path.join(self.avt_dir, dir1, dir2, f"user_{avtid}.png")
os.makedirs(os.path.dirname(fname), exist_ok=True)
logger.info(f"Caching downloaded avatar for {username} to {fname}.")
im.save(fname, 'PNG')
def read_img_from_block(self, filename, pos, size):
file_idx = pos >> 32 file_idx = pos >> 32
fname = os.path.join(self.avt_dir, fname = os.path.join(self.sfs_dir,
'avatar.block.' + '{:05d}'.format(file_idx)) 'avatar.block.' + '{:05d}'.format(file_idx))
# a 64-byte offset of each block file # offset of each block file: 17 + len(path)
start_pos = pos - file_idx * (2**32) + 64 start_pos = pos - file_idx * (2**32) + 16 + len(filename) + 1
try: try:
with open(fname, 'rb') as f: with open(fname, 'rb') as f:
f.seek(start_pos) f.seek(start_pos)
data = f.read(size) data = f.read(size)
im = Image.open(cStringIO.StringIO(data)) im = Image.open(io.BytesIO(data))
return im return im
except IOError as e: except IOError as e:
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e))) logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
return None return None
def query_index(self, filename): def read_bm_file(self, fname):
# history at https://github.com/ppwwyyxx/wechat-dump/pull/14
with open(fname, 'rb') as f:
# filesize is 36880=96x96x4+16
size = (96, 96, 3)
img = np.zeros(size, dtype='uint8')
for i in range(96):
for j in range(96):
r, g, b, a = f.read(4)
img[i,j] = (r, g, b)
return Image.fromarray(img, mode="RGB")
def _search_avt_db(self, avtid):
conn = sqlite3.connect(self.avt_db) conn = sqlite3.connect(self.avt_db)
cursor = conn.execute("select Offset,Size from Index_avatar where FileName='{}'".format(filename)) cursor = conn.execute("select FileName,Offset,Size from Index_avatar")
pos, size = cursor.fetchone() candidates = []
return pos, size for path, offset, size in cursor:
if avtid in path:
candidates.append((path, offset, size))
return candidates
if __name__ == '__main__':
import sys
r = AvatarReader(sys.argv[1], sys.argv[2])
print(r.get_avatar(sys.argv[3]))
+51
View File
@@ -0,0 +1,51 @@
import io
from pathlib import Path
from PIL import Image
try:
import imghdr as _imghdr
except ModuleNotFoundError: # Python 3.13+
_imghdr = None
def what(file: str | Path | None = None, h: bytes | None = None) -> str | None:
"""
Compatibility wrapper for the removed stdlib `imghdr` module (Python 3.13+).
Behaves like `imghdr.what(file, h)` and returns a lowercase format string
like "jpeg", "png", "gif", or None if unknown.
"""
if _imghdr is not None:
return _imghdr.what(file, h)
if h is not None:
return _what_from_bytes(h)
if file is None:
return None
try:
with Image.open(str(file)) as im:
fmt = im.format
except Exception:
return None
return fmt.lower() if fmt else None
def _what_from_bytes(data: bytes) -> str | None:
if data.startswith(b"\xff\xd8\xff"):
return "jpeg"
if data.startswith(b"\x89PNG\r\n\x1a\n"):
return "png"
if data[:6] in (b"GIF87a", b"GIF89a"):
return "gif"
if data.startswith(b"BM"):
return "bmp"
if len(data) >= 12 and data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "webp"
if data.startswith(b"II*\x00") or data.startswith(b"MM\x00*"):
return "tiff"
try:
with Image.open(io.BytesIO(data)) as im:
fmt = im.format
except Exception:
return None
return fmt.lower() if fmt else None
+48
View File
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
import subprocess
import logging
logger = logging.getLogger(__name__)
def subproc_call(cmd, timeout=None):
"""
Execute a command with timeout, and return STDOUT and STDERR
Args:
cmd(str): the command to execute.
timeout(float): timeout in seconds.
Returns:
output(bytes), retcode(int). If timeout, retcode is -1.
"""
try:
output = subprocess.check_output(
cmd, stderr=subprocess.STDOUT,
shell=True, timeout=timeout)
return output, 0
except subprocess.TimeoutExpired as e:
logger.warn("Command '{}' timeout!".format(cmd))
if e.output:
logger.warn(e.output.decode('utf-8'))
return e.output, -1
else:
return "", -1
except subprocess.CalledProcessError as e:
logger.warn("Command '{}' failed, return code={}".format(cmd, e.returncode))
logger.warn(e.output.decode('utf-8'))
return e.output, e.returncode
except Exception:
logger.warn("Command '{}' failed to run.".format(cmd))
return "", -2
def subproc_succ(cmd):
"""
Like subproc_call, but expect the cmd to succeed.
"""
output, ret = subproc_call(cmd)
assert ret == 0
return output

Some files were not shown because too many files have changed in this diff Show More