157 Commits
Author SHA1 Message Date
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
199 changed files with 1062 additions and 4707 deletions
-6
View File
@@ -3,9 +3,3 @@
output/ output/
resource resource
*.db *.db
*.db.decoded
*2015*
*2017*
2020*
avatar.index
emoji.cache
+77 -56
View File
@@ -1,97 +1,118 @@
## 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. WeChat(微信), as the most popular mobile IM app in China, doesn't give users any method to export well-formatted history message.
Customers are not able to analyze their own chat messages or interact with other data analysis tools. This tool can parse and export WeChat messages on a rooted android phone.
We provide this tool that can parse WeChat messages from a rooted android phone. Right now it can dump messages in text-only mode, or generate a single-file html containing voice messages, images, emoji, etc.
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.
The tool is last verified to work with latest version of WeChat on 2025/01/01. __NEWS__: WeChat 6.0+ uses silk to encode audio. The code is updated.
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: __NEWS__: WeChat 6.3 uses a new avatar storage. The code is updated.
If this tools works for you, please take a moment to __add your phone/OS to__ [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
If it doesn't work, please leave an issue together with your phone/OS/wechat version.
### How to use:
#### Dependencies: #### Dependencies:
+ adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash. + python-PIL
+ Python >= 3.8 + [PyQuery](https://pypi.python.org/pypi/pyquery/1.2.1)
+ sox (command line tools) + [pysox](https://pypi.python.org/pypi/pysox/0.3.6.alpha)
+ Silk audio decoder (included; build it with `./third-party/compile_silk.sh`) + [pysqlcipher](https://pypi.python.org/pypi/pysqlcipher)
+ Other python dependencies: `pip install -r requirements.txt`. + numpy
+ csscompressor (suggested, optional)
+ adb and rooted android phone connected to a Linux/Mac OS.
+ Silk audio decoder (included; just run `./third-party/compile_silk.sh`)
+ gnu-sed
#### Get Necessary Data: #### Get Necessary Data:
1. Pull database file and (for older WeChat versions) avatar index: Note that commands involving `./android-interact.sh` are meant to be run on the computer.
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
+ Manual: + (Requires Linux or Mac) Get the decrypted WeChat database and the avatar index:
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device. + Automatic: `./android-interact.sh db-decrypt`
It should be a 32-character-long name consisting of hexadecimal digits. + Requires rooted adb. If the OS distribution does not come with adb support, you can download an app such as https://play.google.com/store/apps/details?id=eu.chainfire.adbd
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device. + Manual:
2. Decode `EnMicroMsg.db`. We do not provide instructions to do that. + 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.
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: + Get `/data/data/com.tencent.mm/MicroMsg/${userid}/{EnMicroMsg.db,sfs/avatar.index}` from the device, possible ways are:
+ `./android-interact.sh db`
+ Use your rooted file system manager app
+ Get WeChat uin (an integer), possible ways are:
+ `./android-interact.sh uin`, which pulls the value from `/data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml`
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
+ Get your phone IMEI number (a positive integer), possible ways are:
+ `./android-interact.sh imei`
+ Call `*#06#` on your phone
+ Find IMEI in system settings
+ Decrypt database, will produce `decrypted.db`:
```
./decrypt-db.py <path to EnMicroMsg.db> <imei> <uin>
```
NOTE: you may need to try different ways to getting imei & uin,
because things behave differently on different phones.
Also, if the decryption doesn't work with pysqlcipher, maybe try the version of sqlcipher in `legacy`.
+ Copy the WeChat user resource directory `/mnt/sdcard/tencent/MicroMsg/${userid}/{emoji,image2,sfs,video,voice2}` from the phone's SD card to the `resource` directory:
+ `./android-interact.sh res` + `./android-interact.sh res`
+ Change `RES_DIR` in the script if the location of these directories is different on your phone. + You might need to tweak `RES_DIR` in the script if the default doesn't work
For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/` + This can take a __long__ time. Some ways to do this faster:
+ This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive, + If there's enough free space on the SD card, you can combine all the files via `busybox tar` without compression in the `adb shell`, use `adb pull` to copy the `tar` archive to the computer, and then extract it. BusyBox is needed as the Android system's `tar` may choke on long paths.
`busybox tar` is recommended as the Android system's `tar` may choke on long paths. + Alternatively, you can use pipes. This is slower, but doesn't require any free space on the SD card:
+ 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. ```sh
See [WXGFDecoder](WXGFDecoder) for instructions. # copy MicroMsg to the current directory
adb shell 'cd /mnt/sdcard/tencent &&
4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2) busybox tar czf - MicroMsg 2>/dev/null | busybox base64' |
and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering. base64 -di | tar xzf -
```
wget -c https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2
tar xf emoji.cache.tar.bz2
#### Run: #### Run:
+ Parse and dump text messages of __every__ chat (requires decoded database): + Parse and dump text messages of __every__ chat (requires `decrypted.db`):
``` ```
./dump-msg.py decoded.db output_dir ./dump-msg.py decrypted.db output_dir
``` ```
+ List all chats (required decoded database): + List all chats (requires `decrypted.db`):
``` ```
./list-chats.py decoded.db ./list-chats.py decrypted.db
``` ```
+ Generate statistics report on text messages (requires `output_dir` from `./dump-msg.py`): + Generate statistical report on text messages (requires `output_dir` from `./dump-msg.py`):
``` ```
./count-message.sh output_dir ./count-message.sh output_dir
``` ```
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires decoded database and `resource`): + Dump messages of one contact to html, containing voice messages, emojis, and images (requires `decrypted.db`, `avatar.index`, and `resource`):
``` ```
./dump-html.py "<contact_display_name>" ./dump-html.py decrypted.db avatar.index resource "<contact_name>" output.html
``` ```
* The output file is `output.html`. Check `./dump-html.py -h` to use different input/output paths.
* Add `--wxgf-server ws://xx.xx.xx.xx:xxxx` to use a WXGF decoder server.
### Examples: ### Examples:
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
Screenshots of generated html: Screenshots of generated html:
![byvoid](https://github.com/ppwwyyxx/wechat-dump/raw/master/screenshots/byvoid.jpg) ![byvoid](https://github.com/ppwwyyxx/wechat-dump/raw/master/screenshots/byvoid.jpg)
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html. ### TODO List
+ Search by uid/username
### TODO List (help needed!) + Faster way to copy a directory from android (I don't know..).
* After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 - + Fix rare unhandled types: > 10000 and < 0
they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis. + Better user experiences... see `grep 'TODO' wechat -R`
* Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu). + more easy-to-use for non-programmers (GUI?)
* Fix rare unhandled message types: > 10000 and < 0
* Better user experiences... see `grep 'TODO' wechat -R`
### Donate! ### Donate!
Paypal:
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted"> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted">
<img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" /> <img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" />
</a> </a>
-11
View File
@@ -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")
+80 -60
View File
@@ -1,21 +1,48 @@
#!/bin/bash #!/bin/bash
# File: android-interact.sh # File: android-interact.sh
# Date: Fri Jun 26 10:38:07 2015 +0800
# Author: Yuxin Wu <[email protected]>
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"` PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
PROG_DIR=`dirname "$PROG_NAME"` PROG_DIR=`dirname "$PROG_NAME"`
cd "$PROG_DIR" cd "$PROG_DIR"
# Please check that your path is the same, since this might be different among devices source compatibility.sh
# RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
RES_DIR="/data/data/com.tencent.mm/MicroMsg"
if [[ $1 == "db" || $1 == "res" ]]; then # Please check that your path is the same, since this might be different among devices
RES_DIR="/mnt/sdcard/tencent/MicroMsg"
MM_DIR="/data/data/com.tencent.mm"
echo "Starting rooted adb server..."
adb root
if [[ $1 == "uin" ]]; then
adb pull $MM_DIR/shared_prefs/system_config_prefs.xml 2>/dev/null
uin=$($GREP 'default_uin' system_config_prefs.xml | $GREP -o 'value="\-?[0-9]*' | cut -c 8-)
[[ -n $uin ]] || {
>&2 echo "Failed to get wechat uin. You can try other methods, or report a bug."
exit 1
}
rm system_config_prefs.xml
echo "Got wechat uin: $uin"
elif [[ $1 == "imei" ]]; then
imei=$(adb shell dumpsys iphonesubinfo | $GREP 'Device ID' | $GREP -o '[0-9]+')
[[ -n $imei ]] || {
imei=$(adb shell service call iphonesubinfo 1 | awk -F "'" '{print $2}' | sed 's/[^0-9A-F]*//g' | tr -d '\n')
}
[[ -n $imei ]] || {
>&2 echo "Failed to get imei. You can try other methods, or report a bug."
exit 1
}
echo "Got imei: $imei"
elif [[ $1 == "db" || $1 == "res" ]]; then
echo "Looking for user dir name..." echo "Looking for user dir name..."
# look for dirname which looks like md5 (32 alpha-numeric chars) sleep 1 # sometimes adb complains: device not found
userList=$(adb shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}') userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' \
numUser=$(echo "$userList" | wc -l) | awk '{if (length() == 32) print}')
numUser=$(echo $userList | wc -l)
# choose the first user. # choose the first user.
chooseUser=$(echo "$userList" | head -n1) chooseUser=$(echo $userList | head -n1)
[[ -n $chooseUser ]] || { [[ -n $chooseUser ]] || {
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR" >&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
exit 1 exit 1
@@ -23,61 +50,54 @@ if [[ $1 == "db" || $1 == "res" ]]; then
echo "Found $numUser user(s). User chosen: $chooseUser" echo "Found $numUser user(s). User chosen: $chooseUser"
if [[ $1 == "res" ]]; then if [[ $1 == "res" ]]; then
mkdir -p resource echo "Pulling resources... this might take a long time, because adb sucks..."
( mkdir -p resource; cd resource
cd resource || exit for d in image2 voice2 emoji video sfs; do
echo "Pulling resources... " mkdir -p $d; cd $d
for d in avatar image2 voice2 emoji video sfs; do adb pull $RES_DIR/$chooseUser/$d
echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..." cd ..
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | [[ -d $d ]] || {
busybox base64" | base64 -di | tar xzf - --strip-components 5 >&2 echo "Failed to download resource directory: $RES_DIR/$chooseUser/$d"
[[ -d $d ]] && continue exit 1
}
echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..." done
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" | cd ..
base64 -di | tar xzf - --strip-components 5 echo "Resource pulled at ./resource"
[[ -d $d ]] && continue echo "Total size: $(du -sh resource | cut -f1)"
echo "Trying to download $RES_DIR/$chooseUser/$d with adb pull (slow) ..."
mkdir -p $d
(
cd $d || exit
adb root
sleep 1 # sometimes adb complains: device not found
adb pull "$RES_DIR/$chooseUser/$d"
)
[[ -d $d ]] || {
echo "Failed to download $RES_DIR/$chooseUser/$d"
}
done
echo "Resource pulled at ./resource"
echo "Total size: $(du -sh | cut -f1)"
)
else else
for f in EnMicroMsg.db sfs/avatar.index; do echo "Pulling database and avatar index file..."
echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..." adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | [[ -f EnMicroMsg.db ]] && \
busybox base64" | base64 -di | tar xzf - --strip-components 5 echo "Database successfully downloaded to EnMicroMsg.db" || {
[[ -f $f ]] && continue >&2 echo "Failed to pull database by adb"
exit 1
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" | adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index
base64 -di | tar xzf - --strip-components 5 [[ -f avatar.index ]] && \
[[ -f $f ]] && continue echo "Avatar index successfully downloaded to avatar.index" || {
>&2 echo "Failed to pull avatar index by adb, are you using latest version of wechat?"
echo "Trying to download $RES_DIR/$chooseUser/$f with adb pull..." exit 1
adb root
sleep 1
adb pull $RES_DIR/$chooseUser/$f
[[ -f $f ]] || {
echo "Failed to download $RES_DIR/$chooseUser/$f"
} }
done
echo "Database and avatar index file successfully downloaded"
fi fi
elif [[ $1 == "db-decrypt" ]]; then
echo "Getting uin..."
$0 uin | tail -n1 | $GREP -o '\-?[0-9]*' | tee /tmp/uin
echo "Getting imei..."
$0 imei | tail -n1 | $GREP -o '[0-9]*' | tee /tmp/imei
echo "Getting db..."
$0 db
echo "Decrypting db..."
imei=$(cat /tmp/imei)
uin=$(cat /tmp/uin)
if [[ -z $imei || -z $uin ]]; then
>&2 echo "Failed to get imei or uin. See README for manual methods."
exit 1
fi
./decrypt-db.py EnMicroMsg.db $imei $uin
rm /tmp/{uin,imei}
echo "Done. See decrypted.db"
else else
echo "Usage: $0 <res|db>" echo "Usage: $0 <res|db-decrypt>"
exit 1 exit 1
fi fi
@@ -1,4 +1,8 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: progress.py
# Date: Wed Jun 17 23:59:52 2015 +0800
# Author: Yuxin Wu <[email protected]>
import time import time
import sys import sys
@@ -32,12 +36,12 @@ class ProgressReporter(object):
self._prev_report_time = now self._prev_report_time = now
dt = now - self._start_time dt = now - self._start_time
if self._total and self._cnt > 0: if self._total and self._cnt > 0:
eta_msg = '{}/{} ETA: {:.1f}'.format(self._cnt, self._total, eta_msg = '{}/{} ETA: {:.2f}'.format(self._cnt, self._total,
(self._total-self._cnt)*dt/self._cnt) (self._total-self._cnt)*dt/self._cnt)
else: else:
eta_msg = '{} done'.format(self._cnt) eta_msg = '{} done'.format(self._cnt)
self._fout.write(u'{}: avg {:.1f}/sec' self._fout.write(u'{}: avg {:.3f}/sec'
u', passed {:.1f}sec, {} {} \r'.format( u', passed {:.3f}sec, {} {} \r'.format(
self._name, self._cnt / dt, dt, eta_msg, extra_msg)) self._name, self._cnt / dt, dt, eta_msg, extra_msg))
self._fout.flush() self._fout.flush()
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
# File: utils.py
# Date: Wed Jun 17 23:59:25 2015 +0800
# Author: Yuxin Wu <[email protected]>
import hashlib
import base64
def ensure_bin_str(s):
if type(s) == str:
return s
if type(s) == unicode:
return s.encode('utf-8')
def ensure_unicode(s):
if type(s) == str:
return s.decode('utf-8')
if type(s) == unicode:
return s
def md5(s):
m = hashlib.md5()
m.update(s)
return m.hexdigest()
def get_file_b64(fname):
data = open(fname, 'rb').read()
return base64.b64encode(data)
def safe_filename(fname):
filename = ensure_unicode(fname)
return "".join(
[c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()
+5 -1
View File
@@ -1,4 +1,8 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: timer.py
# Date: Wed Jun 17 23:25:54 2015 +0800
# Author: Yuxin Wu <[email protected]>
import time, functools import time, functools
from collections import defaultdict from collections import defaultdict
@@ -16,7 +20,7 @@ class TotalTimer(object):
self.times = defaultdict(float) self.times = defaultdict(float)
def __del__(self): def __del__(self):
for k, v in self.times.items(): for k, v in self.times.iteritems():
logger.info("{} took {} seconds in total.".format(k, v)) logger.info("{} took {} seconds in total.".format(k, v))
_total_timer = TotalTimer() _total_timer = TotalTimer()
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash -e
# $File: compatibility.sh
# $Date: Tue Jun 16 22:23:36 2015 +0800
# Author: Vury Leo <i[at]vuryleo[dot]com>
if [ `uname` = 'Darwin' ]; then
GREP='grep -E'
MD5SUM='md5'
else
GREP='grep -E'
MD5SUM='md5sum'
fi
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash -e #!/bin/bash -e
# File: count-message.sh # File: count-message.sh
# Date: Wed Nov 29 02:32:40 2017 -0800 # Date: Sun Apr 12 21:01:01 2015 +0900
# Author: Kangjing Huang <[email protected]> # Author: Kangjing Huang <[email protected]>
@@ -11,7 +11,7 @@ then
fi fi
# TODO work on db directly # TODO work on db directly
echo -e "Filename\t#Lines\t#Chars\t#Words" echo -e "Filename\tCounts of message\tCounts of chars\tCounts of words"
SAVEIFS=$IFS SAVEIFS=$IFS
IFS=$(echo -en "\n\b") IFS=$(echo -en "\n\b")
@@ -23,6 +23,6 @@ do
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m) CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w) WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT" echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
done | sort -t $'\t' -k 2 -n | column -t -s $'\t' done
IFS=$SAVEIFS IFS=$SAVEIFS
Executable
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python2
# -*- coding: UTF-8 -*-
# File: decrypt-db.py
# Author: Yuxin Wu <[email protected]>
from argparse import ArgumentParser
from pysqlcipher import dbapi2 as sqlite
from hashlib import md5
import sys
import os
DEFAULT_OUTPUT_DB_NAME = 'decrypted.db'
def get_args():
parser = ArgumentParser()
parser.add_argument('db', help='path to EnMicroMsg.db')
parser.add_argument('imei', help='15 digit IMEI of your phone')
parser.add_argument('uin', help='WeChat UIN')
parser.add_argument('--output', help='output decrypted database',
default=DEFAULT_OUTPUT_DB_NAME)
args = parser.parse_args()
return args
def get_key(imei, uin):
a = md5(imei + uin)
return a.hexdigest()[:7]
if __name__ == '__main__':
args = get_args()
output = args.output
if os.path.abspath(os.path.dirname(output)) != os.path.abspath('.'):
print "Output file must be in current directory"
sys.exit(1)
if os.path.isfile(output):
print "{} already exists. Remove? (y/n)".format(args.output),
ans = raw_input()
if ans not in ['y', 'Y']:
print "Bye!"
sys.exit()
os.unlink(output)
key = get_key(args.imei, args.uin)
print "KEY: {}".format(key)
print "Decrypt and dump database to {} ... ".format(output)
conn = sqlite.connect(args.db)
c = conn.cursor()
c.execute("PRAGMA key = '" + key + "';")
c.execute("PRAGMA cipher_use_hmac = OFF;")
c.execute("PRAGMA cipher_page_size = 1024;")
c.execute("PRAGMA kdf_iter = 4000;")
c.execute("ATTACH DATABASE '" + args.output + "' AS db KEY '';")
c.execute("SELECT sqlcipher_export('db');" )
c.execute("DETACH DATABASE db;" )
c.close()
+26 -21
View File
@@ -1,44 +1,49 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# File: dump-audio.py
# Author: Yuxin Wu <[email protected]>
import sys import sys
import base64
import argparse import argparse
from common.textutil import ensure_unicode
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from wechat.msg import TYPE_SPEAK
from wechat.res import Resource from wechat.res import Resource
from wechat.render import HTMLRender
from wechat.libchathelper import LibChatHelper
def get_args(): def get_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact') parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output mp3 dir', default='/tmp') parser.add_argument('--output', help='output mp3 dir', default='/tmp')
parser.add_argument('--db', default='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') parser.add_argument('--res', default='resource', help='reseource directory')
args = parser.parse_args() args = parser.parse_args()
return args return args
if __name__ == '__main__': if __name__ == '__main__':
args = get_args() args = get_args()
name = ensure_unicode(args.name)
output_file = args.output
parser = WeChatDBParser(args.db) parser = WeChatDBParser(args.db)
res = Resource(parser, args.res) res = Resource(args.res, '')
try: if name and name in parser.msgs_by_chat:
chatid = parser.get_chat_id(args.name) msgs = parser.msgs_by_chat[name]
except KeyError: else:
sys.stderr.write(u"Valid Contacts: {}\n".format('\n'.join(parser.all_chat_nicknames))) sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name)); sys.stderr.write(u"Couldn't find that contact {}.".format(name));
sys.exit(1) sys.exit(1)
print "Number of Messages: ", len(msgs)
msgs = parser.msgs_by_chat[chatid]
print(f"Number of Messages for {args.name}: ", len(msgs))
assert len(msgs) > 0 assert len(msgs) > 0
voice_msgs = [m for m in msgs if m.type == TYPE_SPEAK] libchat = LibChatHelper(parser, res)
for idx, m in enumerate(voice_msgs): msgs = libchat.convert_msgs(msgs)
audio_str, duration = res.get_voice_mp3(m.imgPath) voices = [m.sound for m in msgs if m.sound]
audio_bytes = base64.b64decode(audio_str) for idx, v in enumerate(voices):
outf = f'/{args.output}/{idx:04d}-{duration:.1f}s.mp3' p = v.find(':')
with open(outf, 'wb') as f: v = v[p:]
f.write(audio_bytes) with open('/{}/{:04d}.mp3'.format(args.output, idx), 'wb') as f:
print(f"Audio written to {outf}") f.write(v)
+22 -36
View File
@@ -1,66 +1,52 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
import os # -*- coding: UTF-8 -*-
# File: dump-html.py
# Date: Wed Mar 25 17:44:20 2015 +0800
# Author: Yuxin Wu <[email protected]>
import sys import sys
import argparse import argparse
import logging
from datetime import datetime
from common.textutil import ensure_unicode
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from wechat.res import Resource from wechat.res import Resource
from wechat.render import HTMLRender from wechat.render import HTMLRender
logger = logging.getLogger("wechat")
def get_args(): def get_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('name', help='name of contact') parser.add_argument('name', help='name of contact')
parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html') parser.add_argument('--output', help='output html file', default='output.html')
parser.add_argument('--db', default='EnMicroMsg.db.decoded', parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
help='path to the decoded database, e.g. EnMicroMsg.db.decoded') parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file')
parser.add_argument('--res', default='resource', help='the resource directory') parser.add_argument('--res', default='resource', help='reseource directory')
parser.add_argument('--wxgf-server', help='address of the wxgf image decoder server')
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat. Ignore for new version of wechat.')
parser.add_argument('--start', help='start time in format of YYYY-MM-DD HH:MM:SS',
type=datetime.fromisoformat)
args = parser.parse_args() args = parser.parse_args()
return args return args
if __name__ == '__main__': if __name__ == '__main__':
args = get_args() args = get_args()
name = ensure_unicode(args.name)
output_file = args.output output_file = args.output
parser = WeChatDBParser(args.db) parser = WeChatDBParser(args.db)
res = Resource(parser, args.res, args.avt)
try: if name and name in parser.msgs_by_chat:
chatid = parser.get_chat_id(args.name) msgs = parser.msgs_by_chat[name]
except KeyError: else:
sys.stderr.write(u"Valid Contacts: {}\n".format( sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
u'\n'.join(parser.all_chat_nicknames))) sys.stderr.write(u"Couldn't find that contact {}.".format(name));
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
sys.exit(1) sys.exit(1)
print "Number of Messages: ", len(msgs)
res = Resource(parser, args.res,
wxgf_server=args.wxgf_server,
avt_db=args.avt)
msgs = parser.msgs_by_chat[chatid]
logger.info(f"Number of Messages for chatid {chatid}: {len(msgs)}")
assert len(msgs) > 0 assert len(msgs) > 0
if args.start is not None:
msgs = [msg for msg in msgs if msg.createTime > args.start]
logger.info(f"Number of Messages after {args.start}: {len(msgs)}")
render = HTMLRender(parser, res) render = HTMLRender(parser, res)
htmls = render.render_msgs(msgs) htmls = render.render_msgs(msgs)
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
if len(htmls) == 1: if len(htmls) == 1:
with open(output_file, 'w') as f: with open(output_file, 'w') as f:
f.write(htmls[0]) print >> f, htmls[0].encode('utf-8')
else: else:
assert output_file.endswith(".html")
basename = output_file[:-5]
for idx, html in enumerate(htmls): for idx, html in enumerate(htmls):
with open(basename + f'{idx:02d}.html', 'w') as f: with open(output_file + '.{}'.format(idx), 'w') as f:
f.write(html) print >> f, html.encode('utf-8')
res.emoji_reader.flush_cache()
+22 -32
View File
@@ -1,40 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: dump-msg.py
# Date: Mon May 25 15:23:05 2015 +0800
# Author: Yuxin Wu <[email protected]>
import logging
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from wechat.common.textutil import safe_filename from common.textutil import safe_filename
import sys, os import sys, os
logger = logging.getLogger("wechat") if len(sys.argv) != 3:
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
if __name__ == '__main__': db_file = sys.argv[1]
if len(sys.argv) != 3: output_dir = sys.argv[2]
sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0])) try:
os.mkdir(output_dir)
except:
pass
if not os.path.isdir(output_dir):
sys.exit("Error creating directory {}".format(output_dir))
db_file = sys.argv[1] parser = WeChatDBParser(db_file)
output_dir = sys.argv[2]
try:
os.mkdir(output_dir)
except:
pass
if not os.path.isdir(output_dir):
sys.exit("Error creating directory {}".format(output_dir))
parser = WeChatDBParser(db_file) for name, msgs in parser.msgs_by_chat.iteritems():
print u"Writing msgs for {}".format(name)
for chatid, msgs in parser.msgs_by_chat.items(): safe_name = safe_filename(name)
name = parser.contacts[chatid] with open(os.path.join(output_dir, safe_name + '.txt'), 'w') as f:
if len(name) == 0: for m in msgs:
logger.info(f"Chat {chatid} doesn't have a valid display name.") print >> f, m
name = str(id(chatid))
logger.info(f"Writing msgs for {name}")
safe_name = safe_filename(name)
outf = os.path.join(output_dir, safe_name + '.txt')
if os.path.isfile(outf):
logger.info(f"File {outf} exists! Skip contact {name}")
continue
with open(outf, 'w') as f:
for m in msgs:
f.write(str(m))
f.write("\n")
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pickle
import sys
import os
import imghdr
import base64
if __name__ == '__main__':
if len(sys.argv) != 3:
print("""\
Usage:
{} unpack output-dir
{} pack input-dir
""".format(sys.argv[0], sys.argv[0]))
sys.exit(1)
if sys.argv[1] == 'unpack':
with open('emoji.cache', 'rb') as f:
dic = pickle.load(f)
outdir = sys.argv[2]
assert os.path.isdir(outdir)
for md5, img in dic.items():
data = img[0]
if not isinstance(data, bytes):
data = data.encode('ascii')
name = os.path.join(outdir, md5 + '.' + img[1].lower())
print(name)
with open(name, 'wb') as f:
f.write(base64.decodebytes(data))
elif sys.argv[1] == 'pack':
ret = {}
indir = sys.argv[2]
files = os.listdir(indir)
for fname in files:
try:
md5, format = fname.split('.')
except:
print("Unable to parse", fname)
continue
with open(os.path.join(indir, fname), 'rb') as f:
b64 = base64.encodebytes(f.read()).decode('ascii')
ret[md5] = (b64, format)
with open('emoji.cache', 'wb') as f:
pickle.dump(ret, f)
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash -e
# $File: compatibility.sh
# $Date: Tue Jun 16 22:23:36 2015 +0800
# Author: Vury Leo <i[at]vuryleo[dot]com>
if [ `uname` = 'Darwin' ]; then
GREP='grep -E'
MD5SUM='md5'
else
GREP='grep -E'
MD5SUM='md5sum'
fi
+1
View File
@@ -0,0 +1 @@
../compatibility.sh
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash -e
# File: decrypt-db.sh
# Date: Tue Jun 16 22:23:13 2015 +0800
# Author: Yuxin Wu <[email protected]>
source compatibility.sh
MSGDB=$1
imei=$2
uin=$3
output=decrypted.db
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
echo "Usage: $0 <path to EnMicroMsg.db> <imei> <uin>"
exit 1
fi
if [[ -f $output ]]; then
echo -n "$output already exists. removed? (y/n)"
read r
[[ $r == "y" ]] && rm -v $output || exit 1
fi
KEY=$(echo -n "$imei$uin" | $MD5SUM | cut -b 1-7)
echo "KEY: $KEY"
uname | grep Darwin > /dev/null && os=darwin || os=linux
uname -m | grep x86_64 > /dev/null && version=64bit || version=32bit
echo "Use $version sqlcipher of $os."
echo "Dump decrypted database... (Don't worry about libcrypt.so version warning.)"
SQLCIPHER=./sqlcipher/$os/$version
export LD_LIBRARY_PATH=$SQLCIPHER
"$SQLCIPHER"/sqlcipher "$MSGDB" << EOF
PRAGMA key='$KEY';
PRAGMA cipher_use_hmac = off;
PRAGMA cipher_page_size = 1024;
PRAGMA kdf_iter = 4000;
ATTACH DATABASE "$output" AS db KEY "";
SELECT sqlcipher_export("db");
DETACH DATABASE db;
EOF
echo "Database successfully dumped to $output"
+24
View File
@@ -0,0 +1,24 @@
Copyright (c) 2008, ZETETIC LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the ZETETIC LLC nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+7
View File
@@ -0,0 +1,7 @@
## NOTE
For convenience, I include the binary version of sqlcipher 2.1.1.
User can also choose to download and install sqlcipher 2.1.1 from their trusted source, such as:
https://launchpad.net/ubuntu/+source/sqlcipher/2.1.1-2
Binary file not shown.
+130
View File
@@ -0,0 +1,130 @@
#! /bin/sh
# sqlite3 - temporary wrapper script for .libs/sqlite3
# Generated by ltmain.sh (GNU libtool) 2.2.6
#
# The sqlite3 program cannot be directly executed until all the libtool
# libraries that it depends on are installed.
#
# This wrapper script should never be moved out of the build directory.
# If it is, it will not operate correctly.
# Sed substitution that helps us do robust quoting. It backslashifies
# metacharacters that are still active within double-quoted strings.
Xsed='/opt/local/bin/gsed -e 1s/^X//'
sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
# Be Bourne compatible
if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
emulate sh
NULLCMD=:
# Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
# is contrary to our usage. Disable this feature.
alias -g '${1+"$@"}'='"$@"'
setopt NO_GLOB_SUBST
else
case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
fi
BIN_SH=xpg4; export BIN_SH # for Tru64
DUALCASE=1; export DUALCASE # for MKS sh
# The HP-UX ksh and POSIX shell print the target directory to stdout
# if CDPATH is set.
(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
relink_command=""
# This environment variable determines our operation mode.
if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
# install mode needs the following variables:
generated_by_libtool_version='2.2.6'
notinst_deplibs=' libsqlite3.la'
else
# When we are sourced in execute mode, $file and $ECHO are already set.
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
ECHO="/bin/echo"
file="$0"
# Make sure echo works.
if test "X$1" = X--no-reexec; then
# Discard the --no-reexec flag, and continue.
shift
elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
# Yippee, $ECHO works!
:
else
# Restart under the correct shell, and then maybe $ECHO will work.
exec /bin/sh "$0" --no-reexec ${1+"$@"}
fi
fi
# Find the directory that this script lives in.
thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
test "x$thisdir" = "x$file" && thisdir=.
# Follow symbolic links until we get to the real thisdir.
file=`ls -ld "$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
while test -n "$file"; do
destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
# If there was a directory component, then change thisdir.
if test "x$destdir" != "x$file"; then
case "$destdir" in
[\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
*) thisdir="$thisdir/$destdir" ;;
esac
fi
file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
file=`ls -ld "$thisdir/$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
done
# Usually 'no', except on cygwin/mingw when embedded into
# the cwrapper.
WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
# special case for '.'
if test "$thisdir" = "."; then
thisdir=`pwd`
fi
# remove .libs from thisdir
case "$thisdir" in
*[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
.libs ) thisdir=. ;;
esac
fi
# Try to get the absolute directory name.
absdir=`cd "$thisdir" && pwd`
test -n "$absdir" && thisdir="$absdir"
program='sqlite3'
progdir="$thisdir/.libs"
if test -f "$progdir/$program"; then
# Add our own library path to DYLD_LIBRARY_PATH
DYLD_LIBRARY_PATH="/Users/vuryleo/Projects/sqlcipher/.libs:$DYLD_LIBRARY_PATH"
# Some systems cannot cope with colon-terminated DYLD_LIBRARY_PATH
# The second colon is a workaround for a bug in BeOS R4 sed
DYLD_LIBRARY_PATH=`$ECHO "X$DYLD_LIBRARY_PATH" | $Xsed -e 's/::*$//'`
export DYLD_LIBRARY_PATH
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
# Run the actual program with our arguments.
exec "$progdir/$program" ${1+"$@"}
$ECHO "$0: cannot exec $program $*" 1>&2
exit 1
fi
else
# The program doesn't exist.
$ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
$ECHO "This script is just a wrapper for $program." 1>&2
/bin/echo "See the libtool documentation for more information." 1>&2
exit 1
fi
fi
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: create_table.py # File: create_table.py
# Date: Wed Mar 25 16:43:22 2015 +0800 # Date: Wed Mar 25 16:43:22 2015 +0800
# Author: Yuxin Wu # Author: Yuxin Wu <[email protected]>
import sys import sys
import os import os
@@ -10,7 +10,7 @@ import os
from libchat import SqliteLibChat from libchat import SqliteLibChat
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: {} <DB file name>") print "Usage: {} <DB file name>"
sys.exit() sys.exit()
db_name = sys.argv[1] db_name = sys.argv[1]
@@ -1,5 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: libchat.py
# Date: Sun Apr 12 21:08:51 2015 +0900
# Author: Yuxin Wu <[email protected]>
import sqlite3 import sqlite3
import os import os
from datetime import datetime from datetime import datetime
@@ -96,7 +99,7 @@ class SqliteLibChat(object):
else: else:
self.c.execute("SELECT * FROM message WHERE {}".format( self.c.execute("SELECT * FROM message WHERE {}".format(
' AND '.join(["{} = {}".format(k, v) ' AND '.join(["{} = {}".format(k, v)
for k, v in predicate.items()]))) for k, v in predicate.iteritems()])))
for row in self.c.fetchall(): for row in self.c.fetchall():
yield ChatMsg(*SqliteLibChat.postfilter(row)) yield ChatMsg(*SqliteLibChat.postfilter(row))
@@ -110,5 +113,5 @@ if __name__ == '__main__':
for k in db.iterate_all_msg(): for k in db.iterate_all_msg():
from IPython import embed; embed() from IPython import embed; embed()
print(k) print k
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: list-chats.py # File: list-chats.py
# Author: Yuxin Wu <[email protected]> # Author: Yuxin Wu <[email protected]>
@@ -6,7 +6,7 @@
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
import sys import sys
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: {} db_file".format(sys.argv[0])) print "Usage: {} db_file".format(sys.argv[0])
sys.exit(1) sys.exit(1)
db_file = sys.argv[1] db_file = sys.argv[1]
@@ -14,4 +14,4 @@ db_file = sys.argv[1]
parser = WeChatDBParser(db_file) parser = WeChatDBParser(db_file)
chats = parser.msgs_by_chat.keys() chats = parser.msgs_by_chat.keys()
for k in chats: for k in chats:
print(parser.contacts[k], '\t', k) print k.encode('utf-8'), '\t', parser.contacts_rev[k].encode('utf-8')
+31 -28
View File
@@ -1,46 +1,49 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: plot-num-msg-by-time.py
# Date: Wed Mar 25 17:44:39 2015 +0800
# Author: Yuxin Wu <[email protected]>
from wechat.parser import WeChatDBParser from wechat.parser import WeChatDBParser
from common.textutil import ensure_unicode
from datetime import timedelta, datetime from datetime import timedelta, datetime
import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import sys 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__': db_file = sys.argv[1]
if len(sys.argv) != 3: name = ensure_unicode(sys.argv[2])
sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0])) every_k_days = 2
db_file = sys.argv[1] parser = WeChatDBParser(db_file)
name = sys.argv[2] msgs = parser.msgs_by_chat[name]
every_k_days = 2 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) width = 20
msgs = parser.msgs_by_chat[name] numbers = range((max_day / width + 1) * width + 1)[::width]
times = [x.createTime for x in msgs] labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
start_time = times[0] plt.xticks(numbers, labels)
diffs = [(x - start_time).days for x in times] plt.xlabel("Date")
max_day = diffs[-1] plt.ylabel("Number of msgs in k days")
plt.hist(diffs, bins=max_day / every_k_days)
width = 20 plt.show()
numbers = range((max_day / width + 1) * width + 1)[::width]
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
plt.xticks(numbers, labels)
plt.xlabel("Date")
plt.ylabel("Number of msgs in k days")
plt.hist(diffs, bins=max_day / every_k_days)
plt.show()
# statistics by hour # statistics by hour
# I'm in a different time zone in this period: # I'm in a different time zone in this period:
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15} #TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
#def real_hour(x): #def real_hour(x):
#for k, v in TZ_DELTA.items(): #for k, v in TZ_DELTA.iteritems():
#if x > k[0] and x < k[1]: #if x > k[0] and x < k[1]:
#print x #print x
#return (x.hour + v + 24) % 24 #return (x.hour + v + 24) % 24
#return x.hour #return x.hour
#hours = [real_hour(x) for x in times] #hours = [real_hour(x) for x in times]
#plt.ylabel("Number of msgs") #plt.ylabel("Number of msgs")
#plt.xlabel("Hour in a day") #plt.xlabel("Hour in a day")
-10
View File
@@ -1,10 +0,0 @@
pyquery
javaobj-py3>=0.4.1
pycryptodome
pillow>=7.0
requests
pysqlcipher3>=1.0.3
csscompressor
numpy
ipython
websocket-client
+3
View File
@@ -1,4 +1,7 @@
#!/bin/bash -e #!/bin/bash -e
# File: compile_silk.sh
# Date: Tue Jun 16 22:26:49 2015 +0800
# Author: Yuxin Wu <[email protected]>
cd `dirname "$0"`/silk cd `dirname "$0"`/silk
make make
+1 -1
View File
@@ -47,7 +47,7 @@ ifeq (yes,$(USE_NEON))
endif endif
CFLAGS += -Wall -O3 CFLAGS += -Wall -enable-threads -O3
CFLAGS += $(call cppflags-from-defines,$(CDEFINES)) CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES)) CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import logging import logging
@@ -21,10 +21,10 @@ def set_level_color(lvl, color):
logger.addHandler(handler) logger.addHandler(handler)
set_level_color(logging.INFO, '\033[1;32m') set_level_color(logging.INFO, '\033[1;32m')
set_level_color(logging.WARN, '\033[1;33m') set_level_color(logging.WARN, '\033[1;31m')
set_level_color(logging.ERROR, '\033[1;31m') set_level_color(logging.ERROR, '\033[1;31m')
if __name__ == '__main__': if __name__ == '__main__':
logger.info("info") logger.info("info")
logger.warning("warn") logger.warn("warn")
+51 -44
View File
@@ -1,71 +1,78 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: audio.py
# Date: Fri Jun 26 10:42:41 2015 +0800
# Author: Yuxin Wu <[email protected]>
import os import os
import tempfile from subprocess import PIPE, Popen, call
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .common.textutil import get_file_b64 import pysox
from .common.procutil import subproc_succ
from common.textutil import get_file_b64
SILK_DECODER = os.path.join(os.path.dirname(__file__), SILK_DECODER = os.path.join(os.path.dirname(__file__),
'../third-party/silk/decoder') '../third-party/silk/decoder')
if not os.path.exists(SILK_DECODER):
logger.error("Silk decoder is not compiled. Please see README.md.")
raise RuntimeError()
def parse_wechat_audio_file(file_name): def parse_wechat_audio_file(file_name):
try: try:
return do_parse_wechat_audio_file(file_name) return do_parse_wechat_audio_file(file_name)
except Exception as e: except Exception as e:
logger.error(f"Error when parsing audio file {file_name}: {str(e)}") logger.error("Pase audio file {} error!".format(file_name))
logger.error(e)
return "", 0 return "", 0
def do_parse_wechat_audio_file(file_name): def do_parse_wechat_audio_file(file_name):
""" return a mp3 stored in base64 unicode string, and the duration""" """ return a mp3 base64 string, and the duration"""
if not file_name: return "", 0 if not file_name: return "", 0
with tempfile.TemporaryDirectory(prefix="wechatdump_audio") as temp: mp3_file = os.path.join('/tmp',
mp3_file = os.path.join(temp, os.path.basename(file_name)[:-4] + '.mp3')
os.path.basename(file_name)[:-4] + '.mp3') with open(file_name) as f:
with open(file_name, 'rb') as f: header = f.read(10)
header = f.read(10) if 'AMR' in header:
if b'AMR' in header: # maybe this is faster than calling sox from command line?
cmd = f"sox -e signed -c 1 {file_name} {mp3_file}" infile = pysox.CSoxStream(file_name)
subproc_succ(cmd) outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
cmd = f"soxi -D {mp3_file}" chain = pysox.CEffectsChain(infile, outfile)
duration = float(subproc_succ(cmd)) chain.flow_effects()
outfile.close()
# The below is python2 only. It should be equivalent to using sox from command line signal = infile.get_signal().get_signalinfo()
# import pysox duration = signal['length'] * 1.0 / signal['rate']
# infile = pysox.CSoxStream(file_name) elif 'SILK' in header:
# outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal()) raw_file = os.path.join('/tmp',
# chain = pysox.CEffectsChain(infile, outfile) os.path.basename(file_name)[:-4] + '.raw')
# chain.flow_effects() proc = Popen('{0} {1} {2}'.format(SILK_DECODER,
# outfile.close() file_name, raw_file),
# signal = infile.get_signal().get_signalinfo() shell=True, stdout=PIPE, stderr=PIPE)
# duration = signal['length'] * 1.0 / signal['rate'] stdout = proc.communicate()[0]
elif b'SILK' in header: for line in stdout.split('\n'):
if not os.path.exists(SILK_DECODER): if 'File length' in line:
raise RuntimeError("Silk decoder is not compiled. Please see README.md.") duration = float(line[13:-3].strip())
break
raw_file = os.path.join(temp,
os.path.basename(file_name)[:-4] + '.raw')
cmd = '{0} {1} {2}'.format(SILK_DECODER, file_name, raw_file)
out = subproc_succ(cmd)
for line in out.split(b'\n'):
if b'File length' in line:
duration = float(line[13:-3].strip())
break
else:
raise RuntimeError("Error decoding silk audio file!" + out.decode('utf-8'))
# TODO don't know how to do this with python
subproc_succ('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(raw_file, mp3_file))
else: else:
raise NotImplementedError("Audio file format cannot be recognized.") raise RuntimeError("Error decoding silk audio file!")
# I don't know how to do this with pysox
proc = call('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(
raw_file, mp3_file), shell=True)
os.unlink(raw_file)
else:
raise NotImplementedError("Unsupported Audio Format! This is a bug!")
try:
mp3_string = get_file_b64(mp3_file) mp3_string = get_file_b64(mp3_file)
os.unlink(mp3_file)
except:
raise RuntimeError("Failed to decode audio file: {}".format(file_name))
return mp3_string, duration return mp3_string, duration
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
fname = sys.argv[1] fname = sys.argv[1]
print(parse_wechat_audio_file(fname)[1]) print parse_wechat_audio_file(fname)[1]
+40 -106
View File
@@ -1,137 +1,71 @@
#!/usr/bin/env python3 #!/usr/bin/env python2
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# File: avatar.py
# Date: Thu Jun 18 00:02:07 2015 +0800
# Author: Yuxin Wu <[email protected]>
from PIL import Image from PIL import Image
import io import cStringIO
import glob
import os import os
import numpy as np import numpy as np
import logging import logging
import sqlite3 import sqlite3
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .common.textutil import md5 from common.textutil import ensure_bin_str, md5
def _filename_priority(s):
if "_hd" in s and s.endswith(".png"):
return 10
else:
return 1
class AvatarReader(object): class AvatarReader(object):
def __init__(self, res_dir, avt_db="avatar.index"): def __init__(self, avt_dir, avt_db="avatar.index"):
self.sfs_dir = os.path.join(res_dir, 'sfs') self.avt_dir = avt_dir
# new location of avatar, see #50
self.avt_dir = os.path.join(res_dir, 'avatar')
if not os.path.isdir(self.avt_dir) or len(os.listdir(self.avt_dir)) == 0:
self.avt_dir = None
self.avt_db = avt_db self.avt_db = avt_db
self._use_avt = True if self.avt_db is None or not os.path.isfile(self.avt_db):
if self.avt_db is not None: logger.warn(
if len(glob.glob(os.path.join(self.sfs_dir, 'avatar*'))) == 0: "Avatar database {} not found. Will not use avatar!".format(avt_db))
# has sfs/avatar* self.avt_db = None
self.avt_db = None
if self.avt_dir is None and self.avt_db is None:
logger.warn("Cannot find avatar storage. Will not use avatar!")
self._use_avt = False
def get_avatar_from_avtdb(self, avtid):
try:
candidates = self._search_avt_db(avtid)
candidates = sorted(candidates, key=lambda x: _filename_priority(x[0]), reverse=True)
for c in candidates:
path, offset, size = c
return self.read_img_from_block(path, offset, size)
except Exception:
pass
def get_avatar_from_avtdir(self, avtid) -> Image.Image | None:
dir1, dir2 = avtid[:2], avtid[2:4]
candidates = glob.glob(os.path.join(self.avt_dir, dir1, dir2, f"*{avtid}*"))
candidates = sorted(set(candidates), key=_filename_priority, reverse=True)
for cand in candidates:
if os.path.isdir(cand):
candidates.extend(os.path.join(cand, x) for x in os.listdir(cand))
for cand in candidates:
if os.path.isdir(cand):
continue
try:
if cand.endswith(".bm"):
return self.read_bm_file(cand)
else:
return Image.open(cand)
except Exception:
logger.exception("")
pass
def get_avatar(self, username): def get_avatar(self, username):
""" username: `username` field in db.rcontact""" """ username: `username` field in db.rcontact"""
if not self._use_avt: if self.avt_db is None: return None
username = ensure_bin_str(username)
filename = md5(username)
dir1, dir2 = filename[:2], filename[2:4]
filename = os.path.join(dir1, dir2,
"user_{}.png".format(filename))
try:
try:
pos, size = self.query_index(filename)
return self.read_img(pos, size)
except TypeError:
logger.warn("Avatar for {} not found in avatar database.".format(username))
return None
except Exception as e:
raise
print e
logger.warn("Failed to retrieve avatar!")
return None return None
avtid = md5(username.encode('utf-8'))
if self.avt_db is not None:
ret = self.get_avatar_from_avtdb(avtid)
if ret is not None:
return ret
if self.avt_dir is not None: def read_img(self, pos, size):
ret = self.get_avatar_from_avtdir(avtid)
if ret is not None:
return ret
logger.warning("Avatar file for {} not found.".format(username))
def save_avatar_to_avtdir(self, username: str, im: Image.Image):
"""Save a downloaded avatar to avtdir so it can be reused next time."""
avtid = md5(username.encode('utf-8'))
dir1, dir2 = avtid[:2], avtid[2:4]
fname = os.path.join(self.avt_dir, dir1, dir2, f"user_{avtid}.png")
os.makedirs(os.path.dirname(fname), exist_ok=True)
logger.info(f"Caching downloaded avatar for {username} to {fname}.")
im.save(fname, 'PNG')
def read_img_from_block(self, filename, pos, size):
file_idx = pos >> 32 file_idx = pos >> 32
fname = os.path.join(self.sfs_dir, fname = os.path.join(self.avt_dir,
'avatar.block.' + '{:05d}'.format(file_idx)) 'avatar.block.' + '{:05d}'.format(file_idx))
# offset of each block file: 17 + len(path) # a 64-byte offset of each block file
start_pos = pos - file_idx * (2**32) + 16 + len(filename) + 1 start_pos = pos - file_idx * (2**32) + 64
try: try:
with open(fname, 'rb') as f: with open(fname, 'rb') as f:
f.seek(start_pos) f.seek(start_pos)
data = f.read(size) data = f.read(size)
im = Image.open(io.BytesIO(data)) im = Image.open(cStringIO.StringIO(data))
return im return im
except IOError as e: except IOError as e:
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e))) logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
return None return None
def read_bm_file(self, fname): def query_index(self, filename):
# history at https://github.com/ppwwyyxx/wechat-dump/pull/14
with open(fname, 'rb') as f:
# filesize is 36880=96x96x4+16
size = (96, 96, 3)
img = np.zeros(size, dtype='uint8')
for i in range(96):
for j in range(96):
r, g, b, a = f.read(4)
img[i,j] = (r, g, b)
return Image.fromarray(img, mode="RGB")
def _search_avt_db(self, avtid):
conn = sqlite3.connect(self.avt_db) conn = sqlite3.connect(self.avt_db)
cursor = conn.execute("select FileName,Offset,Size from Index_avatar") cursor = conn.execute("select Offset,Size from Index_avatar where FileName='{}'".format(filename))
candidates = [] pos, size = cursor.fetchone()
for path, offset, size in cursor: return pos, size
if avtid in path:
candidates.append((path, offset, size))
return candidates
if __name__ == '__main__':
import sys
r = AvatarReader(sys.argv[1], sys.argv[2])
print(r.get_avatar(sys.argv[3]))
-51
View File
@@ -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

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