Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca9fc96b98 | ||
|
|
30eef32ae6 | ||
|
|
11d60a7487 | ||
|
|
928b92ade1 | ||
|
|
643bdd8984 | ||
|
|
a38c12fe4b | ||
|
|
a0d362d9f6 | ||
|
|
eedaa9c9e0 | ||
|
|
e1a1b0b946 | ||
|
|
b271469b94 | ||
|
|
c782325cfa | ||
|
|
837a9ef2f7 | ||
|
|
a508e29003 | ||
|
|
83bbb0a1fd | ||
|
|
1e754f229a | ||
|
|
6d9e4a00a9 | ||
|
|
20708ef755 | ||
|
|
cb76a41edc | ||
|
|
4e7593f3b2 | ||
|
|
3e86880913 | ||
|
|
52fb966dc5 | ||
|
|
a841e342cb | ||
|
|
5b7d293b06 | ||
|
|
541f6aa5ee | ||
|
|
b818b6e2aa | ||
|
|
6f86622c93 | ||
|
|
f61b9d59b7 | ||
|
|
78fffc6d33 | ||
|
|
8b5a28c39c | ||
|
|
f2ba97117c | ||
|
|
7eb7c60a81 | ||
|
|
b12cd93f0d | ||
|
|
e314b9d815 | ||
|
|
e46378aa25 | ||
|
|
64e5e5eb3f | ||
|
|
1f1a28e605 | ||
|
|
eeba4b7e6c | ||
|
|
f6acd0b0bf | ||
|
|
973993723b | ||
|
|
55784bd1bf | ||
|
|
7571334eca | ||
|
|
bfb2d0c914 | ||
|
|
aa9a1c9129 | ||
|
|
86224a8151 | ||
|
|
1395351755 | ||
|
|
628bc024c8 | ||
|
|
d6d2aaea55 | ||
|
|
4dc13a16f7 | ||
|
|
95826392df | ||
|
|
503a43c953 | ||
|
|
0f3fd34b54 | ||
|
|
0f97ecc9f0 | ||
|
|
a82bd281a7 | ||
|
|
249b2cf229 | ||
|
|
7fc6632907 | ||
|
|
e5240137ee | ||
|
|
ed21c3b630 | ||
|
|
8e68c59893 | ||
|
|
b82bcc7673 | ||
|
|
5893e7c6e6 | ||
|
|
20ae09848b | ||
|
|
9e776ca38e | ||
|
|
f8ede162c0 | ||
|
|
b2abfa7309 | ||
|
|
9f1ae9363d | ||
|
|
613f856662 | ||
|
|
4ee06766fd | ||
|
|
06217dbfb8 | ||
|
|
748cccb745 | ||
|
|
54dc3156dd | ||
|
|
c4c20633ea | ||
|
|
9a45dfaf35 | ||
|
|
b4d374cbe2 | ||
|
|
29115b104f | ||
|
|
3731443254 | ||
|
|
11081b2ba8 | ||
|
|
49a4187b46 | ||
|
|
ac958c32db | ||
|
|
1f2f7560bc | ||
|
|
91ac3126f8 | ||
|
|
212caa37cf | ||
|
|
a8777b9c1b | ||
|
|
3c28406935 | ||
|
|
cae58fb5ec | ||
|
|
260ae7ed28 | ||
|
|
e644246311 | ||
|
|
268544183d | ||
|
|
1135c81cc5 | ||
|
|
23a1c09797 | ||
|
|
876b41e3eb | ||
|
|
1e5f15f506 | ||
|
|
2a3d37dd98 | ||
|
|
fd64ea52d7 | ||
|
|
5eb6e71515 | ||
|
|
fcde19dc23 | ||
|
|
f3956fa50a | ||
|
|
1cfc8b707b | ||
|
|
28c7d3d599 | ||
|
|
683bdc30d0 | ||
|
|
85061e9bd5 | ||
|
|
76962d0ac1 | ||
|
|
b0b8a3e867 | ||
|
|
3f80e56365 | ||
|
|
6ab40e92ff | ||
|
|
0875e4d97a | ||
|
|
86619339a5 | ||
|
|
4d13d287d0 | ||
|
|
42ee381c7e | ||
|
|
13e3d456e8 | ||
|
|
ffcfa9237a | ||
|
|
b6066065ed | ||
|
|
62e66b8769 | ||
|
|
3f40bec3ef | ||
|
|
26115c48df | ||
|
|
82240095b6 | ||
|
|
ab1ea56242 | ||
|
|
4eae9cd7c1 | ||
|
|
c156a3142b | ||
|
|
5ca0e4e90e | ||
|
|
88003355c2 | ||
|
|
ce9e2550d0 | ||
|
|
5f176e3bd1 | ||
|
|
7015793abe | ||
|
|
f27476fa1c | ||
|
|
022e439cbe | ||
|
|
f0dc8fb3af | ||
|
|
79fbe7889f | ||
|
|
3b16f40ec2 | ||
|
|
d00c0e5cc1 | ||
|
|
9cc70cec79 | ||
|
|
7457e23829 | ||
|
|
25092daa46 | ||
|
|
e6208102e4 | ||
|
|
15c8fd6168 | ||
|
|
b3ac340072 | ||
|
|
85247692c6 | ||
|
|
ed851845fa | ||
|
|
c60296c6dd | ||
|
|
f399643bb9 | ||
|
|
c6044221d7 | ||
|
|
3358a89faf | ||
|
|
b3acacddfd | ||
|
|
06b7e631b3 | ||
|
|
7348dabbb3 | ||
|
|
41feb7c180 | ||
|
|
7492366d85 | ||
|
|
ca6334e48e | ||
|
|
8ae9a81283 | ||
|
|
cdb0272770 | ||
|
|
987d8bda78 | ||
|
|
5cc0cefb2b | ||
|
|
99faea5637 | ||
|
|
4572cb045e | ||
|
|
9b3dbb2665 | ||
|
|
e7b41487db | ||
|
|
b1dd50e08a | ||
|
|
869bc15e1f | ||
|
|
b85b515137 | ||
|
|
c8c64d9142 | ||
|
|
9656bb3ffc | ||
|
|
a60fa569a9 | ||
|
|
142caa80fc | ||
|
|
49ff512437 | ||
|
|
ca63c5a190 | ||
|
|
7c5a4a3df8 | ||
|
|
651c5dda1c | ||
|
|
9c7ec015ab | ||
|
|
69fb2c5dc9 | ||
|
|
442a3fc6fc | ||
|
|
34a3a7accc | ||
|
|
26e2881f66 | ||
|
|
0baca87c3f | ||
|
|
7468f7db35 | ||
|
|
8eea048660 | ||
|
|
6476338832 | ||
|
|
39dfcf677b | ||
|
|
b35d96418b | ||
|
|
7e28d5e221 | ||
|
|
cddca228b0 | ||
|
|
c2c9de8fcb | ||
|
|
add87ef670 | ||
|
|
1a655c37e7 | ||
|
|
29cb4037e2 | ||
|
|
c4b497145c | ||
|
|
a3f3cedf7a | ||
|
|
555ba90930 | ||
|
|
f477db0c84 | ||
|
|
b892e519c1 | ||
|
|
138ebf63ee | ||
|
|
93b68015a7 | ||
|
|
91c8b63875 | ||
|
|
f35e4248f6 | ||
|
|
c23caa9ad8 | ||
|
|
9cb981749a | ||
|
|
8c17349a2c | ||
|
|
9888506420 | ||
|
|
bcaa7dcae6 | ||
|
|
7ee52e32f5 | ||
|
|
f03eb9b5bd | ||
|
|
e32547421a | ||
|
|
b8f7a15213 | ||
|
|
d03b353173 | ||
|
|
937fa8b88f | ||
|
|
e2d2f5166f | ||
|
|
4065a9bbac | ||
|
|
ea0204fad7 | ||
|
|
4dd5c3ac36 | ||
|
|
0622f64f16 | ||
|
|
c41bdf3815 | ||
|
|
0d219538a6 | ||
|
|
5b97d3d95f | ||
|
|
4bf6d00000 | ||
|
|
378ad73751 | ||
|
|
f91316a9fa | ||
|
|
4b847d0c3b | ||
|
|
0e89e304c3 | ||
|
|
6ba264130c | ||
|
|
74c58a3b9c | ||
|
|
543c79dfc1 | ||
|
|
9c5b9afa12 | ||
|
|
fb05d2722f | ||
|
|
4006bd39df | ||
|
|
d614eaf454 | ||
|
|
5f39a09da3 | ||
|
|
3fcfd43921 | ||
|
|
b15c0f0d79 | ||
|
|
9793382814 | ||
|
|
b7d97d4cb2 | ||
|
|
680ee0e962 | ||
|
|
35dbf6fcda | ||
|
|
a65c9280b5 | ||
|
|
9b55b7a303 | ||
|
|
436717da10 | ||
|
|
83040b8a8d | ||
|
|
68c0e92aac |
@@ -3,3 +3,9 @@
|
|||||||
output/
|
output/
|
||||||
resource
|
resource
|
||||||
*.db
|
*.db
|
||||||
|
*.db.decoded
|
||||||
|
*2015*
|
||||||
|
*2017*
|
||||||
|
2020*
|
||||||
|
avatar.index
|
||||||
|
emoji.cache
|
||||||
|
|||||||
@@ -1,118 +1,97 @@
|
|||||||
## Dump WeChat Messages from Android
|
## Render WeChat Messages from Android
|
||||||
|
|
||||||
## 导出安卓微信聊天数据
|
## 安卓微信消息记录分析工具
|
||||||
|
|
||||||
WeChat(微信), as the most popular mobile IM app in China, doesn't give users any method to export well-formatted history message.
|
WeChat, as the most popular mobile IM app in China, doesn't provide any methods to read its message history.
|
||||||
This tool can parse and export WeChat messages on a rooted android phone.
|
Customers are not able to analyze their own chat messages or interact with other data analysis tools.
|
||||||
|
|
||||||
Right now it can dump messages in text-only mode, or generate a single-file html containing voice messages, images, emoji, etc.
|
We provide this tool that can parse WeChat messages from a rooted android phone.
|
||||||
|
This is necessary to provide interoperability between WeChat messages and other message analysis tools.
|
||||||
|
As examples, we provide sample scripts to obtain statistics of the message history and
|
||||||
|
render the messages into self-contained html files including voice messages, images, emojis, videos, etc.
|
||||||
|
Users can also write custom programs based on this tool to manage their chat messages.
|
||||||
|
|
||||||
__NEWS__: WeChat 6.0+ uses silk to encode audio. The code is updated.
|
The tool is last verified to work with latest version of WeChat on 2025/01/01.
|
||||||
|
If the tool works for you, please take a moment to add your phone/OS to [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
|
||||||
|
|
||||||
__NEWS__: WeChat 6.3 uses a new avatar storage. The code is updated.
|
## How to use:
|
||||||
|
|
||||||
If this tools works for you, please take a moment to __add your phone/OS to__ [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
|
|
||||||
If it doesn't work, please leave an issue together with your phone/OS/wechat version.
|
|
||||||
|
|
||||||
### How to use:
|
|
||||||
|
|
||||||
#### Dependencies:
|
#### Dependencies:
|
||||||
+ python-PIL
|
+ adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash.
|
||||||
+ [PyQuery](https://pypi.python.org/pypi/pyquery/1.2.1)
|
+ Python >= 3.8
|
||||||
+ [pysox](https://pypi.python.org/pypi/pysox/0.3.6.alpha)
|
+ sox (command line tools)
|
||||||
+ [pysqlcipher](https://pypi.python.org/pypi/pysqlcipher)
|
+ Silk audio decoder (included; build it with `./third-party/compile_silk.sh`)
|
||||||
+ numpy
|
+ Other python dependencies: `pip install -r requirements.txt`.
|
||||||
+ csscompressor (suggested, optional)
|
|
||||||
+ adb and rooted android phone connected to a Linux/Mac OS.
|
|
||||||
+ Silk audio decoder (included; just run `./third-party/compile_silk.sh`)
|
|
||||||
+ gnu-sed
|
|
||||||
|
|
||||||
#### Get Necessary Data:
|
#### Get Necessary Data:
|
||||||
|
|
||||||
Note that commands involving `./android-interact.sh` are meant to be run on the computer.
|
1. Pull database file and (for older WeChat versions) avatar index:
|
||||||
|
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
|
||||||
+ (Requires Linux or Mac) Get the decrypted WeChat database and the avatar index:
|
+ Manual:
|
||||||
+ Automatic: `./android-interact.sh db-decrypt`
|
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device.
|
||||||
+ Requires rooted adb. If the OS distribution does not come with adb support, you can download an app such as https://play.google.com/store/apps/details?id=eu.chainfire.adbd
|
It should be a 32-character-long name consisting of hexadecimal digits.
|
||||||
+ Manual:
|
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device.
|
||||||
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device. It should be a 32-character-long name consisting of hexadecimal digits.
|
2. Decode `EnMicroMsg.db`. We do not provide instructions to do that.
|
||||||
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/{EnMicroMsg.db,sfs/avatar.index}` from the device, possible ways are:
|
3. Copy the unencrypted WeChat user resource directory `/data/data/com.tencent.mm/MicroMsg/${userid}/{avatar,emoji,image2,sfs,video,voice2}` from the phone to the `resource` directory:
|
||||||
+ `./android-interact.sh db`
|
|
||||||
+ Use your rooted file system manager app
|
|
||||||
+ Get WeChat uin (an integer), possible ways are:
|
|
||||||
+ `./android-interact.sh uin`, which pulls the value from `/data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml`
|
|
||||||
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
|
|
||||||
+ Get your phone IMEI number (a positive integer), possible ways are:
|
|
||||||
+ `./android-interact.sh imei`
|
|
||||||
+ Call `*#06#` on your phone
|
|
||||||
+ Find IMEI in system settings
|
|
||||||
+ Decrypt database, will produce `decrypted.db`:
|
|
||||||
|
|
||||||
```
|
|
||||||
./decrypt-db.py <path to EnMicroMsg.db> <imei> <uin>
|
|
||||||
```
|
|
||||||
|
|
||||||
NOTE: you may need to try different ways to getting imei & uin,
|
|
||||||
because things behave differently on different phones.
|
|
||||||
|
|
||||||
Also, if the decryption doesn't work with pysqlcipher, maybe try the version of sqlcipher in `legacy`.
|
|
||||||
|
|
||||||
|
|
||||||
+ Copy the WeChat user resource directory `/mnt/sdcard/tencent/MicroMsg/${userid}/{emoji,image2,sfs,video,voice2}` from the phone's SD card to the `resource` directory:
|
|
||||||
+ `./android-interact.sh res`
|
+ `./android-interact.sh res`
|
||||||
+ You might need to tweak `RES_DIR` in the script if the default doesn't work
|
+ Change `RES_DIR` in the script if the location of these directories is different on your phone.
|
||||||
+ This can take a __long__ time. Some ways to do this faster:
|
For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
|
||||||
+ If there's enough free space on the SD card, you can combine all the files via `busybox tar` without compression in the `adb shell`, use `adb pull` to copy the `tar` archive to the computer, and then extract it. BusyBox is needed as the Android system's `tar` may choke on long paths.
|
+ This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive,
|
||||||
+ Alternatively, you can use pipes. This is slower, but doesn't require any free space on the SD card:
|
`busybox tar` is recommended as the Android system's `tar` may choke on long paths.
|
||||||
|
+ In the end, we need a `resource` directory with the following subdir: `avatar,emoji,image2,sfs,video,voice2`.
|
||||||
|
|
||||||
```sh
|
4. (Optional) Install and start a WXGF decoder server on an android device. Without this, certain WXGF images will not be rendered or will be rendered in low resolution.
|
||||||
# copy MicroMsg to the current directory
|
See [WXGFDecoder](WXGFDecoder) for instructions.
|
||||||
adb shell 'cd /mnt/sdcard/tencent &&
|
|
||||||
busybox tar czf - MicroMsg 2>/dev/null | busybox base64' |
|
4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2)
|
||||||
base64 -di | tar xzf -
|
and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering.
|
||||||
```
|
|
||||||
|
wget -c https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2
|
||||||
|
tar xf emoji.cache.tar.bz2
|
||||||
|
|
||||||
#### Run:
|
#### Run:
|
||||||
+ Parse and dump text messages of __every__ chat (requires `decrypted.db`):
|
+ Parse and dump text messages of __every__ chat (requires decoded database):
|
||||||
|
|
||||||
```
|
```
|
||||||
./dump-msg.py decrypted.db output_dir
|
./dump-msg.py decoded.db output_dir
|
||||||
```
|
```
|
||||||
|
|
||||||
+ List all chats (requires `decrypted.db`):
|
+ List all chats (required decoded database):
|
||||||
|
|
||||||
```
|
```
|
||||||
./list-chats.py decrypted.db
|
./list-chats.py decoded.db
|
||||||
```
|
```
|
||||||
|
|
||||||
+ Generate statistical report on text messages (requires `output_dir` from `./dump-msg.py`):
|
+ Generate statistics report on text messages (requires `output_dir` from `./dump-msg.py`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./count-message.sh output_dir
|
./count-message.sh output_dir
|
||||||
```
|
```
|
||||||
|
|
||||||
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires `decrypted.db`, `avatar.index`, and `resource`):
|
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires decoded database and `resource`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./dump-html.py decrypted.db avatar.index resource "<contact_name>" output.html
|
./dump-html.py "<contact_display_name>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* The output file is `output.html`. Check `./dump-html.py -h` to use different input/output paths.
|
||||||
|
* Add `--wxgf-server ws://xx.xx.xx.xx:xxxx` to use a WXGF decoder server.
|
||||||
|
|
||||||
### Examples:
|
### Examples:
|
||||||
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
|
||||||
|
|
||||||
Screenshots of generated html:
|
Screenshots of generated html:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### TODO List
|
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
||||||
+ Search by uid/username
|
|
||||||
+ Faster way to copy a directory from android (I don't know..).
|
### TODO List (help needed!)
|
||||||
+ Fix rare unhandled types: > 10000 and < 0
|
* After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 -
|
||||||
+ Better user experiences... see `grep 'TODO' wechat -R`
|
they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis.
|
||||||
+ more easy-to-use for non-programmers (GUI?)
|
* Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu).
|
||||||
|
* Fix rare unhandled message types: > 10000 and < 0
|
||||||
|
* Better user experiences... see `grep 'TODO' wechat -R`
|
||||||
|
|
||||||
### Donate!
|
### Donate!
|
||||||
Paypal:
|
|
||||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump¤cy_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¤cy_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>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
.idea
|
||||||
|
app/release
|
||||||
|
.DS_Store
|
||||||
|
build
|
||||||
|
captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
# WXGF Decoder
|
||||||
|
|
||||||
|
This is an android app that decodes wechat's proprietary "WXGF/WXAM" image format.
|
||||||
|
|
||||||
|
|
||||||
|
## Build & Install
|
||||||
|
|
||||||
|
Open this project directory using android studio, and build against an ARMv8 device.
|
||||||
|
|
||||||
|
Pre-built APKs can be downloaded at [releases](https://github.com/ppwwyyxx/wechat-dump/releases).
|
||||||
|
|
||||||
|
Install the APK to your android phone.
|
||||||
|
|
||||||
|
Note that this app uses native libs, therefore requires an ARMv8 compatible device to run.
|
||||||
|
The native libs are extracted from the WeChat v7.0.22 APK downloaded from [here](https://www.apkmirror.com/apk/wechat-tencent/wechat/wechat-7-0-22-release/wechat-7-0-22-android-apk-download/)
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshot.jpg" alt="screenshot" width="400">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
1. (Optional) Configure the port number if needed, or leave it to default.
|
||||||
|
2. Click "Start Server"
|
||||||
|
3. Use the server address `ws://xx.xx.xx.xx:xxxx` to decode
|
||||||
|
* Make sure this address is accessible from the computer that runs `wechat-dump`.
|
||||||
|
For example, connect them into the same Wifi.
|
||||||
|
4. Once a client is connected and decoding request is received, logs will be printed in the app.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import com.android.build.api.dsl.Packaging
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.ppwwyyxx.wxgfdecoder"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.ppwwyyxx.wxgfdecoder"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
pickFirst( "jniLibs/arm64-v8a/libvoipCodec.so")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.java-websocket:Java-WebSocket:1.5.4")
|
||||||
|
implementation(libs.appcompat)
|
||||||
|
implementation(libs.material)
|
||||||
|
implementation(libs.constraintlayout)
|
||||||
|
implementation(libs.lifecycle.livedata.ktx)
|
||||||
|
implementation(libs.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.navigation.fragment)
|
||||||
|
implementation(libs.navigation.ui)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.ext.junit)
|
||||||
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.WXAMConverter"
|
||||||
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import com.tencent.mm.plugin.gif.MMWXGFJNI;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.ppwwyyxx.wxgfdecoder.databinding.ActivityMainBinding;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
import org.java_websocket.WebSocket;
|
||||||
|
import org.java_websocket.handshake.ClientHandshake;
|
||||||
|
import org.java_websocket.server.WebSocketServer;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private ActivityMainBinding binding;
|
||||||
|
private Button startButton;
|
||||||
|
private TextView hostnameTextView, portTextView, logTextView;
|
||||||
|
private EditText portEditText;
|
||||||
|
private WebSocketServer server;
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("wechatcommon");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
// Keep screen on
|
||||||
|
getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
// If the app is running on Android 8.0 or higher, request for ignoring battery optimizations
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
|
if (!powerManager.isIgnoringBatteryOptimizations(getPackageName())) {
|
||||||
|
// TODO: request for ignoring battery optimizations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MMWXGFJNI.initialize(getApplicationInfo().nativeLibraryDir);
|
||||||
|
|
||||||
|
// Set up UI
|
||||||
|
startButton = findViewById(R.id.start_server_button);
|
||||||
|
hostnameTextView = findViewById(R.id.text_view_hostname);
|
||||||
|
portTextView = findViewById(R.id.text_view_port);
|
||||||
|
portEditText = findViewById(R.id.edit_text_port);
|
||||||
|
logTextView = findViewById(R.id.log_text_area);
|
||||||
|
logTextView.setText("");
|
||||||
|
startButton.setOnClickListener(v -> {
|
||||||
|
try {
|
||||||
|
startWebSocketServer();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run a unit test:
|
||||||
|
testWxgfDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startWebSocketServer() throws InterruptedException {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop();
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int port;
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(portEditText.getText().toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
port = 8080;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
server = new WebSocketServer(new InetSocketAddress(port)) {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||||
|
printLog("new connection from " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||||
|
printLog("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onMessage( WebSocket conn, ByteBuffer message ) {
|
||||||
|
byte[] msg = message.array();
|
||||||
|
|
||||||
|
printLog("received binary message from " + conn.getRemoteSocketAddress() + ": size=" + msg.length
|
||||||
|
+ " -- " + Arrays.toString(Arrays.copyOfRange(msg, 0, 10)));
|
||||||
|
|
||||||
|
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(msg);
|
||||||
|
if (res != null) {
|
||||||
|
printLog("After decoding: size=" + res.length + " -- " +
|
||||||
|
Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
|
||||||
|
ByteBuffer response = ByteBuffer.wrap(res);
|
||||||
|
conn.send(response); // the response gif
|
||||||
|
} else {
|
||||||
|
printLog("Failed to decode.");
|
||||||
|
conn.send("FAILED".getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage( WebSocket conn, String message) {
|
||||||
|
printLog("Unexpected! received text message from " + conn.getRemoteSocketAddress() + ": " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(WebSocket conn, Exception ex) {
|
||||||
|
printLog("an error occurred on connection " + conn + ":" + ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
try {
|
||||||
|
String hostName = getLocalIpAddress(true);
|
||||||
|
int port = server.getPort();
|
||||||
|
printLog("server started successfully at ws://" + hostName + ":" + port);
|
||||||
|
|
||||||
|
runOnUiThread(() -> { // update UI in UI thread
|
||||||
|
hostnameTextView.setText("Hostname: " + hostName);
|
||||||
|
portTextView.setText("Port: " + String.valueOf(port));
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
server.start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printLog(String log) {
|
||||||
|
System.out.println(log);
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
String currentLog = logTextView.getText().toString();
|
||||||
|
logTextView.setText(currentLog + "\n" + log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocalIpAddress(boolean useIPv4) throws SocketException {
|
||||||
|
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||||
|
for (NetworkInterface intf : interfaces) {
|
||||||
|
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
|
||||||
|
for (InetAddress addr : addrs) {
|
||||||
|
if (!addr.isLoopbackAddress()) {
|
||||||
|
String sAddr = addr.getHostAddress();
|
||||||
|
boolean isIPv4 = sAddr.indexOf(':')<0;
|
||||||
|
if (useIPv4) {
|
||||||
|
if (isIPv4)
|
||||||
|
return sAddr;
|
||||||
|
} else {
|
||||||
|
return sAddr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
return "localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods below are for debugging the decoder:
|
||||||
|
private void testWxgfDecoder() {
|
||||||
|
byte[] wxgf = null;
|
||||||
|
try {
|
||||||
|
wxgf = readBinary(this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
System.out.println("Unit test: wxgf size=" + wxgf.length);
|
||||||
|
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(wxgf);
|
||||||
|
System.out.println("Unit test: res size=" + res.length + " -- " + Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFile(this, "test.jpg", res);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
if (wxgf.length != 64367 || res.length != 705188) {
|
||||||
|
throw new RuntimeException("test failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static byte[] readBinary(Context context) throws IOException {
|
||||||
|
InputStream inputStream = context.getResources().openRawResource(R.raw.test_wxgf); // R.raw.image refers to your image.jpg file
|
||||||
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = inputStream.read(buffer)) != -1) {
|
||||||
|
byteArrayOutputStream.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
inputStream.close(); // Close the input stream
|
||||||
|
return byteArrayOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeFile(Context context, String filename, byte[] data) throws IOException {
|
||||||
|
File file = new File(context.getExternalFilesDir(null), filename); // Use null for the type argument to get the root external files directory
|
||||||
|
System.out.println("Writing file to: " + file.getAbsolutePath());
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(file);
|
||||||
|
outputStream.write(data);
|
||||||
|
outputStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentDashboardBinding;
|
||||||
|
|
||||||
|
public class DashboardFragment extends Fragment {
|
||||||
|
|
||||||
|
private FragmentDashboardBinding binding;
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
DashboardViewModel dashboardViewModel =
|
||||||
|
new ViewModelProvider(this).get(DashboardViewModel.class);
|
||||||
|
|
||||||
|
binding = FragmentDashboardBinding.inflate(inflater, container, false);
|
||||||
|
View root = binding.getRoot();
|
||||||
|
|
||||||
|
final TextView textView = binding.textDashboard;
|
||||||
|
dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
public class DashboardViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final MutableLiveData<String> mText;
|
||||||
|
|
||||||
|
public DashboardViewModel() {
|
||||||
|
mText = new MutableLiveData<>();
|
||||||
|
mText.setValue("This is dashboard fragment");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.home;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentHomeBinding;
|
||||||
|
|
||||||
|
public class HomeFragment extends Fragment {
|
||||||
|
|
||||||
|
private FragmentHomeBinding binding;
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
HomeViewModel homeViewModel =
|
||||||
|
new ViewModelProvider(this).get(HomeViewModel.class);
|
||||||
|
|
||||||
|
binding = FragmentHomeBinding.inflate(inflater, container, false);
|
||||||
|
View root = binding.getRoot();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.home;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
public class HomeViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final MutableLiveData<String> mText;
|
||||||
|
|
||||||
|
public HomeViewModel() {
|
||||||
|
mText = new MutableLiveData<>();
|
||||||
|
mText.setValue("This is home fragment");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentNotificationsBinding;
|
||||||
|
|
||||||
|
public class NotificationsFragment extends Fragment {
|
||||||
|
|
||||||
|
private FragmentNotificationsBinding binding;
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
NotificationsViewModel notificationsViewModel =
|
||||||
|
new ViewModelProvider(this).get(NotificationsViewModel.class);
|
||||||
|
|
||||||
|
binding = FragmentNotificationsBinding.inflate(inflater, container, false);
|
||||||
|
View root = binding.getRoot();
|
||||||
|
|
||||||
|
final TextView textView = binding.textNotifications;
|
||||||
|
notificationsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
binding = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
public class NotificationsViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final MutableLiveData<String> mText;
|
||||||
|
|
||||||
|
public NotificationsViewModel() {
|
||||||
|
mText = new MutableLiveData<>();
|
||||||
|
mText.setValue("This is notifications fragment");
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.tencent.mm.plugin.fts.jni;
|
||||||
|
|
||||||
|
public class FTSJNIUtils {
|
||||||
|
public static native int countTokens(String str);
|
||||||
|
|
||||||
|
public static native int icuInitialize(String str);
|
||||||
|
|
||||||
|
public static native String icuTokenizer(String str);
|
||||||
|
|
||||||
|
public static native void nativeInitFts(long j, byte[] bArr);
|
||||||
|
|
||||||
|
public static native int stringCompareUtfBinary(String str, String str2);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//File:
|
||||||
|
package com.tencent.mm.plugin.gif;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
/*
|
||||||
|
*import com.tencent.matrix.trace.core.AppMethodBeat;
|
||||||
|
*import com.tencent.mm.loader.p558j.C11743b;
|
||||||
|
*import com.tencent.mm.sdk.platformtools.C52656ad;
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* renamed from: com.tencent.mm.plugin.gif.MMWXGFJNI */
|
||||||
|
public class MMWXGFJNI {
|
||||||
|
public static final int PIC_TYPE_JPG = 0;
|
||||||
|
public static final int PIC_TYPE_PNG = 1;
|
||||||
|
private static final String TAG = "MicroMsg.JNI.MMWXGF";
|
||||||
|
static int mECode;
|
||||||
|
public static boolean mIsInit;
|
||||||
|
|
||||||
|
|
||||||
|
public static native int nativeAV2Gif(String str, String str2);
|
||||||
|
|
||||||
|
public static native int nativeAV2Gif(byte[] bArr, byte[] bArr2);
|
||||||
|
|
||||||
|
public static native int nativeAddGifEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
|
||||||
|
|
||||||
|
public static native int nativeAddWxAMEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
|
||||||
|
|
||||||
|
public static native int nativeDecodeBufferFrame(long j, byte[] bArr, int i, Bitmap bitmap, int[] iArr);
|
||||||
|
|
||||||
|
public static native int nativeDecodeBufferHeader(long j, byte[] bArr, int i);
|
||||||
|
|
||||||
|
public static native byte[] nativeFinishGifEncode(long j);
|
||||||
|
|
||||||
|
public static native byte[] nativeFinishWxAMEncode(long j);
|
||||||
|
|
||||||
|
public static native int nativeGetOption(long j, byte[] bArr, int i, int[] iArr);
|
||||||
|
|
||||||
|
public static native int nativeInit(String str);
|
||||||
|
|
||||||
|
public static native long nativeInitGifEncode(int i, int i2, long j);
|
||||||
|
|
||||||
|
public static native long nativeInitWxAMDecoder();
|
||||||
|
|
||||||
|
public static native long nativeInitWxAMEncoder(int i, int i2, long j, int i3);
|
||||||
|
|
||||||
|
public static native boolean nativeIsWXGF(byte[] bArr, int i);
|
||||||
|
|
||||||
|
public static native int nativePic2Wxam(String str, String str2, int i, int i2);
|
||||||
|
|
||||||
|
public static native int nativePic2WxamWithWH(String str, String str2, int i, int i2, int i3, int i4);
|
||||||
|
|
||||||
|
public static native int nativePic2WxamWithWH(String str, byte[] bArr, int i, int i2, int i3, int i4, int i5, int i6, int i7);
|
||||||
|
|
||||||
|
public static native int nativeRewindBuffer(long j);
|
||||||
|
|
||||||
|
public static native int nativeUninit(long j);
|
||||||
|
|
||||||
|
public static native int nativeWxam2Pic(String str, String str2);
|
||||||
|
|
||||||
|
public static native byte[] nativeWxam2PicBuf(byte[] bArr);
|
||||||
|
|
||||||
|
public static native byte[] nativeWxamToGif(byte[] bArr);
|
||||||
|
|
||||||
|
public static void initialize(String nativeDir) {
|
||||||
|
// init wxgf decoder
|
||||||
|
String soFilePath = nativeDir + "/libvoipCodec.so";
|
||||||
|
File soFile = new File(soFilePath);
|
||||||
|
if (!soFile.exists())
|
||||||
|
throw new RuntimeException("missing so file " + soFilePath);
|
||||||
|
int initSuccess = nativeInit(soFilePath); // return -1 if missing so
|
||||||
|
if (initSuccess < 0)
|
||||||
|
throw new RuntimeException("nativeInit failed " + initSuccess);
|
||||||
|
mIsInit = true;
|
||||||
|
}
|
||||||
|
/* // Original code below:
|
||||||
|
*static {
|
||||||
|
* AppMethodBeat.m13231i(104681);
|
||||||
|
* mIsInit = false;
|
||||||
|
* mECode = -1;
|
||||||
|
* C52656ad.m88211i(TAG, "static MMWXGFJNI");
|
||||||
|
* if (!mIsInit) {
|
||||||
|
* String str = C11743b.ahY() + "lib/libvoipCodec.so";
|
||||||
|
* int nativeInit = nativeInit(str);
|
||||||
|
* mECode = nativeInit;
|
||||||
|
* mIsInit = nativeInit >= 0;
|
||||||
|
* C52656ad.m88212i(TAG, "native init MMWXGF mECode:%d result:%b :%s", Integer.valueOf(mECode), Boolean.valueOf(mIsInit), str);
|
||||||
|
* AppMethodBeat.m13232o(104681);
|
||||||
|
* return;
|
||||||
|
* }
|
||||||
|
* C52656ad.m88205d(TAG, "MMWXGF has init.");
|
||||||
|
* AppMethodBeat.m13232o(104681);
|
||||||
|
*}
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static int getErrorCode() {
|
||||||
|
return mECode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/nav_host_fragment_activity_main"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navGraph="@navigation/mobile_navigation" />
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_text_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Enter Port Number (default 8080)"
|
||||||
|
android:inputType="number" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_hostname"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Hostname: " />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_view_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Port: " />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/start_server_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Start Server" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="400dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/log_text_area"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
|
||||||
|
<!-- android:id="@+id/nav_view"-->
|
||||||
|
<!-- android:layout_width="match_parent"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- android:layout_marginStart="0dp"-->
|
||||||
|
<!-- android:layout_marginEnd="0dp"-->
|
||||||
|
<!-- android:layout_gravity="bottom"-->
|
||||||
|
<!-- android:background="?android:attr/windowBackground"-->
|
||||||
|
|
||||||
|
<!-- app:menu="@menu/bottom_nav_menu" />-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.dashboard.DashboardFragment">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_dashboard"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.home.HomeFragment">
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.notifications.NotificationsFragment">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_notifications"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_home"
|
||||||
|
android:icon="@drawable/ic_home_black_24dp"
|
||||||
|
android:title="@string/title_home" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_dashboard"
|
||||||
|
android:icon="@drawable/ic_dashboard_black_24dp"
|
||||||
|
android:title="@string/title_dashboard" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/navigation_notifications"
|
||||||
|
android:icon="@drawable/ic_notifications_black_24dp"
|
||||||
|
android:title="@string/title_notifications" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/mobile_navigation"
|
||||||
|
app:startDestination="@+id/navigation_home">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_home"
|
||||||
|
android:name="com.ppwwyyxx.wxgfdecoder.ui.home.HomeFragment"
|
||||||
|
android:label="@string/title_home"
|
||||||
|
tools:layout="@layout/fragment_home" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_dashboard"
|
||||||
|
android:name="com.ppwwyyxx.wxgfdecoder.ui.dashboard.DashboardFragment"
|
||||||
|
android:label="@string/title_dashboard"
|
||||||
|
tools:layout="@layout/fragment_dashboard" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_notifications"
|
||||||
|
android:name="com.ppwwyyxx.wxgfdecoder.ui.notifications.NotificationsFragment"
|
||||||
|
android:label="@string/title_notifications"
|
||||||
|
tools:layout="@layout/fragment_notifications" />
|
||||||
|
</navigation>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">WXGF Decoder</string>
|
||||||
|
<string name="title_home">Home</string>
|
||||||
|
<string name="title_dashboard">Dashboard</string>
|
||||||
|
<string name="title_notifications">Notifications</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older that API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.8.0"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.1.5"
|
||||||
|
espressoCore = "3.5.1"
|
||||||
|
appcompat = "1.6.1"
|
||||||
|
material = "1.10.0"
|
||||||
|
constraintlayout = "2.1.4"
|
||||||
|
lifecycleLivedataKtx = "2.6.1"
|
||||||
|
lifecycleViewmodelKtx = "2.6.1"
|
||||||
|
navigationFragment = "2.6.0"
|
||||||
|
navigationUi = "2.6.0"
|
||||||
|
media3Common = "1.5.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
|
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
|
lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
|
||||||
|
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||||
|
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
|
||||||
|
navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
|
||||||
|
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#Wed Jan 22 21:22:48 PST 2025
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
|
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "WXGFDecoder"
|
||||||
|
include(":app")
|
||||||
@@ -1,48 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# File: android-interact.sh
|
# File: android-interact.sh
|
||||||
# Date: Fri Jun 26 10:38:07 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
|
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
|
||||||
PROG_DIR=`dirname "$PROG_NAME"`
|
PROG_DIR=`dirname "$PROG_NAME"`
|
||||||
cd "$PROG_DIR"
|
cd "$PROG_DIR"
|
||||||
|
|
||||||
source compatibility.sh
|
|
||||||
|
|
||||||
# Please check that your path is the same, since this might be different among devices
|
# Please check that your path is the same, since this might be different among devices
|
||||||
RES_DIR="/mnt/sdcard/tencent/MicroMsg"
|
# RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
|
||||||
MM_DIR="/data/data/com.tencent.mm"
|
RES_DIR="/data/data/com.tencent.mm/MicroMsg"
|
||||||
|
|
||||||
echo "Starting rooted adb server..."
|
if [[ $1 == "db" || $1 == "res" ]]; then
|
||||||
adb root
|
|
||||||
|
|
||||||
if [[ $1 == "uin" ]]; then
|
|
||||||
adb pull $MM_DIR/shared_prefs/system_config_prefs.xml 2>/dev/null
|
|
||||||
uin=$($GREP 'default_uin' system_config_prefs.xml | $GREP -o 'value="\-?[0-9]*' | cut -c 8-)
|
|
||||||
[[ -n $uin ]] || {
|
|
||||||
>&2 echo "Failed to get wechat uin. You can try other methods, or report a bug."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
rm system_config_prefs.xml
|
|
||||||
echo "Got wechat uin: $uin"
|
|
||||||
elif [[ $1 == "imei" ]]; then
|
|
||||||
imei=$(adb shell dumpsys iphonesubinfo | $GREP 'Device ID' | $GREP -o '[0-9]+')
|
|
||||||
[[ -n $imei ]] || {
|
|
||||||
imei=$(adb shell service call iphonesubinfo 1 | awk -F "'" '{print $2}' | sed 's/[^0-9A-F]*//g' | tr -d '\n')
|
|
||||||
}
|
|
||||||
[[ -n $imei ]] || {
|
|
||||||
>&2 echo "Failed to get imei. You can try other methods, or report a bug."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "Got imei: $imei"
|
|
||||||
elif [[ $1 == "db" || $1 == "res" ]]; then
|
|
||||||
echo "Looking for user dir name..."
|
echo "Looking for user dir name..."
|
||||||
sleep 1 # sometimes adb complains: device not found
|
# look for dirname which looks like md5 (32 alpha-numeric chars)
|
||||||
userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' \
|
userList=$(adb shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}')
|
||||||
| awk '{if (length() == 32) print}')
|
numUser=$(echo "$userList" | wc -l)
|
||||||
numUser=$(echo $userList | wc -l)
|
|
||||||
# choose the first user.
|
# choose the first user.
|
||||||
chooseUser=$(echo $userList | head -n1)
|
chooseUser=$(echo "$userList" | head -n1)
|
||||||
[[ -n $chooseUser ]] || {
|
[[ -n $chooseUser ]] || {
|
||||||
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
|
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -50,54 +23,61 @@ elif [[ $1 == "db" || $1 == "res" ]]; then
|
|||||||
echo "Found $numUser user(s). User chosen: $chooseUser"
|
echo "Found $numUser user(s). User chosen: $chooseUser"
|
||||||
|
|
||||||
if [[ $1 == "res" ]]; then
|
if [[ $1 == "res" ]]; then
|
||||||
echo "Pulling resources... this might take a long time, because adb sucks..."
|
mkdir -p resource
|
||||||
mkdir -p resource; cd resource
|
(
|
||||||
for d in image2 voice2 emoji video sfs; do
|
cd resource || exit
|
||||||
mkdir -p $d; cd $d
|
echo "Pulling resources... "
|
||||||
adb pull $RES_DIR/$chooseUser/$d
|
for d in avatar image2 voice2 emoji video sfs; do
|
||||||
cd ..
|
echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..."
|
||||||
[[ -d $d ]] || {
|
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null |
|
||||||
>&2 echo "Failed to download resource directory: $RES_DIR/$chooseUser/$d"
|
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
||||||
exit 1
|
[[ -d $d ]] && continue
|
||||||
}
|
|
||||||
done
|
echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..."
|
||||||
cd ..
|
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" |
|
||||||
echo "Resource pulled at ./resource"
|
base64 -di | tar xzf - --strip-components 5
|
||||||
echo "Total size: $(du -sh resource | cut -f1)"
|
[[ -d $d ]] && continue
|
||||||
|
|
||||||
|
echo "Trying to download $RES_DIR/$chooseUser/$d with adb pull (slow) ..."
|
||||||
|
mkdir -p $d
|
||||||
|
(
|
||||||
|
cd $d || exit
|
||||||
|
adb root
|
||||||
|
sleep 1 # sometimes adb complains: device not found
|
||||||
|
adb pull "$RES_DIR/$chooseUser/$d"
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ -d $d ]] || {
|
||||||
|
echo "Failed to download $RES_DIR/$chooseUser/$d"
|
||||||
|
}
|
||||||
|
done
|
||||||
|
echo "Resource pulled at ./resource"
|
||||||
|
echo "Total size: $(du -sh | cut -f1)"
|
||||||
|
)
|
||||||
else
|
else
|
||||||
echo "Pulling database and avatar index file..."
|
for f in EnMicroMsg.db sfs/avatar.index; do
|
||||||
adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db
|
echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..."
|
||||||
[[ -f EnMicroMsg.db ]] && \
|
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null |
|
||||||
echo "Database successfully downloaded to EnMicroMsg.db" || {
|
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
||||||
>&2 echo "Failed to pull database by adb"
|
[[ -f $f ]] && continue
|
||||||
exit 1
|
|
||||||
}
|
echo "Trying to download $RES_DIR/$chooseUser/$f with tar & base64 ..."
|
||||||
adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index
|
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | base64" |
|
||||||
[[ -f avatar.index ]] && \
|
base64 -di | tar xzf - --strip-components 5
|
||||||
echo "Avatar index successfully downloaded to avatar.index" || {
|
[[ -f $f ]] && continue
|
||||||
>&2 echo "Failed to pull avatar index by adb, are you using latest version of wechat?"
|
|
||||||
exit 1
|
echo "Trying to download $RES_DIR/$chooseUser/$f with adb pull..."
|
||||||
|
adb root
|
||||||
|
sleep 1
|
||||||
|
adb pull $RES_DIR/$chooseUser/$f
|
||||||
|
|
||||||
|
[[ -f $f ]] || {
|
||||||
|
echo "Failed to download $RES_DIR/$chooseUser/$f"
|
||||||
}
|
}
|
||||||
|
done
|
||||||
|
echo "Database and avatar index file successfully downloaded"
|
||||||
fi
|
fi
|
||||||
elif [[ $1 == "db-decrypt" ]]; then
|
|
||||||
echo "Getting uin..."
|
|
||||||
$0 uin | tail -n1 | $GREP -o '\-?[0-9]*' | tee /tmp/uin
|
|
||||||
echo "Getting imei..."
|
|
||||||
$0 imei | tail -n1 | $GREP -o '[0-9]*' | tee /tmp/imei
|
|
||||||
echo "Getting db..."
|
|
||||||
$0 db
|
|
||||||
echo "Decrypting db..."
|
|
||||||
imei=$(cat /tmp/imei)
|
|
||||||
uin=$(cat /tmp/uin)
|
|
||||||
if [[ -z $imei || -z $uin ]]; then
|
|
||||||
>&2 echo "Failed to get imei or uin. See README for manual methods."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
./decrypt-db.py EnMicroMsg.db $imei $uin
|
|
||||||
rm /tmp/{uin,imei}
|
|
||||||
echo "Done. See decrypted.db"
|
|
||||||
else
|
else
|
||||||
echo "Usage: $0 <res|db-decrypt>"
|
echo "Usage: $0 <res|db>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env python2
|
|
||||||
# -*- coding: UTF-8 -*-
|
|
||||||
# File: utils.py
|
|
||||||
# Date: Wed Jun 17 23:59:25 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
|
|
||||||
def ensure_bin_str(s):
|
|
||||||
if type(s) == str:
|
|
||||||
return s
|
|
||||||
if type(s) == unicode:
|
|
||||||
return s.encode('utf-8')
|
|
||||||
|
|
||||||
def ensure_unicode(s):
|
|
||||||
if type(s) == str:
|
|
||||||
return s.decode('utf-8')
|
|
||||||
if type(s) == unicode:
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def md5(s):
|
|
||||||
m = hashlib.md5()
|
|
||||||
m.update(s)
|
|
||||||
return m.hexdigest()
|
|
||||||
|
|
||||||
def get_file_b64(fname):
|
|
||||||
data = open(fname, 'rb').read()
|
|
||||||
return base64.b64encode(data)
|
|
||||||
|
|
||||||
def safe_filename(fname):
|
|
||||||
filename = ensure_unicode(fname)
|
|
||||||
return "".join(
|
|
||||||
[c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()
|
|
||||||
@@ -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,6 +1,6 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
# File: count-message.sh
|
# File: count-message.sh
|
||||||
# Date: Sun Apr 12 21:01:01 2015 +0900
|
# Date: Wed Nov 29 02:32:40 2017 -0800
|
||||||
# Author: Kangjing Huang <[email protected]>
|
# Author: Kangjing Huang <[email protected]>
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ then
|
|||||||
fi
|
fi
|
||||||
# TODO work on db directly
|
# TODO work on db directly
|
||||||
|
|
||||||
echo -e "Filename\tCounts of message\tCounts of chars\tCounts of words"
|
echo -e "Filename\t#Lines\t#Chars\t#Words"
|
||||||
|
|
||||||
SAVEIFS=$IFS
|
SAVEIFS=$IFS
|
||||||
IFS=$(echo -en "\n\b")
|
IFS=$(echo -en "\n\b")
|
||||||
@@ -23,6 +23,6 @@ do
|
|||||||
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
|
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
|
||||||
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
|
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
|
||||||
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
|
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
|
||||||
done
|
done | sort -t $'\t' -k 2 -n | column -t -s $'\t'
|
||||||
|
|
||||||
IFS=$SAVEIFS
|
IFS=$SAVEIFS
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env python2
|
|
||||||
# -*- coding: UTF-8 -*-
|
|
||||||
# File: decrypt-db.py
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
|
||||||
from pysqlcipher import dbapi2 as sqlite
|
|
||||||
|
|
||||||
from hashlib import md5
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
DEFAULT_OUTPUT_DB_NAME = 'decrypted.db'
|
|
||||||
def get_args():
|
|
||||||
parser = ArgumentParser()
|
|
||||||
parser.add_argument('db', help='path to EnMicroMsg.db')
|
|
||||||
parser.add_argument('imei', help='15 digit IMEI of your phone')
|
|
||||||
parser.add_argument('uin', help='WeChat UIN')
|
|
||||||
parser.add_argument('--output', help='output decrypted database',
|
|
||||||
default=DEFAULT_OUTPUT_DB_NAME)
|
|
||||||
args = parser.parse_args()
|
|
||||||
return args
|
|
||||||
|
|
||||||
def get_key(imei, uin):
|
|
||||||
a = md5(imei + uin)
|
|
||||||
return a.hexdigest()[:7]
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
args = get_args()
|
|
||||||
|
|
||||||
output = args.output
|
|
||||||
if os.path.abspath(os.path.dirname(output)) != os.path.abspath('.'):
|
|
||||||
print "Output file must be in current directory"
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isfile(output):
|
|
||||||
print "{} already exists. Remove? (y/n)".format(args.output),
|
|
||||||
ans = raw_input()
|
|
||||||
if ans not in ['y', 'Y']:
|
|
||||||
print "Bye!"
|
|
||||||
sys.exit()
|
|
||||||
os.unlink(output)
|
|
||||||
|
|
||||||
key = get_key(args.imei, args.uin)
|
|
||||||
print "KEY: {}".format(key)
|
|
||||||
|
|
||||||
print "Decrypt and dump database to {} ... ".format(output)
|
|
||||||
conn = sqlite.connect(args.db)
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("PRAGMA key = '" + key + "';")
|
|
||||||
c.execute("PRAGMA cipher_use_hmac = OFF;")
|
|
||||||
c.execute("PRAGMA cipher_page_size = 1024;")
|
|
||||||
c.execute("PRAGMA kdf_iter = 4000;")
|
|
||||||
c.execute("ATTACH DATABASE '" + args.output + "' AS db KEY '';")
|
|
||||||
c.execute("SELECT sqlcipher_export('db');" )
|
|
||||||
c.execute("DETACH DATABASE db;" )
|
|
||||||
c.close()
|
|
||||||
@@ -1,49 +1,44 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# File: dump-audio.py
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import base64
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from common.textutil import ensure_unicode
|
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
|
from wechat.msg import TYPE_SPEAK
|
||||||
from wechat.res import Resource
|
from wechat.res import Resource
|
||||||
from wechat.render import HTMLRender
|
|
||||||
from wechat.libchathelper import LibChatHelper
|
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('name', help='name of contact')
|
parser.add_argument('name', help='name of contact')
|
||||||
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
|
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
|
||||||
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
|
parser.add_argument('--db', default='decoded.db', help='path to decoded database')
|
||||||
parser.add_argument('--res', default='resource', help='reseource directory')
|
parser.add_argument('--res', default='resource', help='reseource directory')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = get_args()
|
args = get_args()
|
||||||
|
|
||||||
name = ensure_unicode(args.name)
|
|
||||||
output_file = args.output
|
|
||||||
|
|
||||||
parser = WeChatDBParser(args.db)
|
parser = WeChatDBParser(args.db)
|
||||||
res = Resource(args.res, '')
|
res = Resource(parser, args.res)
|
||||||
|
|
||||||
if name and name in parser.msgs_by_chat:
|
try:
|
||||||
msgs = parser.msgs_by_chat[name]
|
chatid = parser.get_chat_id(args.name)
|
||||||
else:
|
except KeyError:
|
||||||
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
sys.stderr.write(u"Valid Contacts: {}\n".format('\n'.join(parser.all_chat_nicknames)))
|
||||||
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print "Number of Messages: ", len(msgs)
|
|
||||||
|
msgs = parser.msgs_by_chat[chatid]
|
||||||
|
print(f"Number of Messages for {args.name}: ", len(msgs))
|
||||||
assert len(msgs) > 0
|
assert len(msgs) > 0
|
||||||
|
|
||||||
libchat = LibChatHelper(parser, res)
|
voice_msgs = [m for m in msgs if m.type == TYPE_SPEAK]
|
||||||
msgs = libchat.convert_msgs(msgs)
|
for idx, m in enumerate(voice_msgs):
|
||||||
voices = [m.sound for m in msgs if m.sound]
|
audio_str, duration = res.get_voice_mp3(m.imgPath)
|
||||||
for idx, v in enumerate(voices):
|
audio_bytes = base64.b64decode(audio_str)
|
||||||
p = v.find(':')
|
outf = f'/{args.output}/{idx:04d}-{duration:.1f}s.mp3'
|
||||||
v = v[p:]
|
with open(outf, 'wb') as f:
|
||||||
with open('/{}/{:04d}.mp3'.format(args.output, idx), 'wb') as f:
|
f.write(audio_bytes)
|
||||||
f.write(v)
|
print(f"Audio written to {outf}")
|
||||||
|
|||||||
@@ -1,52 +1,66 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
import os
|
||||||
# File: dump-html.py
|
|
||||||
# Date: Wed Mar 25 17:44:20 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from common.textutil import ensure_unicode
|
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from wechat.res import Resource
|
from wechat.res import Resource
|
||||||
from wechat.render import HTMLRender
|
from wechat.render import HTMLRender
|
||||||
|
|
||||||
|
logger = logging.getLogger("wechat")
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('name', help='name of contact')
|
parser.add_argument('name', help='name of contact')
|
||||||
parser.add_argument('--output', help='output html file', default='output.html')
|
parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html')
|
||||||
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
|
parser.add_argument('--db', default='EnMicroMsg.db.decoded',
|
||||||
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file')
|
help='path to the decoded database, e.g. EnMicroMsg.db.decoded')
|
||||||
parser.add_argument('--res', default='resource', help='reseource directory')
|
parser.add_argument('--res', default='resource', help='the resource directory')
|
||||||
|
parser.add_argument('--wxgf-server', help='address of the wxgf image decoder server')
|
||||||
|
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat. Ignore for new version of wechat.')
|
||||||
|
parser.add_argument('--start', help='start time in format of YYYY-MM-DD HH:MM:SS',
|
||||||
|
type=datetime.fromisoformat)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = get_args()
|
args = get_args()
|
||||||
|
|
||||||
name = ensure_unicode(args.name)
|
|
||||||
output_file = args.output
|
output_file = args.output
|
||||||
|
|
||||||
parser = WeChatDBParser(args.db)
|
parser = WeChatDBParser(args.db)
|
||||||
res = Resource(parser, args.res, args.avt)
|
|
||||||
|
|
||||||
if name and name in parser.msgs_by_chat:
|
try:
|
||||||
msgs = parser.msgs_by_chat[name]
|
chatid = parser.get_chat_id(args.name)
|
||||||
else:
|
except KeyError:
|
||||||
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
sys.stderr.write(u"Valid Contacts: {}\n".format(
|
||||||
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
u'\n'.join(parser.all_chat_nicknames)))
|
||||||
|
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print "Number of Messages: ", len(msgs)
|
|
||||||
|
res = Resource(parser, args.res,
|
||||||
|
wxgf_server=args.wxgf_server,
|
||||||
|
avt_db=args.avt)
|
||||||
|
msgs = parser.msgs_by_chat[chatid]
|
||||||
|
logger.info(f"Number of Messages for chatid {chatid}: {len(msgs)}")
|
||||||
assert len(msgs) > 0
|
assert len(msgs) > 0
|
||||||
|
if args.start is not None:
|
||||||
|
msgs = [msg for msg in msgs if msg.createTime > args.start]
|
||||||
|
logger.info(f"Number of Messages after {args.start}: {len(msgs)}")
|
||||||
|
|
||||||
render = HTMLRender(parser, res)
|
render = HTMLRender(parser, res)
|
||||||
htmls = render.render_msgs(msgs)
|
htmls = render.render_msgs(msgs)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
|
||||||
if len(htmls) == 1:
|
if len(htmls) == 1:
|
||||||
with open(output_file, 'w') as f:
|
with open(output_file, 'w') as f:
|
||||||
print >> f, htmls[0].encode('utf-8')
|
f.write(htmls[0])
|
||||||
else:
|
else:
|
||||||
|
assert output_file.endswith(".html")
|
||||||
|
basename = output_file[:-5]
|
||||||
for idx, html in enumerate(htmls):
|
for idx, html in enumerate(htmls):
|
||||||
with open(output_file + '.{}'.format(idx), 'w') as f:
|
with open(basename + f'{idx:02d}.html', 'w') as f:
|
||||||
print >> f, html.encode('utf-8')
|
f.write(html)
|
||||||
|
res.emoji_reader.flush_cache()
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: dump-msg.py
|
|
||||||
# Date: Mon May 25 15:23:05 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
|
import logging
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from common.textutil import safe_filename
|
from wechat.common.textutil import safe_filename
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
if len(sys.argv) != 3:
|
logger = logging.getLogger("wechat")
|
||||||
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
|
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
if __name__ == '__main__':
|
||||||
output_dir = sys.argv[2]
|
if len(sys.argv) != 3:
|
||||||
try:
|
sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0]))
|
||||||
os.mkdir(output_dir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if not os.path.isdir(output_dir):
|
|
||||||
sys.exit("Error creating directory {}".format(output_dir))
|
|
||||||
|
|
||||||
parser = WeChatDBParser(db_file)
|
db_file = sys.argv[1]
|
||||||
|
output_dir = sys.argv[2]
|
||||||
|
try:
|
||||||
|
os.mkdir(output_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not os.path.isdir(output_dir):
|
||||||
|
sys.exit("Error creating directory {}".format(output_dir))
|
||||||
|
|
||||||
for name, msgs in parser.msgs_by_chat.iteritems():
|
parser = WeChatDBParser(db_file)
|
||||||
print u"Writing msgs for {}".format(name)
|
|
||||||
safe_name = safe_filename(name)
|
for chatid, msgs in parser.msgs_by_chat.items():
|
||||||
with open(os.path.join(output_dir, safe_name + '.txt'), 'w') as f:
|
name = parser.contacts[chatid]
|
||||||
for m in msgs:
|
if len(name) == 0:
|
||||||
print >> f, m
|
logger.info(f"Chat {chatid} doesn't have a valid display name.")
|
||||||
|
name = str(id(chatid))
|
||||||
|
logger.info(f"Writing msgs for {name}")
|
||||||
|
safe_name = safe_filename(name)
|
||||||
|
outf = os.path.join(output_dir, safe_name + '.txt')
|
||||||
|
if os.path.isfile(outf):
|
||||||
|
logger.info(f"File {outf} exists! Skip contact {name}")
|
||||||
|
continue
|
||||||
|
with open(outf, 'w') as f:
|
||||||
|
for m in msgs:
|
||||||
|
f.write(str(m))
|
||||||
|
f.write("\n")
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import imghdr
|
||||||
|
import base64
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("""\
|
||||||
|
Usage:
|
||||||
|
{} unpack output-dir
|
||||||
|
{} pack input-dir
|
||||||
|
""".format(sys.argv[0], sys.argv[0]))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if sys.argv[1] == 'unpack':
|
||||||
|
with open('emoji.cache', 'rb') as f:
|
||||||
|
dic = pickle.load(f)
|
||||||
|
outdir = sys.argv[2]
|
||||||
|
assert os.path.isdir(outdir)
|
||||||
|
for md5, img in dic.items():
|
||||||
|
data = img[0]
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
data = data.encode('ascii')
|
||||||
|
name = os.path.join(outdir, md5 + '.' + img[1].lower())
|
||||||
|
print(name)
|
||||||
|
with open(name, 'wb') as f:
|
||||||
|
f.write(base64.decodebytes(data))
|
||||||
|
elif sys.argv[1] == 'pack':
|
||||||
|
ret = {}
|
||||||
|
indir = sys.argv[2]
|
||||||
|
files = os.listdir(indir)
|
||||||
|
for fname in files:
|
||||||
|
try:
|
||||||
|
md5, format = fname.split('.')
|
||||||
|
except:
|
||||||
|
print("Unable to parse", fname)
|
||||||
|
continue
|
||||||
|
with open(os.path.join(indir, fname), 'rb') as f:
|
||||||
|
b64 = base64.encodebytes(f.read()).decode('ascii')
|
||||||
|
ret[md5] = (b64, format)
|
||||||
|
with open('emoji.cache', 'wb') as f:
|
||||||
|
pickle.dump(ret, f)
|
||||||
@@ -1 +0,0 @@
|
|||||||
../compatibility.sh
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
# File: decrypt-db.sh
|
|
||||||
# Date: Tue Jun 16 22:23:13 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
source compatibility.sh
|
|
||||||
|
|
||||||
MSGDB=$1
|
|
||||||
imei=$2
|
|
||||||
uin=$3
|
|
||||||
output=decrypted.db
|
|
||||||
|
|
||||||
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
|
|
||||||
echo "Usage: $0 <path to EnMicroMsg.db> <imei> <uin>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f $output ]]; then
|
|
||||||
echo -n "$output already exists. removed? (y/n)"
|
|
||||||
read r
|
|
||||||
[[ $r == "y" ]] && rm -v $output || exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
KEY=$(echo -n "$imei$uin" | $MD5SUM | cut -b 1-7)
|
|
||||||
echo "KEY: $KEY"
|
|
||||||
|
|
||||||
uname | grep Darwin > /dev/null && os=darwin || os=linux
|
|
||||||
uname -m | grep x86_64 > /dev/null && version=64bit || version=32bit
|
|
||||||
echo "Use $version sqlcipher of $os."
|
|
||||||
|
|
||||||
echo "Dump decrypted database... (Don't worry about libcrypt.so version warning.)"
|
|
||||||
|
|
||||||
|
|
||||||
SQLCIPHER=./sqlcipher/$os/$version
|
|
||||||
export LD_LIBRARY_PATH=$SQLCIPHER
|
|
||||||
"$SQLCIPHER"/sqlcipher "$MSGDB" << EOF
|
|
||||||
PRAGMA key='$KEY';
|
|
||||||
PRAGMA cipher_use_hmac = off;
|
|
||||||
PRAGMA cipher_page_size = 1024;
|
|
||||||
PRAGMA kdf_iter = 4000;
|
|
||||||
ATTACH DATABASE "$output" AS db KEY "";
|
|
||||||
SELECT sqlcipher_export("db");
|
|
||||||
DETACH DATABASE db;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Database successfully dumped to $output"
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: create_table.py
|
# File: create_table.py
|
||||||
# Date: Wed Mar 25 16:43:22 2015 +0800
|
# Date: Wed Mar 25 16:43:22 2015 +0800
|
||||||
# Author: Yuxin Wu <[email protected]>
|
# Author: Yuxin Wu
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
from libchat import SqliteLibChat
|
from libchat import SqliteLibChat
|
||||||
|
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print "Usage: {} <DB file name>"
|
print("Usage: {} <DB file name>")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
db_name = sys.argv[1]
|
db_name = sys.argv[1]
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: libchat.py
|
|
||||||
# Date: Sun Apr 12 21:08:51 2015 +0900
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -99,7 +96,7 @@ class SqliteLibChat(object):
|
|||||||
else:
|
else:
|
||||||
self.c.execute("SELECT * FROM message WHERE {}".format(
|
self.c.execute("SELECT * FROM message WHERE {}".format(
|
||||||
' AND '.join(["{} = {}".format(k, v)
|
' AND '.join(["{} = {}".format(k, v)
|
||||||
for k, v in predicate.iteritems()])))
|
for k, v in predicate.items()])))
|
||||||
for row in self.c.fetchall():
|
for row in self.c.fetchall():
|
||||||
yield ChatMsg(*SqliteLibChat.postfilter(row))
|
yield ChatMsg(*SqliteLibChat.postfilter(row))
|
||||||
|
|
||||||
@@ -113,5 +110,5 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
for k in db.iterate_all_msg():
|
for k in db.iterate_all_msg():
|
||||||
from IPython import embed; embed()
|
from IPython import embed; embed()
|
||||||
print k
|
print(k)
|
||||||
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: libchathelper.py
|
|
||||||
# Date: Thu Jun 18 00:02:35 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
from pyquery import PyQuery
|
from pyquery import PyQuery
|
||||||
@@ -12,8 +8,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from libchat.libchat import SqliteLibChat, ChatMsg
|
from libchat.libchat import SqliteLibChat, ChatMsg
|
||||||
from .msg import *
|
from .msg import *
|
||||||
from common.timer import timing
|
from .common.timer import timing
|
||||||
from common.progress import ProgressReporter
|
from .common.progress import ProgressReporter
|
||||||
|
|
||||||
class LibChatHelper(object):
|
class LibChatHelper(object):
|
||||||
""" Build LibChat messages from WeChat Msg"""
|
""" Build LibChat messages from WeChat Msg"""
|
||||||
@@ -46,21 +42,27 @@ class LibChatHelper(object):
|
|||||||
return img, 'jpeg'
|
return img, 'jpeg'
|
||||||
elif msg.type == TYPE_EMOJI:
|
elif msg.type == TYPE_EMOJI:
|
||||||
md5 = msg.imgPath
|
md5 = msg.imgPath
|
||||||
emoji_img, format = self.res.get_emoji_by_md5(md5)
|
if md5:
|
||||||
return emoji_img, format
|
emoji_img, format = self.res.get_emoji_by_md5(md5)
|
||||||
|
return emoji_img, format
|
||||||
|
else:
|
||||||
|
return '', ''
|
||||||
elif msg.type == TYPE_CUSTOM_EMOJI:
|
elif msg.type == TYPE_CUSTOM_EMOJI:
|
||||||
pq = PyQuery(msg.content)
|
pq = PyQuery(msg.content)
|
||||||
md5 = pq('emoticonmd5').text()
|
md5 = pq('emoticonmd5').text()
|
||||||
img, format = self.res.get_emoji(md5, None)
|
if md5:
|
||||||
return img, format
|
img, format = self.res.get_emoji(md5, None)
|
||||||
|
return img, format
|
||||||
|
else:
|
||||||
|
return '', ''
|
||||||
else:
|
else:
|
||||||
return '', ''
|
return '', ''
|
||||||
|
|
||||||
def _get_sound(self, msg):
|
def _get_sound(self, msg):
|
||||||
if msg.type == TYPE_SPEAK:
|
if msg.type == TYPE_SPEAK:
|
||||||
audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
|
audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
|
||||||
return '{}:{}'.format(duration, base64.b64decode(audio_str))
|
return base64.b64decode(audio_str)
|
||||||
return ''
|
return b''
|
||||||
|
|
||||||
def _get_extra(self, msg):
|
def _get_extra(self, msg):
|
||||||
ret = {}
|
ret = {}
|
||||||
@@ -77,7 +79,7 @@ class LibChatHelper(object):
|
|||||||
if img:
|
if img:
|
||||||
# TODO don't use b64, directly return image content
|
# TODO don't use b64, directly return image content
|
||||||
img = base64.b64decode(img)
|
img = base64.b64decode(img)
|
||||||
# TODO do we need to save format?
|
# TODO do we need to save format or voice duration?
|
||||||
sound = self._get_sound(msg)
|
sound = self._get_sound(msg)
|
||||||
extra = self._get_extra(msg)
|
extra = self._get_extra(msg)
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
Copyright (c) 2008, ZETETIC LLC
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
* Neither the name of the ZETETIC LLC nor the
|
|
||||||
names of its contributors may be used to endorse or promote products
|
|
||||||
derived from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
|
|
||||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
|
|
||||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
## NOTE
|
|
||||||
|
|
||||||
For convenience, I include the binary version of sqlcipher 2.1.1.
|
|
||||||
|
|
||||||
User can also choose to download and install sqlcipher 2.1.1 from their trusted source, such as:
|
|
||||||
https://launchpad.net/ubuntu/+source/sqlcipher/2.1.1-2
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
#! /bin/sh
|
|
||||||
|
|
||||||
# sqlite3 - temporary wrapper script for .libs/sqlite3
|
|
||||||
# Generated by ltmain.sh (GNU libtool) 2.2.6
|
|
||||||
#
|
|
||||||
# The sqlite3 program cannot be directly executed until all the libtool
|
|
||||||
# libraries that it depends on are installed.
|
|
||||||
#
|
|
||||||
# This wrapper script should never be moved out of the build directory.
|
|
||||||
# If it is, it will not operate correctly.
|
|
||||||
|
|
||||||
# Sed substitution that helps us do robust quoting. It backslashifies
|
|
||||||
# metacharacters that are still active within double-quoted strings.
|
|
||||||
Xsed='/opt/local/bin/gsed -e 1s/^X//'
|
|
||||||
sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
|
|
||||||
|
|
||||||
# Be Bourne compatible
|
|
||||||
if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
|
|
||||||
emulate sh
|
|
||||||
NULLCMD=:
|
|
||||||
# Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
|
|
||||||
# is contrary to our usage. Disable this feature.
|
|
||||||
alias -g '${1+"$@"}'='"$@"'
|
|
||||||
setopt NO_GLOB_SUBST
|
|
||||||
else
|
|
||||||
case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
|
|
||||||
fi
|
|
||||||
BIN_SH=xpg4; export BIN_SH # for Tru64
|
|
||||||
DUALCASE=1; export DUALCASE # for MKS sh
|
|
||||||
|
|
||||||
# The HP-UX ksh and POSIX shell print the target directory to stdout
|
|
||||||
# if CDPATH is set.
|
|
||||||
(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
|
|
||||||
|
|
||||||
relink_command=""
|
|
||||||
|
|
||||||
# This environment variable determines our operation mode.
|
|
||||||
if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
|
|
||||||
# install mode needs the following variables:
|
|
||||||
generated_by_libtool_version='2.2.6'
|
|
||||||
notinst_deplibs=' libsqlite3.la'
|
|
||||||
else
|
|
||||||
# When we are sourced in execute mode, $file and $ECHO are already set.
|
|
||||||
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
|
|
||||||
ECHO="/bin/echo"
|
|
||||||
file="$0"
|
|
||||||
# Make sure echo works.
|
|
||||||
if test "X$1" = X--no-reexec; then
|
|
||||||
# Discard the --no-reexec flag, and continue.
|
|
||||||
shift
|
|
||||||
elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
|
|
||||||
# Yippee, $ECHO works!
|
|
||||||
:
|
|
||||||
else
|
|
||||||
# Restart under the correct shell, and then maybe $ECHO will work.
|
|
||||||
exec /bin/sh "$0" --no-reexec ${1+"$@"}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find the directory that this script lives in.
|
|
||||||
thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
|
|
||||||
test "x$thisdir" = "x$file" && thisdir=.
|
|
||||||
|
|
||||||
# Follow symbolic links until we get to the real thisdir.
|
|
||||||
file=`ls -ld "$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
|
|
||||||
while test -n "$file"; do
|
|
||||||
destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
|
|
||||||
|
|
||||||
# If there was a directory component, then change thisdir.
|
|
||||||
if test "x$destdir" != "x$file"; then
|
|
||||||
case "$destdir" in
|
|
||||||
[\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
|
|
||||||
*) thisdir="$thisdir/$destdir" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
|
|
||||||
file=`ls -ld "$thisdir/$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
# Usually 'no', except on cygwin/mingw when embedded into
|
|
||||||
# the cwrapper.
|
|
||||||
WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
|
|
||||||
if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
|
|
||||||
# special case for '.'
|
|
||||||
if test "$thisdir" = "."; then
|
|
||||||
thisdir=`pwd`
|
|
||||||
fi
|
|
||||||
# remove .libs from thisdir
|
|
||||||
case "$thisdir" in
|
|
||||||
*[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
|
|
||||||
.libs ) thisdir=. ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try to get the absolute directory name.
|
|
||||||
absdir=`cd "$thisdir" && pwd`
|
|
||||||
test -n "$absdir" && thisdir="$absdir"
|
|
||||||
|
|
||||||
program='sqlite3'
|
|
||||||
progdir="$thisdir/.libs"
|
|
||||||
|
|
||||||
|
|
||||||
if test -f "$progdir/$program"; then
|
|
||||||
# Add our own library path to DYLD_LIBRARY_PATH
|
|
||||||
DYLD_LIBRARY_PATH="/Users/vuryleo/Projects/sqlcipher/.libs:$DYLD_LIBRARY_PATH"
|
|
||||||
|
|
||||||
# Some systems cannot cope with colon-terminated DYLD_LIBRARY_PATH
|
|
||||||
# The second colon is a workaround for a bug in BeOS R4 sed
|
|
||||||
DYLD_LIBRARY_PATH=`$ECHO "X$DYLD_LIBRARY_PATH" | $Xsed -e 's/::*$//'`
|
|
||||||
|
|
||||||
export DYLD_LIBRARY_PATH
|
|
||||||
|
|
||||||
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
|
|
||||||
# Run the actual program with our arguments.
|
|
||||||
|
|
||||||
exec "$progdir/$program" ${1+"$@"}
|
|
||||||
|
|
||||||
$ECHO "$0: cannot exec $program $*" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# The program doesn't exist.
|
|
||||||
$ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
|
|
||||||
$ECHO "This script is just a wrapper for $program." 1>&2
|
|
||||||
/bin/echo "See the libtool documentation for more information." 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: list-chats.py
|
# File: list-chats.py
|
||||||
# Author: Yuxin Wu <[email protected]>
|
# Author: Yuxin Wu <[email protected]>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
import sys
|
import sys
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print "Usage: {} db_file".format(sys.argv[0])
|
print("Usage: {} db_file".format(sys.argv[0]))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
db_file = sys.argv[1]
|
||||||
@@ -14,4 +14,4 @@ db_file = sys.argv[1]
|
|||||||
parser = WeChatDBParser(db_file)
|
parser = WeChatDBParser(db_file)
|
||||||
chats = parser.msgs_by_chat.keys()
|
chats = parser.msgs_by_chat.keys()
|
||||||
for k in chats:
|
for k in chats:
|
||||||
print k.encode('utf-8'), '\t', parser.contacts_rev[k].encode('utf-8')
|
print(parser.contacts[k], '\t', k)
|
||||||
|
|||||||
@@ -1,49 +1,46 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: plot-num-msg-by-time.py
|
|
||||||
# Date: Wed Mar 25 17:44:39 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from common.textutil import ensure_unicode
|
|
||||||
|
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import sys, os
|
import sys
|
||||||
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
sys.exit("Usage: {0} <path to decrypted_database.db> <name>".format(sys.argv[0]))
|
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
if __name__ == '__main__':
|
||||||
name = ensure_unicode(sys.argv[2])
|
if len(sys.argv) != 3:
|
||||||
every_k_days = 2
|
sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0]))
|
||||||
|
|
||||||
parser = WeChatDBParser(db_file)
|
db_file = sys.argv[1]
|
||||||
msgs = parser.msgs_by_chat[name]
|
name = sys.argv[2]
|
||||||
times = [x.createTime for x in msgs]
|
every_k_days = 2
|
||||||
start_time = times[0]
|
|
||||||
diffs = [(x - start_time).days for x in times]
|
|
||||||
max_day = diffs[-1]
|
|
||||||
|
|
||||||
width = 20
|
parser = WeChatDBParser(db_file)
|
||||||
numbers = range((max_day / width + 1) * width + 1)[::width]
|
msgs = parser.msgs_by_chat[name]
|
||||||
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
|
times = [x.createTime for x in msgs]
|
||||||
plt.xticks(numbers, labels)
|
start_time = times[0]
|
||||||
plt.xlabel("Date")
|
diffs = [(x - start_time).days for x in times]
|
||||||
plt.ylabel("Number of msgs in k days")
|
max_day = diffs[-1]
|
||||||
plt.hist(diffs, bins=max_day / every_k_days)
|
|
||||||
plt.show()
|
width = 20
|
||||||
|
numbers = range((max_day / width + 1) * width + 1)[::width]
|
||||||
|
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
|
||||||
|
plt.xticks(numbers, labels)
|
||||||
|
plt.xlabel("Date")
|
||||||
|
plt.ylabel("Number of msgs in k days")
|
||||||
|
plt.hist(diffs, bins=max_day / every_k_days)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
# statistics by hour
|
# statistics by hour
|
||||||
# I'm in a different time zone in this period:
|
# I'm in a different time zone in this period:
|
||||||
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
|
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
|
||||||
#def real_hour(x):
|
#def real_hour(x):
|
||||||
#for k, v in TZ_DELTA.iteritems():
|
#for k, v in TZ_DELTA.items():
|
||||||
#if x > k[0] and x < k[1]:
|
#if x > k[0] and x < k[1]:
|
||||||
#print x
|
#print x
|
||||||
#return (x.hour + v + 24) % 24
|
#return (x.hour + v + 24) % 24
|
||||||
#return x.hour
|
#return x.hour
|
||||||
#hours = [real_hour(x) for x in times]
|
#hours = [real_hour(x) for x in times]
|
||||||
#plt.ylabel("Number of msgs")
|
#plt.ylabel("Number of msgs")
|
||||||
#plt.xlabel("Hour in a day")
|
#plt.xlabel("Hour in a day")
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pyquery
|
||||||
|
javaobj-py3>=0.4.1
|
||||||
|
pycryptodome
|
||||||
|
pillow>=7.0
|
||||||
|
requests
|
||||||
|
pysqlcipher3>=1.0.3
|
||||||
|
csscompressor
|
||||||
|
numpy
|
||||||
|
ipython
|
||||||
|
websocket-client
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
# File: compile_silk.sh
|
|
||||||
# Date: Tue Jun 16 22:26:49 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
cd `dirname "$0"`/silk
|
cd `dirname "$0"`/silk
|
||||||
make
|
make
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ ifeq (yes,$(USE_NEON))
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
CFLAGS += -Wall -enable-threads -O3
|
CFLAGS += -Wall -O3
|
||||||
|
|
||||||
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
||||||
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
|
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -21,10 +21,10 @@ def set_level_color(lvl, color):
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
set_level_color(logging.INFO, '\033[1;32m')
|
set_level_color(logging.INFO, '\033[1;32m')
|
||||||
set_level_color(logging.WARN, '\033[1;31m')
|
set_level_color(logging.WARN, '\033[1;33m')
|
||||||
set_level_color(logging.ERROR, '\033[1;31m')
|
set_level_color(logging.ERROR, '\033[1;31m')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logger.info("info")
|
logger.info("info")
|
||||||
logger.warn("warn")
|
logger.warning("warn")
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +1,71 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: audio.py
|
|
||||||
# Date: Fri Jun 26 10:42:41 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from subprocess import PIPE, Popen, call
|
import tempfile
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import pysox
|
from .common.textutil import get_file_b64
|
||||||
|
from .common.procutil import subproc_succ
|
||||||
from common.textutil import get_file_b64
|
|
||||||
|
|
||||||
SILK_DECODER = os.path.join(os.path.dirname(__file__),
|
SILK_DECODER = os.path.join(os.path.dirname(__file__),
|
||||||
'../third-party/silk/decoder')
|
'../third-party/silk/decoder')
|
||||||
if not os.path.exists(SILK_DECODER):
|
|
||||||
logger.error("Silk decoder is not compiled. Please see README.md.")
|
|
||||||
raise RuntimeError()
|
|
||||||
|
|
||||||
def parse_wechat_audio_file(file_name):
|
def parse_wechat_audio_file(file_name):
|
||||||
try:
|
try:
|
||||||
return do_parse_wechat_audio_file(file_name)
|
return do_parse_wechat_audio_file(file_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Pase audio file {} error!".format(file_name))
|
logger.error(f"Error when parsing audio file {file_name}: {str(e)}")
|
||||||
logger.error(e)
|
|
||||||
return "", 0
|
return "", 0
|
||||||
|
|
||||||
def do_parse_wechat_audio_file(file_name):
|
def do_parse_wechat_audio_file(file_name):
|
||||||
""" return a mp3 base64 string, and the duration"""
|
""" return a mp3 stored in base64 unicode string, and the duration"""
|
||||||
if not file_name: return "", 0
|
if not file_name: return "", 0
|
||||||
|
|
||||||
mp3_file = os.path.join('/tmp',
|
with tempfile.TemporaryDirectory(prefix="wechatdump_audio") as temp:
|
||||||
os.path.basename(file_name)[:-4] + '.mp3')
|
mp3_file = os.path.join(temp,
|
||||||
with open(file_name) as f:
|
os.path.basename(file_name)[:-4] + '.mp3')
|
||||||
header = f.read(10)
|
with open(file_name, 'rb') as f:
|
||||||
if 'AMR' in header:
|
header = f.read(10)
|
||||||
# maybe this is faster than calling sox from command line?
|
if b'AMR' in header:
|
||||||
infile = pysox.CSoxStream(file_name)
|
cmd = f"sox -e signed -c 1 {file_name} {mp3_file}"
|
||||||
outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
subproc_succ(cmd)
|
||||||
chain = pysox.CEffectsChain(infile, outfile)
|
cmd = f"soxi -D {mp3_file}"
|
||||||
chain.flow_effects()
|
duration = float(subproc_succ(cmd))
|
||||||
outfile.close()
|
|
||||||
|
|
||||||
signal = infile.get_signal().get_signalinfo()
|
# The below is python2 only. It should be equivalent to using sox from command line
|
||||||
duration = signal['length'] * 1.0 / signal['rate']
|
# import pysox
|
||||||
elif 'SILK' in header:
|
# infile = pysox.CSoxStream(file_name)
|
||||||
raw_file = os.path.join('/tmp',
|
# outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
||||||
os.path.basename(file_name)[:-4] + '.raw')
|
# chain = pysox.CEffectsChain(infile, outfile)
|
||||||
proc = Popen('{0} {1} {2}'.format(SILK_DECODER,
|
# chain.flow_effects()
|
||||||
file_name, raw_file),
|
# outfile.close()
|
||||||
shell=True, stdout=PIPE, stderr=PIPE)
|
# signal = infile.get_signal().get_signalinfo()
|
||||||
stdout = proc.communicate()[0]
|
# duration = signal['length'] * 1.0 / signal['rate']
|
||||||
for line in stdout.split('\n'):
|
elif b'SILK' in header:
|
||||||
if 'File length' in line:
|
if not os.path.exists(SILK_DECODER):
|
||||||
duration = float(line[13:-3].strip())
|
raise RuntimeError("Silk decoder is not compiled. Please see README.md.")
|
||||||
break
|
|
||||||
|
raw_file = os.path.join(temp,
|
||||||
|
os.path.basename(file_name)[:-4] + '.raw')
|
||||||
|
cmd = '{0} {1} {2}'.format(SILK_DECODER, file_name, raw_file)
|
||||||
|
out = subproc_succ(cmd)
|
||||||
|
for line in out.split(b'\n'):
|
||||||
|
if b'File length' in line:
|
||||||
|
duration = float(line[13:-3].strip())
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Error decoding silk audio file!" + out.decode('utf-8'))
|
||||||
|
|
||||||
|
# TODO don't know how to do this with python
|
||||||
|
subproc_succ('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(raw_file, mp3_file))
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Error decoding silk audio file!")
|
raise NotImplementedError("Audio file format cannot be recognized.")
|
||||||
|
|
||||||
# I don't know how to do this with pysox
|
|
||||||
proc = call('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(
|
|
||||||
raw_file, mp3_file), shell=True)
|
|
||||||
os.unlink(raw_file)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError("Unsupported Audio Format! This is a bug!")
|
|
||||||
try:
|
|
||||||
mp3_string = get_file_b64(mp3_file)
|
mp3_string = get_file_b64(mp3_file)
|
||||||
os.unlink(mp3_file)
|
|
||||||
except:
|
|
||||||
raise RuntimeError("Failed to decode audio file: {}".format(file_name))
|
|
||||||
return mp3_string, duration
|
return mp3_string, duration
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
fname = sys.argv[1]
|
fname = sys.argv[1]
|
||||||
print parse_wechat_audio_file(fname)[1]
|
print(parse_wechat_audio_file(fname)[1])
|
||||||
|
|||||||
@@ -1,71 +1,137 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: avatar.py
|
|
||||||
# Date: Thu Jun 18 00:02:07 2015 +0800
|
|
||||||
# Author: Yuxin Wu <[email protected]>
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cStringIO
|
import io
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from common.textutil import ensure_bin_str, md5
|
from .common.textutil import md5
|
||||||
|
|
||||||
|
|
||||||
|
def _filename_priority(s):
|
||||||
|
if "_hd" in s and s.endswith(".png"):
|
||||||
|
return 10
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
class AvatarReader(object):
|
class AvatarReader(object):
|
||||||
def __init__(self, avt_dir, avt_db="avatar.index"):
|
def __init__(self, res_dir, avt_db="avatar.index"):
|
||||||
self.avt_dir = avt_dir
|
self.sfs_dir = os.path.join(res_dir, 'sfs')
|
||||||
|
|
||||||
|
# new location of avatar, see #50
|
||||||
|
self.avt_dir = os.path.join(res_dir, 'avatar')
|
||||||
|
if not os.path.isdir(self.avt_dir) or len(os.listdir(self.avt_dir)) == 0:
|
||||||
|
self.avt_dir = None
|
||||||
|
|
||||||
self.avt_db = avt_db
|
self.avt_db = avt_db
|
||||||
if self.avt_db is None or not os.path.isfile(self.avt_db):
|
self._use_avt = True
|
||||||
logger.warn(
|
if self.avt_db is not None:
|
||||||
"Avatar database {} not found. Will not use avatar!".format(avt_db))
|
if len(glob.glob(os.path.join(self.sfs_dir, 'avatar*'))) == 0:
|
||||||
self.avt_db = None
|
# has sfs/avatar*
|
||||||
|
self.avt_db = None
|
||||||
|
if self.avt_dir is None and self.avt_db is None:
|
||||||
|
logger.warn("Cannot find avatar storage. Will not use avatar!")
|
||||||
|
self._use_avt = False
|
||||||
|
|
||||||
|
def get_avatar_from_avtdb(self, avtid):
|
||||||
|
try:
|
||||||
|
candidates = self._search_avt_db(avtid)
|
||||||
|
candidates = sorted(candidates, key=lambda x: _filename_priority(x[0]), reverse=True)
|
||||||
|
for c in candidates:
|
||||||
|
path, offset, size = c
|
||||||
|
return self.read_img_from_block(path, offset, size)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_avatar_from_avtdir(self, avtid) -> Image.Image | None:
|
||||||
|
dir1, dir2 = avtid[:2], avtid[2:4]
|
||||||
|
candidates = glob.glob(os.path.join(self.avt_dir, dir1, dir2, f"*{avtid}*"))
|
||||||
|
candidates = sorted(set(candidates), key=_filename_priority, reverse=True)
|
||||||
|
for cand in candidates:
|
||||||
|
if os.path.isdir(cand):
|
||||||
|
candidates.extend(os.path.join(cand, x) for x in os.listdir(cand))
|
||||||
|
for cand in candidates:
|
||||||
|
if os.path.isdir(cand):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if cand.endswith(".bm"):
|
||||||
|
return self.read_bm_file(cand)
|
||||||
|
else:
|
||||||
|
return Image.open(cand)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("")
|
||||||
|
pass
|
||||||
|
|
||||||
def get_avatar(self, username):
|
def get_avatar(self, username):
|
||||||
""" username: `username` field in db.rcontact"""
|
""" username: `username` field in db.rcontact"""
|
||||||
if self.avt_db is None: return None
|
if not self._use_avt:
|
||||||
|
|
||||||
username = ensure_bin_str(username)
|
|
||||||
filename = md5(username)
|
|
||||||
dir1, dir2 = filename[:2], filename[2:4]
|
|
||||||
filename = os.path.join(dir1, dir2,
|
|
||||||
"user_{}.png".format(filename))
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
pos, size = self.query_index(filename)
|
|
||||||
return self.read_img(pos, size)
|
|
||||||
except TypeError:
|
|
||||||
logger.warn("Avatar for {} not found in avatar database.".format(username))
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
raise
|
|
||||||
print e
|
|
||||||
logger.warn("Failed to retrieve avatar!")
|
|
||||||
return None
|
return None
|
||||||
|
avtid = md5(username.encode('utf-8'))
|
||||||
|
|
||||||
|
if self.avt_db is not None:
|
||||||
|
ret = self.get_avatar_from_avtdb(avtid)
|
||||||
|
if ret is not None:
|
||||||
|
return ret
|
||||||
|
|
||||||
def read_img(self, pos, size):
|
if self.avt_dir is not None:
|
||||||
|
ret = self.get_avatar_from_avtdir(avtid)
|
||||||
|
if ret is not None:
|
||||||
|
return ret
|
||||||
|
logger.warning("Avatar file for {} not found.".format(username))
|
||||||
|
|
||||||
|
def save_avatar_to_avtdir(self, username: str, im: Image.Image):
|
||||||
|
"""Save a downloaded avatar to avtdir so it can be reused next time."""
|
||||||
|
avtid = md5(username.encode('utf-8'))
|
||||||
|
dir1, dir2 = avtid[:2], avtid[2:4]
|
||||||
|
fname = os.path.join(self.avt_dir, dir1, dir2, f"user_{avtid}.png")
|
||||||
|
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
||||||
|
logger.info(f"Caching downloaded avatar for {username} to {fname}.")
|
||||||
|
im.save(fname, 'PNG')
|
||||||
|
|
||||||
|
def read_img_from_block(self, filename, pos, size):
|
||||||
file_idx = pos >> 32
|
file_idx = pos >> 32
|
||||||
fname = os.path.join(self.avt_dir,
|
fname = os.path.join(self.sfs_dir,
|
||||||
'avatar.block.' + '{:05d}'.format(file_idx))
|
'avatar.block.' + '{:05d}'.format(file_idx))
|
||||||
# a 64-byte offset of each block file
|
# offset of each block file: 17 + len(path)
|
||||||
start_pos = pos - file_idx * (2**32) + 64
|
start_pos = pos - file_idx * (2**32) + 16 + len(filename) + 1
|
||||||
try:
|
try:
|
||||||
with open(fname, 'rb') as f:
|
with open(fname, 'rb') as f:
|
||||||
f.seek(start_pos)
|
f.seek(start_pos)
|
||||||
data = f.read(size)
|
data = f.read(size)
|
||||||
im = Image.open(cStringIO.StringIO(data))
|
im = Image.open(io.BytesIO(data))
|
||||||
return im
|
return im
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
|
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def query_index(self, filename):
|
def read_bm_file(self, fname):
|
||||||
|
# history at https://github.com/ppwwyyxx/wechat-dump/pull/14
|
||||||
|
with open(fname, 'rb') as f:
|
||||||
|
# filesize is 36880=96x96x4+16
|
||||||
|
size = (96, 96, 3)
|
||||||
|
img = np.zeros(size, dtype='uint8')
|
||||||
|
for i in range(96):
|
||||||
|
for j in range(96):
|
||||||
|
r, g, b, a = f.read(4)
|
||||||
|
img[i,j] = (r, g, b)
|
||||||
|
return Image.fromarray(img, mode="RGB")
|
||||||
|
|
||||||
|
def _search_avt_db(self, avtid):
|
||||||
conn = sqlite3.connect(self.avt_db)
|
conn = sqlite3.connect(self.avt_db)
|
||||||
cursor = conn.execute("select Offset,Size from Index_avatar where FileName='{}'".format(filename))
|
cursor = conn.execute("select FileName,Offset,Size from Index_avatar")
|
||||||
pos, size = cursor.fetchone()
|
candidates = []
|
||||||
return pos, size
|
for path, offset, size in cursor:
|
||||||
|
if avtid in path:
|
||||||
|
candidates.append((path, offset, size))
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
r = AvatarReader(sys.argv[1], sys.argv[2])
|
||||||
|
print(r.get_avatar(sys.argv[3]))
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
try:
|
||||||
|
import imghdr as _imghdr
|
||||||
|
except ModuleNotFoundError: # Python 3.13+
|
||||||
|
_imghdr = None
|
||||||
|
|
||||||
|
|
||||||
|
def what(file: str | Path | None = None, h: bytes | None = None) -> str | None:
|
||||||
|
"""
|
||||||
|
Compatibility wrapper for the removed stdlib `imghdr` module (Python 3.13+).
|
||||||
|
|
||||||
|
Behaves like `imghdr.what(file, h)` and returns a lowercase format string
|
||||||
|
like "jpeg", "png", "gif", or None if unknown.
|
||||||
|
"""
|
||||||
|
if _imghdr is not None:
|
||||||
|
return _imghdr.what(file, h)
|
||||||
|
if h is not None:
|
||||||
|
return _what_from_bytes(h)
|
||||||
|
if file is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with Image.open(str(file)) as im:
|
||||||
|
fmt = im.format
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return fmt.lower() if fmt else None
|
||||||
|
|
||||||
|
|
||||||
|
def _what_from_bytes(data: bytes) -> str | None:
|
||||||
|
if data.startswith(b"\xff\xd8\xff"):
|
||||||
|
return "jpeg"
|
||||||
|
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||||
|
return "png"
|
||||||
|
if data[:6] in (b"GIF87a", b"GIF89a"):
|
||||||
|
return "gif"
|
||||||
|
if data.startswith(b"BM"):
|
||||||
|
return "bmp"
|
||||||
|
if len(data) >= 12 and data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
||||||
|
return "webp"
|
||||||
|
if data.startswith(b"II*\x00") or data.startswith(b"MM\x00*"):
|
||||||
|
return "tiff"
|
||||||
|
try:
|
||||||
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
fmt = im.format
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return fmt.lower() if fmt else None
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def subproc_call(cmd, timeout=None):
|
||||||
|
"""
|
||||||
|
Execute a command with timeout, and return STDOUT and STDERR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd(str): the command to execute.
|
||||||
|
timeout(float): timeout in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
output(bytes), retcode(int). If timeout, retcode is -1.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
cmd, stderr=subprocess.STDOUT,
|
||||||
|
shell=True, timeout=timeout)
|
||||||
|
return output, 0
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
logger.warn("Command '{}' timeout!".format(cmd))
|
||||||
|
if e.output:
|
||||||
|
logger.warn(e.output.decode('utf-8'))
|
||||||
|
return e.output, -1
|
||||||
|
else:
|
||||||
|
return "", -1
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.warn("Command '{}' failed, return code={}".format(cmd, e.returncode))
|
||||||
|
logger.warn(e.output.decode('utf-8'))
|
||||||
|
return e.output, e.returncode
|
||||||
|
except Exception:
|
||||||
|
logger.warn("Command '{}' failed to run.".format(cmd))
|
||||||
|
return "", -2
|
||||||
|
|
||||||
|
|
||||||
|
def subproc_succ(cmd):
|
||||||
|
"""
|
||||||
|
Like subproc_call, but expect the cmd to succeed.
|
||||||
|
"""
|
||||||
|
output, ret = subproc_call(cmd)
|
||||||
|
assert ret == 0
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||