231 Commits
Author SHA1 Message Date
Yuxin Wu 5320456e80 update 2025-01-05 17:03:38 +08:00
JakeandGitHub 5da9994eef Update README.md (#99)
old link doesn't work anymore.
2024-08-13 16:33:02 -07:00
Yuxin Wu baef03db85 fix #90 2023-05-20 18:15:50 -07:00
Yuxin WuandGitHub 3971729641 Update README.md 2021-12-13 16:46:57 -08:00
SeaandGitHub d4b8cf3530 Update decrypt-db.py (#87)
Add error output for try block of do_decrypt
2021-08-29 12:51:11 -07:00
Yuxin Wu d0ec0886c0 check sqlcipher version (fix #85) 2021-04-23 15:10:33 -07:00
Yuxin Wu 46cda89270 update docs 2021-04-19 01:22:36 -07:00
76f0a8f44e 🌈style(view): fix body scroll (#84)
Co-authored-by: lqzh <[email protected]>
2021-04-13 15:58:53 -07:00
lqzhgoodandGitHub 1009d8fcb2 Update requirements.txt (#82)
add `numpy` module
2021-04-13 00:19:28 -07:00
Yuxin Wu 0bd5a7d6f5 update docs about sfs/avatar.index 2021-02-02 11:00:48 -08:00
Yuxin Wu a98c923d8f convert javaobj to int 2021-02-01 18:19:34 -08:00
Yuxin Wu 9b1315a9c5 update 2021-01-07 19:12:20 -08:00
Yuxin Wu 6b50191420 uin int to str 2020-11-27 15:34:30 -08:00
Yuxin Wu 7361d53bd1 make uin a str 2020-11-27 15:33:17 -08:00
Yuxin Wu 70f2925026 catch decryption error (#72) 2020-10-03 04:38:19 -07:00
Yuxin Wu 9d5682485c fix #70 2020-09-19 11:54:12 -07:00
Yuxin Wu dfdcd4ba0f use tempfile for audio decoding 2020-07-17 12:26:25 -07:00
Yuxin Wu 9b64982af0 fix encoding (fix #67) 2020-07-17 12:06:16 -07:00
Kai JiaandGitHub c5ee3026f0 Fix a typo in README (#66) 2020-07-06 10:11:34 -07:00
Yuxin Wu 100b8e2391 update readme 2020-07-02 17:00:46 -07:00
Yuxin Wu a914018579 update readme 2020-07-01 01:26:15 -07:00
Yuxin Wu 8e8b1d3534 better error message 2020-06-30 23:31:56 -07:00
Yuxin Wu e1157a18b7 remove "internal_emoji" 2020-06-30 23:16:19 -07:00
Yuxin Wu 641af7a3cd decrypt local emoji 2020-06-30 23:09:46 -07:00
Yuxin Wu 45c28312d3 check md5 of local emoji as well 2020-06-29 11:21:07 -07:00
Yuxin Wu 146855d1fb use encrypturl if cdnurl does not produce correct md5 2020-06-29 11:07:44 -07:00
Yuxin Wu a14e270ff2 remove failed decryption 2020-06-29 03:30:15 -07:00
Yuxin Wu fd0a065b6e aes-decrypt emoji from encrypturl 2020-06-28 02:57:14 -07:00
Yuxin Wu 02f81eb505 check for two avatar storage 2020-06-28 00:33:07 -07:00
Yuxin Wu 99c5ed6d33 fix old avatar storage 2020-06-28 00:12:39 -07:00
Yuxin Wu bb44745bb4 move common/ into wechat/ 2020-06-27 20:34:05 -07:00
Yuxin Wu 5f729e4227 support both chatid and nickname (fix #51) 2020-06-27 20:04:34 -07:00
Yuxin Wu b272858b1a avoid assertion for type 49 (close #52) 2020-06-27 19:56:28 -07:00
Yuxin Wu 58ce6ea853 update readme 2020-06-27 19:45:42 -07:00
Yuxin Wu ceec29afc9 add missing file 2020-06-27 19:32:21 -07:00
Yuxin Wu d8ac9d8fad compatibility with an old database file 2020-06-27 19:00:43 -07:00
Yuxin Wu b295d62f41 update 2020-06-27 17:34:52 -07:00
Yuxin Wu 394b1d75aa support latest smileys 2020-06-27 17:31:35 -07:00
Yuxin Wu 5f93b39916 move libchat to legacy 2020-06-27 16:34:35 -07:00
Yuxin Wu d439dfe72d handle emoji thumbnail 2020-06-27 16:32:07 -07:00
Yuxin Wu 49c4ba4346 add support for video file 2020-06-27 15:54:03 -07:00
Yuxin Wu 476bdd5971 rewrite with python3 2020-06-27 15:21:17 -07:00
Yuxin Wu 659742c929 better logging 2020-06-27 12:07:54 -07:00
Yuxin Wu 16de676f91 update 2020-06-27 02:21:57 -07:00
Yuxin Wu b246807304 update 2020-06-27 01:49:45 -07:00
Yuxin Wu 23b797b1f6 rewrite decryption logic 2020-06-27 01:45:15 -07:00
Yuxin Wu ce84066d85 sqlcipher compatibilty 2020-06-26 19:01:18 -07:00
Yuxin Wu 6bea581fa0 update in parser 2020-06-26 16:53:20 -07:00
Yuxin Wu 2d618281d9 remove my emails 2018-08-26 01:21:45 -07:00
Yuxin Wu 47141e9356 update docs 2017-11-29 04:04:22 -08:00
Yuxin Wu 14ffca8532 update docs 2017-11-29 04:02:49 -08:00
Yuxin Wu a1fd932a77 use emoji cover for failures 2017-11-29 03:55:09 -08:00
Yuxin Wu 5026cbde84 fix avatar rendering 2017-11-29 03:26:40 -08:00
Yuxin Wu d5c097277d update docs 2017-11-29 02:33:15 -08:00
futurewrgandYuxin Wu dcad5ed99d 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 265b57755d fix dependencies (fix #49) 2017-10-14 23:55:42 -07:00
Yuxin Wu 7f875c761b use userid instead of nickname as identifiers everywhere. (#39) 2017-03-02 17:46:30 +08:00
Yuxin Wu 1406453e47 update README (#39) 2017-03-02 12:09:30 +08:00
Yuxin Wu 9eecc6dd38 bug fix on chatroom msg handling 2017-02-09 13:24:37 +08:00
Yuxin Wu 080c751eb2 update readme 2016-10-12 14:03:10 -04:00
Yuxin Wu 1e2a625452 bug fix 2016-10-12 13:53:45 -04:00
Yuxin Wu 6ef50f5685 bugfix 2016-10-12 13:53:11 -04:00
Yuxin WuandGitHub b069338a9f Update README.md 2016-10-11 12:11:07 -04:00
Yuxin Wu a7b6d437d3 update readme 2016-10-10 01:19:59 -04:00
Yuxin Wu 32f41ae31d update readme 2016-10-10 01:19:21 -04:00
Yuxin Wu eb5066f74a bug fix 2016-09-06 02:01:32 -04:00
Jakukyo FrielandYuxin Wu 49ba23c3cd 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 37dd9f0909 see if readme looks better 2016-07-02 22:54:39 -07:00
Yuxin Wu 870a1852bf avoid using cover, and use compressed emoji.cache 2016-06-18 23:37:04 -07:00
Yuxin Wu 386cfe39b7 script improvements 2016-06-18 22:24:20 -07:00
Yuxin Wu 76efd3836a update readme 2016-06-18 10:16:41 -07:00
Yuxin Wu d0e428f2ac add help 2016-06-18 10:12:41 -07:00
Yuxin Wu 92e3d3d31c emoji cache downloadd 2016-06-18 10:04:53 -07:00
Yuxin Wu 98cc7c7fc8 support red envelope 2016-06-17 23:20:08 -07:00
Yuxin Wu 1344589d8d request emoji from cdnUrl 2016-06-17 23:13:39 -07:00
Yuxin Wu 049e3c3387 use static cover for emojis 2016-06-01 11:22:34 -07:00
Yuxin Wu 68b3f2e543 fix printing 2016-04-29 11:14:42 -04:00
Yuxin Wu 2c16fcea9d Merge pull request #27 from myGod0rz/patch-1
Update plot-num-msg-by-time.py
2016-04-23 12:50:39 -04:00
myGod0rz 8638b8d824 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 e259a5e436 dump audio 2016-04-21 18:35:38 -04:00
Yuxin Wu 2bd26b6138 update script with argparse 2016-04-21 18:13:06 -04:00
Yuxin Wu 18ee4f78fe update donate button 2016-04-16 00:26:49 -04:00
Yuxin Wu 57dfadd3d5 Merge pull request #25 from xrf/readme
Update README.md
2016-03-18 22:02:54 -04:00
Fei Yuan 6ef2ef718b Update README.md
Clarify a few things and improve the formatting.
2016-03-18 22:01:28 -04:00
Yuxin Wu 2fc7b060ff 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 02c40402bb Merge pull request #23 from xrf/iphonesubinfo
Fallback to 'service call' to obtain IMEI
2016-03-18 10:07:20 -04:00
Fei Yuan 7136819979 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 c9af436951 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 2d928222c6 update 2016-02-08 10:38:38 -05:00
Yuxin Wu 90f2b47ca5 list chats 2016-01-21 00:53:53 -05:00
Yuxin Wu 06c37f0b63 update avatar parsing 2016-01-21 00:16:04 -05:00
Yuxin Wu 77191e1885 group message with proper handling of user name 2016-01-20 14:12:14 -05:00
Yuxin Wu 7510fba1c1 Update README.md 2016-01-14 19:43:29 -05:00
Yuxin Wu 6450b419ad Update README.md 2016-01-10 01:27:20 -05:00
Yuxin Wu 70e953d499 Delete DOES_IT_WORK.md 2016-01-10 01:26:44 -05:00
Yuxin Wu 9d55ec15a6 Update DOES_IT_WORK.md 2016-01-10 01:20:04 -05:00
Yuxin Wu 4338e34694 Update DOES_IT_WORK.md 2016-01-10 01:16:47 -05:00
Yuxin Wu 433578f4bf add does-it-work 2016-01-10 01:14:22 -05:00
Yuxin Wu 32312072ab use empty avatar in failure 2016-01-08 21:38:21 -05:00
ymy513 0d8136bed5 Welcome 2016-01-05 00:10:28 -05:00
ppwwyyxx e84fc52eeb add gitattr 2015-11-21 22:04:49 -05:00
ppwwyyxx 2990f36483 fix when contacts are deleted 2015-11-09 10:48:41 -05:00
ppwwyyxx 7763aecfa3 bug fix 2015-11-03 23:25:45 -05:00
ppwwyyxx 8db2d6d11a use newer version of sqlcipher 2015-11-03 15:17:56 -05:00
Yuxin Wu 1d5ef17499 Merge pull request #15 from SwordYork/master
find correct avatar.block.xxxxx file (maybe)
2015-10-09 23:13:58 -04:00
Sword York c7a1923403 find correct avatar.block.xxxxx file (maybe) 2015-10-10 11:01:19 +08:00
ppwwyyxx 5dfc5de86e update README 2015-10-09 15:15:35 -04:00
ppwwyyxx 630c15fed2 clean-up legacy code 2015-10-09 15:07:28 -04:00
ppwwyyxx 3ee1d56926 update scripts for avatar code 2015-10-09 14:54:19 -04:00
Yuxin Wu 4978d20847 Merge pull request #14 from SwordYork/master
Avatar support for new version wechat
2015-10-09 13:36:49 -04:00
Sword York 0b52a123c4 more detail usage 2015-10-09 15:16:50 +08:00
Sword York 5c5ea71a62 fix bug, add avt_db support 2015-10-09 10:21:41 +08:00
Sword York 9b24348465 read avatar from sfs/avatar.block.0000x 2015-10-08 18:16:01 +08:00
ppwwyyxx 5723e2a078 more logging in android-interact 2015-08-30 09:22:04 -04:00
ppwwyyxx e9189ceced use root in adb 2015-08-27 20:24:08 -04:00
ppwwyyxx 33bff2c11e fix donation 2015-08-23 11:19:46 -04:00
ppwwyyxx d5531c2658 add donation 2015-08-23 11:04:14 -04:00
ppwwyyxx 020c550b21 fix import bug 2015-08-23 10:27:10 -04:00
ppwwyyxx 70e60e5b9d add a chinese title 2015-06-25 21:20:43 +08:00
ppwwyyxx 43cda58e71 move utils to common 2015-06-18 00:07:15 +08:00
ppwwyyxx a55f8ec341 fix rename of a script 2015-06-17 10:10:26 +08:00
ppwwyyxx fff040279c update readme 2015-06-17 10:07:18 +08:00
ppwwyyxx 8810f35c22 fix readme 2015-06-16 23:54:45 +08:00
ppwwyyxx 476997032b add silk and corresponding code, for wechat 6.0+ 2015-06-16 23:52:27 +08:00
ppwwyyxx c2148e17bf make jquery loaded first 2015-06-16 23:47:53 +08:00
Yuxin Wu 593a2040a0 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 45566b28b6 Revert "Changed the formatting of the generated HTML" 2015-06-16 23:45:13 +08:00
ppwwyyxx 1c1325911a add silk parsing code 2015-06-16 23:17:43 +08:00
ppwwyyxx 39587c9c3b rename scripts 2015-06-16 22:25:18 +08:00
ppwwyyxx 460a0b82d7 move sqlcipher to third-party 2015-06-16 22:20:39 +08:00
Yuxin Wu 38031f3317 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 3cdcbeb020 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 bad354e6dd bug fix 2015-05-25 15:23:06 +08:00
ppwwyyxx 538a2e0f66 some small fix.. 2015-05-25 15:17:17 +08:00
Yuxin Wu 5d0acef6db 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) af5db3cc6c Use XML parsers to parse messages 2015-04-27 20:03:24 +08:00
Jonathon Padfield 208deee0e3 Accidentally used spaces instead of tabs in markdown list item. 2015-04-27 16:03:40 +08:00
Yuxin Wu ef2ea15d9b 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 35f2a2956a 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 6ac9bdb1cc 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 e88df3496a Added more meaningful error message when the user provides an unknown contact name. 2015-04-27 13:29:22 +08:00
Jonathon Padfield e63e4de3e5 Fixed smiley unicode parsing to work on Python 2.7, Mac OSX 2015-04-27 13:28:45 +08:00
Jonathon Padfield 016462d4a9 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 4a5cefbcd1 add encryption in todo 2015-04-19 17:32:54 +08:00
ppwwyyxx b80bbb39c2 some small change 2015-04-12 21:10:11 +08:00
ppwwyyxx f8c2f2bee4 msg get through sqlite 2015-03-28 00:09:12 +08:00
ppwwyyxx 24b1ad56e8 msg to libchat seems to work 2015-03-25 23:15:22 +08:00
ppwwyyxx 3e671e16a3 rename 2015-03-25 17:47:31 +08:00
ppwwyyxx d556050327 add libchat db 2015-03-25 16:44:12 +08:00
ppwwyyxx e5cdf99d19 add libchat 2015-03-25 15:41:29 +08:00
Yuxin Wu 31322be438 Merge pull request #3 from chaserhkj/chaserhkj
Added a stat report script.
2015-03-08 10:54:39 +08:00
HuangKangJing 0d8cbb2762 Added a stat report script. 2015-03-07 21:22:57 +08:00
Yuxin Wu 8f12344e50 Merge pull request #2 from chaserhkj/chaserhkj
Fix 2 problems
2015-03-02 11:40:53 +08:00
HuangKangJing 15a6eb79bf Image is in PIL.Image now on most platforms 2015-03-02 11:20:28 +08:00
HuangKangJing 6c60b3d717 dill should be added to dependencies 2015-03-02 11:19:59 +08:00
ppwwyyxx 929bd0da89 bug fix 2015-02-02 13:32:11 +08:00
Yuxin Wu bb2a924e1f 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 fe1113d7c2 remove useless file 2015-01-20 14:45:46 +08:00
Vury Leo 27a43b653d Add darwin version of sqlcipher v2.1.1 2015-01-20 12:03:44 +08:00
Vury Leo 6e94b8c9ad Add bsd compatibility 2015-01-19 14:35:40 +08:00
ppwwyyxx 6ea8e2e377 add future 2015-01-12 00:19:11 +08:00
ppwwyyxx 9793975465 change order or example 2015-01-12 00:09:51 +08:00
ppwwyyxx 45669a38b5 add example 2015-01-12 00:09:00 +08:00
ppwwyyxx 59036d3a30 smaller size by avoid repeating image and unnecessary smiley css 2015-01-11 23:43:56 +08:00
Yuxin Wu 93c1eecc23 add license 2015-01-11 21:57:58 +08:00
ppwwyyxx fbd11cce20 add note about sqlcipher 2015-01-11 21:53:42 +08:00
ppwwyyxx 17d53f4d95 fix dirname check 2015-01-09 22:43:24 +08:00
ppwwyyxx 838dbd1a85 fix todos in readme 2015-01-09 22:32:11 +08:00
ppwwyyxx 2debbab21c better logger, locate voice message 2015-01-09 22:29:01 +08:00
ppwwyyxx eb22611b78 not required 2015-01-09 12:11:23 +08:00
ppwwyyxx 98bec94d48 resource dir 2015-01-09 12:09:53 +08:00
ppwwyyxx 9ee58f1ee1 add adb in readme 2015-01-09 12:08:22 +08:00
ppwwyyxx 3993b8e8b5 add automatic db decrypt 2015-01-09 12:06:09 +08:00
ppwwyyxx 5fb536dbbe change decrypted db name 2015-01-09 12:03:16 +08:00
ppwwyyxx 7ee5312783 add screenshots 2015-01-09 11:57:12 +08:00
ppwwyyxx d4772c679d speed up voice rendering 2015-01-08 00:53:16 +08:00
ppwwyyxx 587e8040f6 fix some efficient issue 2015-01-08 00:25:09 +08:00
ppwwyyxx 54fef961b0 fix 1048625 2015-01-07 23:46:08 +08:00
ppwwyyxx 1cddbadf8c readme and prompt 2015-01-07 22:19:17 +08:00
ppwwyyxx b325ab24a9 add video TODO 2015-01-07 22:03:56 +08:00
ppwwyyxx 2ad3e4fddc group chat 2015-01-07 22:02:53 +08:00
ppwwyyxx 8d8ebf925c update readme 2015-01-07 21:13:30 +08:00
ppwwyyxx 16a2dd769b rename template.html 2015-01-03 00:24:52 +08:00
ppwwyyxx 7952910152 refactor avatar css 2015-01-02 23:24:14 +08:00
ppwwyyxx e0eaa15e3c fix bug in android script 2014-12-31 23:28:29 +08:00
ppwwyyxx d90ad5b7cd some bug fix 2014-12-27 00:18:42 +08:00
ppwwyyxx 2b6f4c6d28 32bit support 2014-12-26 00:17:23 +08:00
ppwwyyxx 0bb1104d98 add pysox, 2x faster 2014-12-25 23:20:35 +08:00
ppwwyyxx 3ed3dbef88 bug fix 2014-12-25 10:18:51 +08:00
ppwwyyxx d9a9fa8e25 various bug fixes 2014-12-25 10:04:54 +08:00
ppwwyyxx 9f5e01b990 fix readme 2014-12-24 23:25:27 +08:00
ppwwyyxx d97a18b4f2 update readme 2014-12-24 22:54:22 +08:00
ppwwyyxx 0a6ba53d0f multiple html output 2014-12-23 00:03:40 +08:00
ppwwyyxx 927c53876f bug fix in time-slice 2014-12-22 23:07:36 +08:00
ppwwyyxx 562abef35e add time in html 2014-12-22 22:49:33 +08:00
ppwwyyxx 7e23b45294 fix missing internal emoji 2014-12-22 22:00:20 +08:00
ppwwyyxx db3434d539 minor fix 2014-12-22 16:53:23 +08:00
ppwwyyxx f7b66958e8 add internal emoji files 2014-12-22 09:57:39 +08:00
ppwwyyxx 93448f44f9 add internal emoji 2014-12-22 09:49:30 +08:00
ppwwyyxx 7d463de45e minor fixes.. 2014-12-21 23:54:19 +08:00
ppwwyyxx 7b5daa5b58 some minor fixes 2014-12-21 23:35:54 +08:00
ppwwyyxx 7710efd19d fix DOM parsing 2014-12-21 21:54:26 +08:00
ppwwyyxx 41982c3f2d fix missing file 2014-12-21 21:23:44 +08:00
ppwwyyxx 1a5ae2b1f2 fix emoji size 2014-12-21 21:15:51 +08:00
ppwwyyxx 337c1f8acf emoji works 2014-12-20 20:30:46 +08:00
ppwwyyxx e970fa2fb5 emoji -> smiley 2014-12-20 17:55:46 +08:00
ppwwyyxx acbd81e5fa some bug fixes 2014-12-20 15:47:55 +08:00
ppwwyyxx 4ad694e6bc emoji done 2014-12-17 00:09:28 +08:00
ppwwyyxx c18010356d a lot of emoji file 2014-12-16 23:51:14 +08:00
ppwwyyxx 42d6619b06 emoji works for only some format... 2014-12-15 22:40:42 +08:00
ppwwyyxx 6f9af98ee6 add emoji parse 2014-12-14 01:57:44 +08:00
ppwwyyxx a3b641df2e bigimg fancybox work 2014-12-13 00:39:10 +08:00
ppwwyyxx 45379cb8b5 fix title hight 2014-12-12 23:42:29 +08:00
ppwwyyxx bf7697afaf add logger 2014-12-12 23:15:59 +08:00
ppwwyyxx f136829a2b image thumbnail 2014-11-23 23:12:07 +08:00
ppwwyyxx 618e12ec00 voice works! 2014-11-23 18:04:22 +08:00
ppwwyyxx a86e63cb7f voice works 2014-11-23 17:53:00 +08:00
ppwwyyxx bcc5925a55 update readme 2014-11-23 16:45:10 +08:00
ppwwyyxx 9d65b23737 avatar done 2014-11-23 16:38:04 +08:00
ppwwyyxx e73192fd87 update readme 2014-11-22 23:57:48 +08:00
ppwwyyxx 6874885881 generate msg-only html 2014-11-22 23:52:50 +08:00
ppwwyyxx 7284d72d2b change file name 2014-11-22 22:33:02 +08:00
ppwwyyxx 9a509ab2d8 add Render 2014-11-22 22:30:10 +08:00
ppwwyyxx d3c20d3490 add Res.py 2014-11-22 21:59:59 +08:00
ppwwyyxx d819eca642 video/links 2014-11-22 08:26:10 +08:00
ppwwyyxx 199ced0087 plt by hour 2014-11-21 15:57:44 +08:00
ppwwyyxx 55a33c1cf6 plot 2014-11-21 15:19:35 +08:00
ppwwyyxx 079832fc78 add todo 2014-11-21 14:41:19 +08:00
ppwwyyxx 6742b27352 add readme and driver 2014-11-21 14:37:18 +08:00
ppwwyyxx 0736cdb1c8 msg can work 2014-11-21 14:09:09 +08:00
ppwwyyxx a77147a3d3 init 2014-11-21 13:09:01 +08:00
110 changed files with 473 additions and 3463 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
output/
resource
*.db
*.db.decoded
*.db.decrypted
*2015*
*2017*
2020*
+44 -27
View File
@@ -1,17 +1,14 @@
## Render WeChat Messages from Android
## Dump WeChat Messages from Android
## 安卓微信消息记录分析工具
## 导出安卓微信消息记录
WeChat, as the most popular mobile IM app in China, doesn't provide any methods to read its message history.
Customers are not able to analyze their own chat messages or interact with other data analysis tools.
WeChat, as the most popular mobile IM app in China, doesn't provide any methods to export structured message history.
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.
We reverse-engineered the storage protocol of WeChat messages, and
provide this tool to decrypt and parse WeChat messages on a rooted android phone.
It can also render the messages into self-contained html files including voice messages, images, emojis, videos, etc.
The tool is last verified to work with latest version of WeChat on 2025/01/01.
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).
## How to use:
@@ -19,30 +16,51 @@ If the tool works for you, please take a moment to add your phone/OS to [the wik
#### Dependencies:
+ adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash.
+ Python >= 3.8
+ [sqlcipher](https://github.com/sqlcipher/sqlcipher) >= 4.1
+ sox (command line tools)
+ Silk audio decoder (included; build it with `./third-party/compile_silk.sh`)
+ Other python dependencies: `pip install -r requirements.txt`.
#### Get Necessary Data:
1. Pull database file and (for older WeChat versions) avatar index:
1. Pull database file and (for older wechat versions) avatar index:
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
+ Manual:
+ 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.
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device.
2. Decode `EnMicroMsg.db`. We do not provide instructions to do that.
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:
2. Decrypt database file:
+ Automatic: `./decrypt-db.py decrypt --input EnMicroMsg.db`
+ Manual:
+ Get WeChat uin (an integer), possible ways are:
+ `./decrypt-db.py uin`, which looks for uin in `/data/data/com.tencent.mm/shared_prefs/`
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
+ Get your device id (a positive integer), possible ways are:
+ `./decrypt-db.py imei` implements some ways to find device id.
+ Call `*#06#` on your phone
+ Find IMEI in system settings
+ Decrypt database with combination of uin and device id:
```
./decrypt-db.py decrypt --input EnMicroMsg.db --imei <device id> --uin <uin>
```
NOTE: you may need to try different ways to get device id and find one that can decrypt the
database. Some phones may have multiple IMEIs, you may need to try them all.
See [#33](https://github.com/ppwwyyxx/wechat-dump/issues/33).
The command will dump decrypted database at `EnMicroMsg.db.decrypted`.
If the above decryption doesn't work, you can also try the [password cracker](https://github.com/chg-hou/EnMicroMsg.db-Password-Cracker)
to brute-force the key. The encryption key is not very strong.
3. Copy the 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 res`
+ Change `RES_DIR` in the script if the location of these directories is different on your phone.
For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
For older version of wechat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
+ This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive,
`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`.
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.
See [WXGFDecoder](WXGFDecoder) for instructions.
4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2)
and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering.
@@ -50,16 +68,16 @@ If the tool works for you, please take a moment to add your phone/OS to [the wik
tar xf emoji.cache.tar.bz2
#### Run:
+ Parse and dump text messages of __every__ chat (requires decoded database):
+ Parse and dump text messages of __every__ chat (requires decrypted database):
```
./dump-msg.py decoded.db output_dir
./dump-msg.py decrypted.db output_dir
```
+ List all chats (required decoded database):
+ List all chats (required decrypted database):
```
./list-chats.py decoded.db
./list-chats.py decrypted.db
```
+ Generate statistics report on text messages (requires `output_dir` from `./dump-msg.py`):
@@ -68,14 +86,15 @@ If the tool works for you, please take a moment to add your phone/OS to [the wik
./count-message.sh output_dir
```
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires decoded database and `resource`):
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires decrypted database and `resource`):
```
./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.
The output file is `output.html`.
Check `./dump-html.py -h` to use different paths.
### Examples:
Screenshots of generated html:
@@ -85,9 +104,7 @@ Screenshots of generated html:
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
### TODO List (help needed!)
* After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 -
they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis.
* Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu).
* __IMPORTANT__ Some emojis and chat images are stored in a proprietary "wxgf" format. We don't yet know how to decode this format.
* Fix rare unhandled message types: > 10000 and < 0
* Better user experiences... see `grep 'TODO' wechat -R`
-11
View File
@@ -1,11 +0,0 @@
*.iml
.gradle
local.properties
.idea
app/release
.DS_Store
build
captures
.externalNativeBuild
.cxx
local.properties
-29
View File
@@ -1,29 +0,0 @@
# 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
@@ -1 +0,0 @@
build
-55
View File
@@ -1,55 +0,0 @@
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
@@ -1,21 +0,0 @@
# 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
@@ -1,29 +0,0 @@
<?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>
@@ -1,227 +0,0 @@
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();
}
}
@@ -1,37 +0,0 @@
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;
}
}
@@ -1,19 +0,0 @@
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;
}
}
@@ -1,34 +0,0 @@
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;
}
}
@@ -1,19 +0,0 @@
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;
}
}
@@ -1,37 +0,0 @@
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;
}
}
@@ -1,19 +0,0 @@
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;
}
}
@@ -1,14 +0,0 @@
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);
}
@@ -1,101 +0,0 @@
//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;
}
}
@@ -1,9 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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>
@@ -1,170 +0,0 @@
<?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>
@@ -1,30 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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>
@@ -1,88 +0,0 @@
<?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>
@@ -1,22 +0,0 @@
<?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>
@@ -1,9 +0,0 @@
<?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>
@@ -1,22 +0,0 @@
<?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>
@@ -1,19 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -1,25 +0,0 @@
<?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.
@@ -1,16 +0,0 @@
<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>
@@ -1,10 +0,0 @@
<?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>
@@ -1,5 +0,0 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
@@ -1,6 +0,0 @@
<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>
@@ -1,16 +0,0 @@
<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>
@@ -1,13 +0,0 @@
<?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>
@@ -1,19 +0,0 @@
<?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
@@ -1,4 +0,0 @@
// 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
@@ -1,21 +0,0 @@
# 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
@@ -1,30 +0,0 @@
[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
@@ -1,6 +0,0 @@
#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
-185
View File
@@ -1,185 +0,0 @@
#!/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
@@ -1,89 +0,0 @@
@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.

Before

Width:  |  Height:  |  Size: 85 KiB

-23
View File
@@ -1,23 +0,0 @@
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")
+22 -28
View File
@@ -7,12 +7,18 @@ cd "$PROG_DIR"
# Please check that your path is the same, since this might be different among devices
# RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
RES_DIR="/data/data/com.tencent.mm/MicroMsg"
RES_DIR="/data/data/com.tencent.mm"
MM_DIR="/data/data/com.tencent.mm"
echo "Starting rooted adb server..."
adb root
if [[ $1 == "db" || $1 == "res" ]]; then
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 shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}')
userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' | sed 's/[^0-9a-z]//g' \
| awk '{if (length() == 32) print}')
numUser=$(echo "$userList" | wc -l)
# choose the first user.
chooseUser=$(echo "$userList" | head -n1)
@@ -29,21 +35,20 @@ if [[ $1 == "db" || $1 == "res" ]]; then
echo "Pulling resources... "
for d in avatar image2 voice2 emoji video sfs; do
echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..."
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null |
busybox base64" | base64 -di | tar xzf - --strip-components 5
adb shell "cd $RES_DIR/$chooseUser &&
busybox tar czf - $d 2>/dev/null | busybox base64" |
base64 -di | tar xzf -
[[ -d $d ]] && continue
echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..."
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" |
base64 -di | tar xzf - --strip-components 5
adb shell "cd $RES_DIR/$chooseUser &&
tar czf - $d 2>/dev/null | base64" | base64 -di | tar xzf -
[[ -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"
)
@@ -55,29 +60,18 @@ if [[ $1 == "db" || $1 == "res" ]]; then
echo "Total size: $(du -sh | cut -f1)"
)
else
for f in EnMicroMsg.db sfs/avatar.index; do
echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..."
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null |
busybox base64" | base64 -di | tar xzf - --strip-components 5
[[ -f $f ]] && continue
echo "Trying to download $RES_DIR/$chooseUser/$f with tar & base64 ..."
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | base64" |
base64 -di | tar xzf - --strip-components 5
[[ -f $f ]] && continue
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"
echo "Pulling database and avatar index file..."
adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db
[[ -f EnMicroMsg.db ]] && \
echo "Database successfully downloaded to EnMicroMsg.db" || {
>&2 echo "Failed to pull database by adb!"
exit 1
}
done
echo "Database and avatar index file successfully downloaded"
adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index
[[ -f avatar.index ]] && echo "Avatar index successfully downloaded to avatar.index"
fi
else
echo "Usage: $0 <res|db>"
exit 1
fi
Executable
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shlex
import sys
import re
import struct
import argparse
import logging
import javaobj
from pyquery import PyQuery
from pysqlcipher3 import dbapi2 as sqlite
from hashlib import md5
from wechat.common.procutil import subproc_succ, subproc_call
logger = logging.getLogger("wechat")
RES_DIR = "/mnt/sdcard/tencent/MicroMsg"
MM_DIR = "/data/data/com.tencent.mm"
def adb_command(command):
return subproc_succ("adb shell su -c " + shlex.quote(command))
def get_uin():
candidates = []
try:
uin = None
out = adb_command(f"cat {MM_DIR}/shared_prefs/system_config_prefs.xml")
for line in out.decode('utf-8').split("\n"):
if "default_uin" in line:
line = PyQuery(line)
uin = line.attr["value"]
break
uin = int(uin)
except Exception:
logger.warning("uin not found in system_config_prefs.xml")
else:
candidates.append(uin)
logger.info(f"found uin={uin} in system_config_prefs.xml")
try:
uin = None
out = adb_command(f"cat {MM_DIR}/shared_prefs/com.tencent.mm_preferences.xml")
for line in out.decode('utf-8').split("\n"):
if "last_login_uin" in line:
line = PyQuery(line)
uin = line.text()
break
uin = int(uin)
except Exception:
logger.warning("uin not found in com.tencent.mm_preferences.xml")
else:
candidates.append(uin)
logger.info(f"found uin={uin} in com.tencent.mm_preferences.xml")
try:
uin = None
out = adb_command(f"cat {MM_DIR}/shared_prefs/auth_info_key_prefs.xml")
for line in out.decode('utf-8').split("\n"):
if "auth_uin" in line:
line = PyQuery(line)
uin = line.attr["value"]
break
uin = int(uin)
except Exception:
logger.warning("uin not found in auth_info_key_prefs.xml")
else:
candidates.append(uin)
logger.info(f"found uin={uin} in auth_info_key_prefs.xml")
try:
out = adb_command(f"cat {MM_DIR}/MicroMsg/systemInfo.cfg")
uin = int(javaobj.loads(out).get(1, 0))
except:
logger.warning("default uin not found in systemInfo.cfg")
else:
candidates.append(uin)
logger.info(f"found uin={uin} in systemInfo.cfg")
candidates = list({str(x) if isinstance(x, int) else x
for x in candidates if x != 0})
logger.info(f"Possible uin: {candidates}")
return candidates
def get_imei():
candidates = []
class Parcel(object):
# https://gist.github.com/ktnr74/60ac7bcc2cd17b43f2cb
def __init__(self, text):
if text.startswith(b'Result: Parcel(') and text.endswith(b'\')'):
self.data = b''.join([struct.pack('<L', int(x, 16)) for x in re.findall(b'([0-9a-f]{8}) ', text)])
self.resultcode = self.get_int(0)
else:
raise Exception('Unexpected input!')
def get_int(self, offset=4):
return int(struct.unpack('<L', self.data[offset:offset+4])[0])
def get_utf16(self, offset=4):
return (self.data[offset + 4: offset+4+self.get_int(offset) * 2]).decode('utf-16')
out = adb_command(f"service call iphonesubinfo 1")
imei = Parcel(out.strip()).get_utf16()
logger.info(f"found imei={imei} from iphonesubinfo")
candidates.append(imei)
try:
out = adb_command(f"cat {MM_DIR}/MicroMsg/CompatibleInfo.cfg")
# https://gist.github.com/ChiChou/36556fd412a9e3216abecf06e084e4d9
jobj = javaobj.loads(out)
imei = jobj[258]
except:
logger.warning("imei not found in CompatibleInfo.cfg")
else:
candidates.append(imei)
logger.info(f"found imei={imei} in CompatibleInfo.cfg")
candidates.append("1234567890ABCDEF") # https://github.com/ppwwyyxx/wechat-dump/issues/70
logger.info(f"Possible imei: {candidates}")
return list(set(candidates))
def get_key(imei, uin):
"""
Args:
imei, uin: str
"""
if isinstance(uin, str):
uin = uin.encode('ascii')
if isinstance(imei, str):
imei = imei.encode('ascii')
a = md5(imei + uin)
return a.hexdigest()[:7]
def do_decrypt(input, output, key):
conn = sqlite.connect(input)
c = conn.cursor()
version_str = list(conn.execute("PRAGMA cipher_version"))[0][0]
version = tuple([int(x) for x in version_str.split(".")[:2]])
assert version >= (4, 1), "Sqlcipher>=4.1 is required"
c.execute("PRAGMA key = '" + key + "';")
# https://github.com/sqlcipher/sqlcipher/commit/e4b66d6cc8a2b7547a32ff2c3ac52f148eba3516
c.execute("PRAGMA cipher_compatibility = 1;")
try:
c.execute("ATTACH DATABASE '" + output + "' AS db KEY '';")
except Exception as e:
logger.error(f"Decryption failed: '{e}'")
os.unlink(output)
raise
logger.info(f"Decryption succeeded! Writing database to {output} ...")
c.execute("SELECT sqlcipher_export('db');" )
c.execute("DETACH DATABASE db;" )
c.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('task', choices=['uin', 'imei', 'decrypt'])
parser.add_argument('--imei', help='overwrite imei')
parser.add_argument('--uin', help='overwrite uin')
parser.add_argument('--input', help='encrypted EnMicroMsg.db')
args = parser.parse_args()
subproc_succ("adb root")
if args.task == 'uin':
uin = get_uin()
elif args.task == 'imei':
imei = get_imei()
elif args.task == 'decrypt':
uins = [args.uin] if args.uin else get_uin()
imeis = [args.imei] if args.imei else get_imei()
output_file = args.input + ".decrypted"
assert not os.path.isfile(output_file), f"Output {output_file} exists!"
for uin in uins:
for imei in imeis:
key = get_key(imei, uin)
logger.info(f"Trying key {key} ...")
try:
do_decrypt(args.input, output_file, key)
except Exception as error:
logger.warning(f"An unexpected error occurred.{error}")
pass
else:
logger.info(f"Database dumped to {output_file}")
sys.exit()
+2 -2
View File
@@ -13,7 +13,7 @@ def get_args():
parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
parser.add_argument('--db', default='decoded.db', help='path to decoded database')
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
parser.add_argument('--res', default='resource', help='reseource directory')
args = parser.parse_args()
return args
@@ -21,7 +21,7 @@ def get_args():
if __name__ == '__main__':
args = get_args()
parser = WeChatDBParser(args.db)
res = Resource(parser, args.res)
res = Resource(parser, args.res, '')
try:
chatid = parser.get_chat_id(args.name)
+9 -16
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import os
import sys
import argparse
import logging
from datetime import datetime
from wechat.parser import WeChatDBParser
from wechat.res import Resource
from wechat.common.textutil import ensure_unicode
from wechat.render import HTMLRender
logger = logging.getLogger("wechat")
@@ -14,20 +15,17 @@ logger = logging.getLogger("wechat")
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html')
parser.add_argument('--db', default='EnMicroMsg.db.decoded',
help='path to the decoded database, e.g. EnMicroMsg.db.decoded')
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)
parser.add_argument('--output', help='output html file', default='output.html')
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat')
parser.add_argument('--res', default='resource', help='reseource directory')
args = parser.parse_args()
return args
if __name__ == '__main__':
args = get_args()
name = ensure_unicode(args.name)
output_file = args.output
parser = WeChatDBParser(args.db)
@@ -37,18 +35,13 @@ if __name__ == '__main__':
except KeyError:
sys.stderr.write(u"Valid Contacts: {}\n".format(
u'\n'.join(parser.all_chat_nicknames)))
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
sys.stderr.write(u"Couldn't find the chat {}.".format(name));
sys.exit(1)
res = Resource(parser, args.res,
wxgf_server=args.wxgf_server,
avt_db=args.avt)
res = Resource(parser, args.res, args.avt)
msgs = parser.msgs_by_chat[chatid]
logger.info(f"Number of Messages for chatid {chatid}: {len(msgs)}")
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)
htmls = render.render_msgs(msgs)
+1 -1
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger("wechat")
if __name__ == '__main__':
if len(sys.argv) != 3:
sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0]))
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
db_file = sys.argv[1]
output_dir = sys.argv[2]
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash -e
# File: decrypt-db.sh
# Date: Tue Jun 16 22:23:13 2015 +0800
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"
echo "Dump decrypted database... "
# https://github.com/sqlcipher/sqlcipher/commit/e4b66d6cc8a2b7547a32ff2c3ac52f148eba3516
sqlcipher "$MSGDB" << EOF
PRAGMA key='$KEY';
PRAGMA cipher_compatibility = 1;
ATTACH DATABASE "$output" AS db KEY "";
SELECT sqlcipher_export("db");
DETACH DATABASE db;
EOF
echo "Database successfully dumped to $output"
+27 -27
View File
@@ -2,45 +2,45 @@
# -*- coding: UTF-8 -*-
from wechat.parser import WeChatDBParser
from wechat.common.textutil import ensure_unicode
from datetime import timedelta, datetime
import numpy as np
import matplotlib.pyplot as plt
import sys
import sys, os
if len(sys.argv) != 3:
sys.exit("Usage: {0} <path to decrypted_database.db> <name>".format(sys.argv[0]))
if __name__ == '__main__':
if len(sys.argv) != 3:
sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0]))
db_file = sys.argv[1]
name = ensure_unicode(sys.argv[2])
every_k_days = 2
db_file = sys.argv[1]
name = sys.argv[2]
every_k_days = 2
parser = WeChatDBParser(db_file)
msgs = parser.msgs_by_chat[name]
times = [x.createTime for x in msgs]
start_time = times[0]
diffs = [(x - start_time).days for x in times]
max_day = diffs[-1]
parser = WeChatDBParser(db_file)
msgs = parser.msgs_by_chat[name]
times = [x.createTime for x in msgs]
start_time = times[0]
diffs = [(x - start_time).days for x in times]
max_day = diffs[-1]
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()
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
# I'm in a different time zone in this period:
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
#def real_hour(x):
#for k, v in TZ_DELTA.items():
#if x > k[0] and x < k[1]:
#print x
#return (x.hour + v + 24) % 24
#return x.hour
#for k, v in TZ_DELTA.items():
#if x > k[0] and x < k[1]:
#print x
#return (x.hour + v + 24) % 24
#return x.hour
#hours = [real_hour(x) for x in times]
#plt.ylabel("Number of msgs")
#plt.xlabel("Hour in a day")
-2
View File
@@ -6,5 +6,3 @@ requests
pysqlcipher3>=1.0.3
csscompressor
numpy
ipython
websocket-client
+1 -1
View File
@@ -21,7 +21,7 @@ def set_level_color(lvl, color):
logger.addHandler(handler)
set_level_color(logging.INFO, '\033[1;32m')
set_level_color(logging.WARN, '\033[1;33m')
set_level_color(logging.WARN, '\033[1;31m')
set_level_color(logging.ERROR, '\033[1;31m')
if __name__ == '__main__':
+4 -12
View File
@@ -10,7 +10,7 @@ import logging
import sqlite3
logger = logging.getLogger(__name__)
from .common.textutil import md5
from .common.textutil import ensure_unicode, md5
def _filename_priority(s):
@@ -49,7 +49,7 @@ class AvatarReader(object):
except Exception:
pass
def get_avatar_from_avtdir(self, avtid) -> Image.Image | None:
def get_avatar_from_avtdir(self, avtid):
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)
@@ -72,6 +72,7 @@ class AvatarReader(object):
""" username: `username` field in db.rcontact"""
if not self._use_avt:
return None
username = ensure_unicode(username)
avtid = md5(username.encode('utf-8'))
if self.avt_db is not None:
@@ -83,16 +84,7 @@ class AvatarReader(object):
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')
logger.warning("Avatar for {} not found anywhere.".format(username))
def read_img_from_block(self, filename, pos, size):
file_idx = pos >> 32
-51
View File
@@ -1,51 +0,0 @@
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
+9 -1
View File
@@ -3,6 +3,13 @@
import hashlib
import base64
def ensure_unicode(s):
if type(s) == str:
return s
elif type(s) == bytes:
return s.decode('utf-8')
raise TypeError(f"type of string is {type(s)}")
def md5(s):
m = hashlib.md5()
@@ -18,5 +25,6 @@ def get_file_md5(fname):
return md5(f.read())
def safe_filename(fname):
filename = ensure_unicode(fname)
return "".join(
[c for c in fname if c.isalpha() or c.isdigit() or c ==' ']).rstrip()
[c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()
+19 -27
View File
@@ -1,16 +1,16 @@
import os
from pathlib import Path
import logging
import tempfile
import io
import requests
import base64
import imghdr
from PIL import Image
import pickle
from Crypto.Cipher import AES
from .wxgf import WxgfAndroidDecoder, is_wxgf_buffer
from .parser import WeChatDBParser
from .common.imgutil import what as img_what
from .common.textutil import md5 as get_md5_hex, get_file_b64, get_file_md5
@@ -23,20 +23,21 @@ def _get_aes_key(md5):
# ascii representation of the first half of md5 is used as aes key
assert len(md5) == 32
return md5[:16].encode('ascii')
# ret = ""
# for ch in md5[:16]:
# ret += format(ord(ch), 'x')
# return ret
class EmojiReader:
def __init__(self,
resource_dir: str,
parser: WeChatDBParser,
*,
wxgf_decoder: WxgfAndroidDecoder,
cache_file: str=None):
"""
Args:
resource_dir: path to resource/
parser: Database parser
wxgf_decoder: Wxgf image decoder
cache_file: a cache file to store emoji downloaded from URLs.
default to a emoji.cache file under wechat-dump.
"""
@@ -47,7 +48,6 @@ class EmojiReader:
# mapping from md5 to the (cdnurl, encrypturl, aeskey)
# columns in EmojiInfo table.
self.cache_file = cache_file or DEFAULT_EMOJI_CACHE
self.wxgf_decoder = wxgf_decoder
# cache stores md5 -> (base64str, format)
if os.path.isfile(self.cache_file):
@@ -124,32 +124,27 @@ class EmojiReader:
candidates = []
def get_data_no_fallback(fname):
if img_what(fname):
if imghdr.what(fname):
data_md5 = get_file_md5(fname)
if data_md5 == md5:
return get_file_b64(fname), img_what(fname)
return get_file_b64(fname), imghdr.what(fname)
try:
content = self._decode_emoji(fname)
content = self._decrypt_emoji(fname)
data_md5 = get_md5_hex(content)
if data_md5 != md5:
if is_wxgf_buffer(content):
content = self.wxgf_decoder.decode_with_cache(fname, content)
if content is None:
if not self.wxgf_decoder.has_server():
logger.warning("wxgf decoder server is not provided. Cannot decode wxgf emojis.")
raise ValueError("Failed to decode wxgf file.")
else:
raise ValueError("Decoded data mismatch md5!")
if content.startswith(b"wxgf"):
raise ValueError("Unsupported mysterious image format: wxgf")
raise ValueError("Decrypted data mismatch md5!")
im = Image.open(io.BytesIO(content))
return (base64.b64encode(content).decode('ascii'), im.format.lower())
except Exception as e:
logger.error(f"Error decoding emoji {fname} : {str(e)}")
logger.error(f"Error decrypting emoji {fname} : {str(e)}")
def get_data_fallback(fname):
if not img_what(fname):
if not imghdr.what(fname):
return # fallback files are not encrypted
return get_file_b64(fname), img_what(fname)
return get_file_b64(fname), imghdr.what(fname)
get_data_func = get_data_fallback if allow_fallback else get_data_no_fallback
results = [(x, get_data_func(x)) for x in candidates]
@@ -159,7 +154,7 @@ class EmojiReader:
return results[0][1]
return (None, None)
def _decode_emoji(self, fname):
def _decrypt_emoji(self, fname):
cipher = AES.new(self.encryption_key, AES.MODE_ECB)
with open(fname, 'rb') as f:
head = f.read(1024)
@@ -188,15 +183,12 @@ class EmojiReader:
try:
logger.info("Requesting encrypted emoji {} from {} ...".format(md5, encrypturl))
buf = requests.get(encrypturl).content
if buf == b'':
logger.error(f"Failed to download emoji {md5}")
return None, None
aeskey = bytes.fromhex(aeskey)
cipher = AES.new(aeskey, AES.MODE_CBC, iv=aeskey)
decoded_buf = cipher.decrypt(buf)
decrypted_buf = cipher.decrypt(buf)
im = Image.open(io.BytesIO(decoded_buf))
ret = (base64.b64encode(decoded_buf).decode('ascii'), im.format.lower())
im = Image.open(io.BytesIO(decrypted_buf))
ret = (base64.b64encode(decrypted_buf).decode('ascii'), im.format.lower())
self._cache_add(md5, ret)
return ret
except Exception:
+12 -111
View File
@@ -15,21 +15,19 @@ TYPE_REDENVELOPE = 436207665
TYPE_MONEY_TRANSFER = 419430449 # 微信转账
TYPE_LOCATION_SHARING = -1879048186
TYPE_REPLY = 822083633 # 回复的消息.
TYPE_FILE = 1090519089
TYPE_QQMUSIC = 1040187441
TYPE_APP_MSG = 16777265
_KNOWN_TYPES = tuple([eval(k) for k in dir() if k.startswith('TYPE_')])
_KNOWN_TYPES = [eval(k) for k in dir() if k.startswith('TYPE_')]
import re
import json
import io
import html
from pyquery import PyQuery
import xml.etree.ElementTree as ET
import logging
logger = logging.getLogger(__name__)
from .common.textutil import ensure_unicode
class WeChatMsg(object):
@@ -42,13 +40,12 @@ class WeChatMsg(object):
def __init__(self, values):
for k, v in values.items():
setattr(self, k, v)
self.known_type = self.type in _KNOWN_TYPES
if self.type not in _KNOWN_TYPES:
logger.warn("Unhandled message type: {}".format(self.type))
# only to supress repeated warning:
_KNOWN_TYPES.append(self.type)
def msg_str(self):
if self.type == TYPE_IMG:
return "Image"
elif self.type == TYPE_SPEAK:
return "Voice"
if self.type == TYPE_LOCATION:
try:
pq = PyQuery(self.content_xml_ready, parser='xml')
@@ -69,7 +66,7 @@ class WeChatMsg(object):
title = pq('title').text()
if title: # may not be correct
return "FILE:{}".format(title)
return "Link"
return "NOT IMPLEMENTED: " + self.content_xml_ready
return "URL:{}".format(url)
elif self.type == TYPE_NAMECARD:
pq = PyQuery(self.content_xml_ready, parser='xml')
@@ -93,13 +90,7 @@ class WeChatMsg(object):
return "LOCATION SHARING"
elif self.type == TYPE_EMOJI:
# TODO add emoji name
if self.content.lstrip().startswith("<"):
return "Emoji"
if not self.content:
return "Emoji"
return self.content
elif self.type == TYPE_CUSTOM_EMOJI:
return "Emoji"
elif self.type == TYPE_REDENVELOPE:
data_to_parse = io.BytesIO(self.content.encode('utf-8'))
try:
@@ -122,98 +113,13 @@ class WeChatMsg(object):
return "[Money Transfer]"
elif self.type == TYPE_REPLY:
pq = PyQuery(self.content_xml_ready)
titles = pq('title')
if len(titles) == 0:
return self.content_xml_ready
msg = titles[0].text
msg = pq('title').text()
# TODO parse reply.
return msg
elif self.type == TYPE_FILE:
pq = PyQuery(self.content_xml_ready)
titles = pq('title')
if len(titles) == 0:
return self.content_xml_ready
return "FILE:" + titles[0].text
elif self.type == TYPE_QQMUSIC:
pq = PyQuery(self.content_xml_ready)
title = pq('title')[0].text
singer = pq('des')[0].text
url = html.unescape(pq('url')[0].text)
return json.dumps(dict(
title=title, singer=singer, url=url
))
else:
# TODO replace smiley with text
return self.content
def reply_info(self):
"""Parse TYPE_REPLY payload.
Returns: {title, ref_name, ref_content, ref_type, ref_svrid}.
"""
if self.type != TYPE_REPLY:
return None
def _one_line(text: str, *, max_len: int) -> str:
text = re.sub(r"\s+", " ", (text or "")).strip()
if len(text) > max_len:
return text[: max_len - 1] + ""
return text
xml = self.content_xml_ready
idx = xml.find("<msg")
if idx != -1:
xml = xml[idx:]
try:
pq = PyQuery(xml, parser="xml")
except Exception:
return None
title = html.unescape(pq("appmsg > title").text() or pq("title").eq(0).text() or "")
ref_name_raw = pq("refermsg displayname").text() or pq("refermsg fromusr").text() or ""
ref_name = _one_line(html.unescape(ref_name_raw), max_len=80)
ref_content_raw = pq("refermsg content").text() or ""
ref_svrid_i = None
ref_svrid = pq("refermsg svrid").text() or pq("refermsg svrId").text()
if ref_svrid:
try:
ref_svrid_i = int(ref_svrid)
except Exception:
ref_svrid_i = None
ref_type_i = None
ref_type = pq("refermsg type").text()
if ref_type:
try:
ref_type_i = int(ref_type)
except Exception:
ref_type_i = None
ref_content_fallback = html.unescape(ref_content_raw or "")
if ref_type_i is None:
ref_content = ref_content_fallback
else:
try:
ref_content = WeChatMsg({"type": ref_type_i, "content": ref_content_fallback}).msg_str()
except Exception:
ref_content = ref_content_fallback
ref_content = _one_line(ref_content, max_len=200)
if not title and not ref_name and not ref_content:
return None
return {
"title": title.strip(),
"ref_name": ref_name,
"ref_content": ref_content,
"ref_type": ref_type_i,
"ref_svrid": ref_svrid_i,
}
@property
def content_xml_ready(self):
# remove xml headers to avoid possible errors it may create
@@ -226,19 +132,13 @@ class WeChatMsg(object):
self.type,
self.talker_nickname if not self.isSend else 'me',
self.createTime,
self.msg_str())
ensure_unicode(self.msg_str()))
if self.imgPath:
ret = "{}|img:{}".format(ret.strip(), self.imgPath)
ret = "{}|img:{}".format(ensure_unicode(ret.strip()), self.imgPath)
return ret
else:
return ret
def __eq__(self, r):
return self.createTime == r.createTime and \
self.talker == r.talker and \
self.isSend == r.isSend
# imgPath might change after migration.
def __lt__(self, r):
return self.createTime < r.createTime
@@ -258,3 +158,4 @@ class WeChatMsg(object):
if not emoji:
return None
return emoji.attrs['productid']
+10 -44
View File
@@ -1,13 +1,14 @@
# -*- coding: UTF-8 -*-
import sqlite3
from collections import defaultdict, Counter
from collections import defaultdict
import itertools
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
from .msg import WeChatMsg, TYPE_SYSTEM
from .common.textutil import ensure_unicode
""" tables in concern:
emojiinfo
@@ -16,28 +17,22 @@ addr_upload2
chatroom
message
rcontact
img_flag
"""
class WeChatDBParser(object):
FIELDS = ["msgSvrId","type","isSend","createTime","talker","content","imgPath"]
def __init__(self, db_fname):
""" db_fname: a decoded EnMicroMsg.db"""
""" db_fname: a decrypted EnMicroMsg.db"""
self.db_fname = db_fname
self.db_conn = sqlite3.connect(self.db_fname)
self.db_conn_bytes = sqlite3.connect(self.db_fname)
# https://stackoverflow.com/questions/22751363/sqlite3-operationalerror-could-not-decode-to-utf-8-column
self.db_conn_bytes.text_factory = lambda b: b
self.cc = self.db_conn.cursor()
self.contacts = {} # username -> nickname
self.contacts_rev = defaultdict(list)
self.msgs_by_chat = defaultdict(list)
self.emoji_groups = {}
self.emoji_info = {}
self.emoji_encryption_key = None
self.avatar_urls = {}
self._parse()
def _parse_contact(self):
@@ -48,9 +43,9 @@ SELECT username,conRemark,nickname FROM rcontact
for row in contacts:
username, remark, nickname = row
if remark:
self.contacts[username] = remark
self.contacts[username] = ensure_unicode(remark)
else:
self.contacts[username] = nickname
self.contacts[username] = ensure_unicode(nickname)
for k, v in self.contacts.items():
self.contacts_rev[v].append(k)
@@ -58,11 +53,10 @@ SELECT username,conRemark,nickname FROM rcontact
def _parse_msg(self):
msgs_tot_cnt = 0
db_msgs = self.db_conn_bytes.cursor().execute(
db_msgs = self.cc.execute(
"""
SELECT {} FROM message
""".format(','.join(WeChatDBParser.FIELDS)))
unknown_type_cnt = Counter()
for row in db_msgs:
values = self._parse_msg_row(row)
if not values:
@@ -71,9 +65,6 @@ SELECT {} FROM message
# TODO keep system message?
if not WeChatMsg.filter_type(msg.type):
self.msgs_by_chat[msg.chat].append(msg)
if not msg.known_type:
unknown_type_cnt[msg.type] += 1
logger.warning("[Parser] Unhandled messages (type->cnt): {}".format(unknown_type_cnt))
for k, v in self.msgs_by_chat.items():
self.msgs_by_chat[k] = sorted(v, key=lambda x: x.createTime)
@@ -84,14 +75,9 @@ SELECT {} FROM message
userinfo_q = self.cc.execute(""" SELECT id, value FROM userinfo """)
userinfo = dict(userinfo_q)
self.username = userinfo.get(2, None)
if self.username is None:
nickname = userinfo.get(4, None)
if nickname is not None:
self.username = self.contacts_rev.get(nickname, [None])[0]
if self.username is None:
logger.error("Cannot find username in userinfo table!")
self.username = input("Please enter your username:")
assert isinstance(self.username, str), self.username
logger.info("Your username is: {}".format(self.username))
def _parse_imginfo(self):
@@ -128,22 +114,13 @@ SELECT {} FROM message
if cdnUrl or encrypturl:
self.emoji_info[md5] = (catalog, cdnUrl, encrypturl, aeskey)
def _parse_img_flag(self):
"""Parse the img_flag table which stores avatar for each id."""
query = self.cc.execute(
""" SELECT username, reserved1 FROM img_flag """)
for row in query:
username, url = row
if url:
self.avatar_urls[username] = url
def _parse(self):
self._parse_userinfo()
self._parse_contact()
self._parse_userinfo() # depend on self.contacts
self._parse_msg()
self._parse_imginfo()
self._parse_emoji()
self._parse_img_flag()
def get_emoji_encryption_key(self):
# obtain local encryption key in a special entry in the database
@@ -157,24 +134,13 @@ SELECT {} FROM message
# process the values in a row
def _parse_msg_row(self, row):
"""Parse a record of message into my format.
Note that message are read in binary format.
"""
""" parse a record of message into my format"""
values = dict(zip(WeChatDBParser.FIELDS, row))
values['createTime'] = datetime.fromtimestamp(values['createTime']/ 1000)
if values['content']:
try:
values['content'] = values['content'].decode()
except:
logger.warning(f"Invalid byte sequence in message content (type={values['type']}, createTime={values['createTime']})")
values['content'] = 'FAILED TO DECODE'
values['content'] = ensure_unicode(values['content'])
else:
values['content'] = ''
values['talker'] = values['talker'].decode()
if values['imgPath']:
values['imgPath'] = values['imgPath'].decode()
values['createTime'] = datetime.fromtimestamp(values['createTime']/ 1000)
values['chat'] = values['talker']
try:
if values['chat'].endswith('@chatroom'):
+29 -134
View File
@@ -2,8 +2,7 @@
# -*- coding: UTF-8 -*-
import os
from collections import Counter
from functools import lru_cache
import base64
import glob
from pyquery import PyQuery
import logging
@@ -21,7 +20,7 @@ except ImportError:
css_compress = lambda x: x
from .msg import *
from .common.textutil import get_file_b64
from .common.textutil import ensure_unicode, get_file_b64
from .common.progress import ProgressReporter
from .common.timer import timing
from .smiley import SmileyProvider
@@ -33,30 +32,17 @@ TEMPLATES_FILES = {TYPE_MSG: "TP_MSG",
TYPE_EMOJI: "TP_EMOJI",
TYPE_CUSTOM_EMOJI: "TP_EMOJI",
TYPE_LINK: "TP_MSG",
TYPE_REPLY: "TP_REPLY",
TYPE_VIDEO_FILE: "TP_VIDEO_FILE",
TYPE_QQMUSIC: "TP_QQMUSIC",
TYPE_VIDEO_FILE: "TP_VIDEO_FILE"
}
@lru_cache()
def get_template(name: str | int) -> str | None:
"""Return the html template given a file name or msg type."""
if isinstance(name, int):
name = TEMPLATES_FILES.get(name, None)
if name is None:
return None
html_path = os.path.join(STATIC_PATH, f"{name}.html")
with open(html_path) as f:
return f.read()
TEMPLATES = {
k: open(os.path.join(STATIC_PATH, '{}.html'.format(v))).read()
for k, v in TEMPLATES_FILES.items()
}
class HTMLRender(object):
def __init__(self, parser, res=None):
with open(HTML_FILE) as f:
self.html = f.read()
with open(TIME_HTML_FILE) as f:
self.time_html = f.read()
self.html = ensure_unicode(open(HTML_FILE).read())
self.time_html = open(TIME_HTML_FILE).read()
self.parser = parser
self.res = res
assert self.res is not None, \
@@ -67,8 +53,8 @@ class HTMLRender(object):
self.css_string = [] # css to add
for css in css_files:
logger.info("Loading {}".format(os.path.basename(css)))
with open(css) as f:
self.css_string.append(f.read())
css = ensure_unicode((open(css).read()))
self.css_string.append(css)
js_files = glob.glob(os.path.join(LIB_PATH, 'static/*.js'))
# to load jquery before other js
@@ -76,10 +62,8 @@ class HTMLRender(object):
self.js_string = []
for js in js_files:
logger.info("Loading {}".format(os.path.basename(js)))
with open(js) as f:
self.js_string.append(f.read())
self.unknown_type_cnt = Counter()
js = ensure_unicode(open(js).read())
self.js_string.append(js)
@property
def all_css(self):
@@ -106,29 +90,24 @@ class HTMLRender(object):
return self.final_js
#@timing(total=True)
def render_msg(self, msg: WeChatMsg):
def render_msg(self, msg):
""" render a message, return the html block"""
# TODO for chatroom, add nickname on avatar
sender = u'you ' + msg.talker if not msg.isSend else 'me'
format_dict = {'sender_label': sender,
'time': msg.createTime }
if not msg.known_type:
self.unknown_type_cnt[msg.type] += 1
if(not msg.isSend and msg.is_chatroom()):
format_dict['nickname'] = '>\n <pre align=\'left\'>'+msg.talker_nickname+'</pre'
else:
format_dict['nickname'] = ' '
def fallback():
template = get_template(TYPE_MSG)
template = TEMPLATES[TYPE_MSG]
content = msg.msg_str()
content = self.smiley.replace_smileycode(content)
if not msg.known_type:
# Show raw (usually xml) content if unknown.
content = html.escape(content)
return template.format(content=content, **format_dict)
format_dict['content'] = self.smiley.replace_smileycode(content)
return template.format(**format_dict)
template = get_template(msg.type)
template = TEMPLATES.get(msg.type)
if msg.type == TYPE_SPEAK:
audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
format_dict['voice_duration'] = duration
@@ -149,80 +128,6 @@ class HTMLRender(object):
# TODO do not show fancybox when no bigimg found
format_dict['img'] = (img, 'jpeg')
return template.format(**format_dict)
elif msg.type == TYPE_QQMUSIC:
jobj = json.loads(msg.msg_str())
content = f"{jobj['title']} - {jobj['singer']}"
if msg.imgPath is not None:
# imgPath was original THUMBNAIL_DIRPATH://th_xxxxxxxxx
imgpath = msg.imgPath.split('_')[-1]
img = self.res.get_img([imgpath])
format_dict['img'] = (img, 'jpeg')
else:
template = get_template("TP_QQMUSIC_NOIMG")
return template.format(url=jobj['url'], content=content, **format_dict)
elif msg.type == TYPE_REPLY:
info = msg.reply_info()
if not info:
return fallback()
def _escape_fmt(s: str) -> str:
return s.replace("{", "{{").replace("}", "}}")
title = info.get("title") or ""
reply_to = info.get("ref_name") or "unknown"
reply_quote = info.get("ref_content") or ""
ref_svrid = info.get("ref_svrid")
if not title and not reply_quote:
return fallback()
format_dict["content"] = _escape_fmt(self.smiley.replace_smileycode(title))
format_dict["reply_to"] = _escape_fmt(reply_to)
reply_thumb_html = ""
ref_msg = getattr(self, "_msg_by_svrid", {}).get(ref_svrid) if ref_svrid is not None else None
if ref_msg is not None:
try:
if ref_msg.type == TYPE_IMG and ref_msg.imgPath:
imgpath = ref_msg.imgPath.split("_")[-1]
bigimgpath = self.parser.imginfo.get(ref_msg.msgSvrId)
fnames = [k for k in [imgpath, bigimgpath] if k]
b64 = self.res.get_img_thumb(fnames, max_size=64)
if b64:
reply_thumb_html = f'<img class="replyThumb" src="data:image/jpeg;base64,{b64}" />'
elif ref_msg.type in (TYPE_VIDEO_FILE, TYPE_WX_VIDEO) and ref_msg.imgPath:
b64 = self.res.get_video_thumb(ref_msg.imgPath, max_size=64)
if b64:
reply_thumb_html = f'<img class="replyThumb" src="data:image/jpeg;base64,{b64}" />'
elif ref_msg.type in (TYPE_EMOJI, TYPE_CUSTOM_EMOJI):
if "emoticonmd5" in ref_msg.content:
pq = PyQuery(ref_msg.content)
md5 = pq("emoticonmd5").text()
else:
md5 = ref_msg.imgPath
if md5:
emoji_img, fmt = self.res.get_emoji_by_md5(md5)
if emoji_img and fmt:
fmt = fmt.lower()
if fmt == "jpg":
fmt = "jpeg"
reply_thumb_html = (
f'<img class="replyThumb replyThumbEmoji" '
f'src="data:image/{fmt};base64,{emoji_img}" />'
)
except Exception:
logger.exception("Failed to render reply thumbnail (%s).", ref_svrid)
if reply_thumb_html:
reply_quote_html = reply_thumb_html
else:
quote_text = self.smiley.replace_smileycode(reply_quote)
reply_quote_html = f'<span class="replyText">{quote_text}</span>'
format_dict["reply_quote_html"] = _escape_fmt(reply_quote_html)
template = template or get_template(TYPE_MSG)
return template.format(**format_dict)
elif msg.type == TYPE_EMOJI or msg.type == TYPE_CUSTOM_EMOJI:
if 'emoticonmd5' in msg.content:
pq = PyQuery(msg.content)
@@ -240,25 +145,16 @@ class HTMLRender(object):
import IPython as IP; IP.embed()
return template.format(**format_dict)
elif msg.type == TYPE_LINK:
pq = PyQuery(msg.content_xml_ready)
url = pq('url').text()
if url:
try:
title = pq('title')[0].text
except Exception as e:
logger.warning('No title found in LINK message: ' + str(e))
title = url
content = '<a target="_blank" href="{0}">{1}</a>'.format(url, title)
content = msg.msg_str()
# TODO show a short link with long href, if link too long
if content.startswith(u'URL:'):
url = content[4:]
content = u'URL:<a target="_blank" href="{0}">{0}</a>'.format(url)
format_dict['content'] = content
return template.format(**format_dict)
elif msg.type == TYPE_VIDEO_FILE:
video = self.res.get_video(msg.imgPath)
if video is None:
logger.warning(f"Cannot find video {msg.imgPath} ({msg.createTime})")
# fallback
format_dict['content'] = f"VIDEO FILE {msg.imgPath}"
return get_template(TYPE_MSG).format(**format_dict)
elif video.endswith(".mp4"):
if video.endswith(".mp4"):
video_str = get_file_b64(video)
format_dict["video_str"] = video_str
return template.format(**format_dict)
@@ -266,7 +162,10 @@ class HTMLRender(object):
# only has thumbnail
image_str = get_file_b64(video)
format_dict["img"] = (image_str, 'jpeg')
return get_template(TYPE_IMG).format(**format_dict)
return TEMPLATES[TYPE_IMG].format(**format_dict)
# fallback
format_dict['content'] = f"VIDEO FILE {msg.imgPath}"
return TEMPLATES_FILES[TYPE_MSG].format(**format_dict)
elif msg.type == TYPE_WX_VIDEO:
# TODO: fetch video from resource
return fallback()
@@ -298,8 +197,7 @@ class HTMLRender(object):
)
def prepare_avatar_css(self, talkers):
with open(FRIEND_AVATAR_CSS_FILE) as f:
avatar_tpl = f.read()
avatar_tpl= ensure_unicode(open(FRIEND_AVATAR_CSS_FILE).read())
my_avatar = self.res.get_avatar(self.parser.username)
css = avatar_tpl.format(name='me', avatar=my_avatar)
@@ -310,8 +208,6 @@ class HTMLRender(object):
def render_msgs(self, msgs):
""" render msgs of one chat, return a list of html"""
chatid = msgs[0].chat
self._msg_by_svrid = {m.msgSvrId: m for m in self.parser.msgs_by_chat.get(chatid, msgs)}
if msgs[0].is_chatroom():
talkers = set([m.talker for m in msgs])
else:
@@ -328,7 +224,6 @@ class HTMLRender(object):
slice_by_size = MessageSlicerBySize().slice(msgs)
ret = [self._render_partial_msgs(s) for s in slice_by_size]
self.prgs.finish()
logger.warning("[HTMLRenderer] Unhandled messages (type->cnt): {}".format(self.unknown_type_cnt))
return ret
if __name__ == '__main__':
+33 -94
View File
@@ -1,23 +1,23 @@
# -*- coding: UTF-8 -*-
import glob
import os
import re
from PIL import Image
import requests
import time
import io
import base64
import logging
logger = logging.getLogger(__name__)
import imghdr
from multiprocessing import Pool
import atexit
from .emoji import EmojiReader
from .avatar import AvatarReader
from .common.imgutil import what as img_what
from .common.textutil import md5 as get_md5_hex, get_file_b64
from .common.timer import timing
from .msg import TYPE_SPEAK
from .audio import parse_wechat_audio_file
from .wxgf import WxgfAndroidDecoder, is_wxgf_file
LIB_PATH = os.path.dirname(os.path.abspath(__file__))
VOICE_DIRNAME = 'voice2'
@@ -26,19 +26,13 @@ EMOJI_DIRNAME = 'emoji'
VIDEO_DIRNAME = 'video'
JPEG_QUALITY = 50
THUMB_JPEG_QUALITY = 35
class Resource(object):
""" Multimedia resources parser."""
def __init__(self, parser,
res_dir: str,
*,
wxgf_server: str | None = None,
avt_db: str | None = None):
""" multimedia resources in chat"""
def __init__(self, parser, res_dir, avt_db):
"""
Args:
res_dir: path to the resource directory
wxgf_server: "hostname:port" that points to the wxgf converter android app
avt_db: "avatar.index" file that only exists in old versions of wechat
"""
def check(subdir):
@@ -53,8 +47,7 @@ class Resource(object):
self.voice_dir = os.path.join(res_dir, VOICE_DIRNAME)
self.video_dir = os.path.join(res_dir, VIDEO_DIRNAME)
self.avt_reader = AvatarReader(res_dir, avt_db)
self.wxgf_decoder = WxgfAndroidDecoder(wxgf_server)
self.emoji_reader = EmojiReader(res_dir, self.parser, wxgf_decoder=self.wxgf_decoder)
self.emoji_reader = EmojiReader(res_dir, self.parser)
def _get_voice_filename(self, imgpath):
fname = get_md5_hex(imgpath.encode('ascii'))
@@ -85,24 +78,12 @@ class Resource(object):
self.voice_cache = [pool.apply_async(parse_wechat_audio_file,
(self._get_voice_filename(k),)) for k in voice_paths]
def get_avatar(self, username) -> str:
def get_avatar(self, username):
""" return base64 unicode string"""
im = self.avt_reader.get_avatar(username)
if im is None:
# Try downloading the avatar directly.
avatar_url = self.parser.avatar_urls.get(username)
if avatar_url is None:
return ""
logger.info(f"Requesting avatar of {username} from {avatar_url} ...")
try:
r = requests.get(avatar_url).content
im = Image.open(io.BytesIO(r))
except Exception:
logger.exception(f"Failed to fetch avatar of {username}.")
return ""
else:
self.avt_reader.save_avatar_to_avtdir(username, im)
logger.warning(f"Cannot find avatar for {username}.")
return ""
buf = io.BytesIO()
try:
im.save(buf, 'JPEG', quality=JPEG_QUALITY)
@@ -161,82 +142,40 @@ class Resource(object):
"""
fnames = [k for k in fnames if k] # filter out empty string
big_file, small_file = self._get_img_file(fnames)
big_file = self._img_file_to_jpg_b64(big_file)
if big_file:
return big_file
return self._img_file_to_jpg_b64(small_file)
def _img_file_to_jpg_b64(self, img_file: str, *, max_size: int | None = None, quality: int = JPEG_QUALITY) -> str | None:
if not img_file:
return None
# True jpeg. Simplest case. Avoid re-compressing.
if max_size is None and img_file.endswith('jpg') and img_what(img_file) == 'jpeg':
def get_jpg_b64(img_file):
if not img_file:
return None
if not img_file.endswith('jpg') and \
imghdr.what(img_file) != 'jpeg':
try:
im = Image.open(open(img_file, 'rb'))
except:
return None
buf = io.BytesIO()
im.convert('RGB').save(buf, 'JPEG', quality=JPEG_QUALITY)
return base64.b64encode(buf.getvalue()).decode('ascii')
with open(img_file, 'rb') as f:
if f.read(4) == b'wxgf':
logger.warning(f"Don't know how to decode wxgf image {img_file}")
return None
return get_file_b64(img_file)
if is_wxgf_file(img_file):
start = time.time()
buf = self.wxgf_decoder.decode_with_cache(img_file, None)
if buf is None:
if not self.wxgf_decoder.has_server():
logger.warning("wxgf decoder server is not provided. Cannot decode wxgf images. Please follow instructions to create wxgf decoder server if these images need to be decoded.")
else:
logger.error("Failed to decode wxgf file: {}".format(img_file))
return None
else:
elapsed = time.time() - start
if elapsed > 0.01 and self.wxgf_decoder.has_server():
logger.info(f"Decoded {img_file} in {elapsed:.2f} seconds")
else:
with open(img_file, "rb") as f:
buf = f.read()
# If we don't need resize/convert and it's already jpeg, avoid re-compressing.
if max_size is None and img_what(file=None, h=buf) == 'jpeg':
return base64.b64encode(buf).decode('ascii')
try:
im = Image.open(io.BytesIO(buf))
except Exception:
return None
try:
im = im.convert("RGB")
if max_size:
im.thumbnail((max_size, max_size))
bufio = io.BytesIO()
im.save(bufio, "JPEG", quality=quality)
return base64.b64encode(bufio.getvalue()).decode("ascii")
except Exception:
return None
def get_img_thumb(self, fnames, *, max_size: int = 64) -> str | None:
"""Return a small JPEG thumbnail (b64) for an image message."""
fnames = [k for k in fnames if k]
big_file, small_file = self._get_img_file(fnames)
return (
self._img_file_to_jpg_b64(small_file, max_size=max_size, quality=THUMB_JPEG_QUALITY)
or self._img_file_to_jpg_b64(big_file, max_size=max_size, quality=THUMB_JPEG_QUALITY)
)
def get_video_thumb(self, videoid: str, *, max_size: int = 64) -> str | None:
"""Return a small JPEG thumbnail (b64) for a video message, if available."""
if not videoid:
return None
video_thumbnail_file = os.path.join(self.video_dir, videoid + ".jpg")
if not os.path.exists(video_thumbnail_file):
return None
return self._img_file_to_jpg_b64(video_thumbnail_file, max_size=max_size, quality=THUMB_JPEG_QUALITY)
big_file = get_jpg_b64(big_file)
if big_file:
return big_file
return get_jpg_b64(small_file)
def get_emoji_by_md5(self, md5):
""" Returns: (b64 encoded img string, format) """
return self.emoji_reader.get_emoji(md5)
def get_video(self, videoid) -> str | None:
def get_video(self, videoid):
video_file = os.path.join(self.video_dir, videoid + ".mp4")
video_thumbnail_file = os.path.join(self.video_dir, videoid + ".jpg")
if os.path.exists(video_file):
return video_file
elif os.path.exists(video_thumbnail_file):
return video_thumbnail_file
return None
logger.warning(f"Cannot find video {videoid}")
return ""
+9 -19
View File
@@ -2,9 +2,10 @@
# -*- coding: UTF-8 -*-
import os
import re
import functools
import re
import json
import struct
from .common.textutil import get_file_b64
@@ -39,20 +40,10 @@ TEMPLATE = """.{name} {{
background-size: 24px 24px;
}}"""
def _css_class_name(name: str) -> str:
"""
Sanitizes a string to be a valid CSS class name by replacing invalid characters with underscores.
"""
# CSS class names must start with a letter or underscore, followed by letters, digits, hyphens, or underscores
name = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
# Ensure the first character is a valid start character (letter or underscore)
if not name[0].isalpha() and name[0] != '_':
name = '_' + name
return "smiley_" + name
def _css_class_name(s):
s = s.replace("/", "_")
s = s.replace(".", "_")
return "smiley_" + s
class SmileyProvider(object):
def __init__(self, html_replace=True):
@@ -85,6 +76,7 @@ class SmileyProvider(object):
if k in msg:
msg = msg.replace(k, self.gen_replace_elem(v))
return msg
return msg
def gen_used_smiley_css(self):
ret = HEAD
@@ -96,8 +88,6 @@ class SmileyProvider(object):
if __name__ == '__main__':
smiley = SmileyProvider()
msg = u"[天啊]哈哈呵呵hihi\U0001f684\u2728\u0001 /::<\ue415"
msg = u"[挥手]哈哈呵呵hihi\U0001f684\u2728\u0001 /::<\ue415"
msg = smiley.replace_smileycode(msg)
print(msg)
print()
print(smiley.gen_used_smiley_css())
smiley.gen_used_smiley_css()
-18
View File
@@ -1,18 +0,0 @@
<div class="chatItem {sender_label}">
<div class="chatItemContent">
<span class="avatar"></span>
<div class="cloud cloudText">
<div class="cloudPannel" title="{time}" {nickname}>
<div class="cloudBody">
<div class="cloudContent">
<pre style="white-space:pre-wrap">
QQ Music:<a href="{url}" target="_blank">{content}</a> </pre>
<span class="img_wrap">
<img class="imageBorder" src="data:image/{img[1]};base64,{img[0]}"/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
-15
View File
@@ -1,15 +0,0 @@
<div class="chatItem {sender_label}">
<div class="chatItemContent">
<span class="avatar"></span>
<div class="cloud cloudText">
<div class="cloudPannel" title="{time}" {nickname}>
<div class="cloudBody">
<div class="cloudContent">
<pre style="white-space:pre-wrap">
QQ Music:<a href="{url}" target="_blank">{content}</a> </pre>
</div>
</div>
</div>
</div>
</div>
</div>
-20
View File
@@ -1,20 +0,0 @@
<div class="chatItem {sender_label}">
<div class="chatItemContent">
<span class="avatar"></span>
<div class="cloud cloudText">
<div class="cloudPannel" title="{time}" {nickname}>
<div class="cloudBody">
<div class="cloudContent">
<pre style="white-space:pre-wrap">{content}</pre>
</div>
</div>
</div>
<div class="replyWrap">
<div class="replyQuote">
<span class="replyName">{reply_to}:</span>
{reply_quote_html}
</div>
</div>
</div>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

-1
View File
@@ -1 +0,0 @@
Copied from /data/data/com.tencent.mm/files/public/emoji/newemoji

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