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/
|
||||
resource
|
||||
*.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.
|
||||
This tool can parse and export WeChat messages on a rooted android phone.
|
||||
WeChat, as the most popular mobile IM app in China, doesn't provide any methods to read its message history.
|
||||
Customers are not able to analyze their own chat messages or interact with other data analysis tools.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
## How to use:
|
||||
|
||||
#### Dependencies:
|
||||
+ python-PIL
|
||||
+ [PyQuery](https://pypi.python.org/pypi/pyquery/1.2.1)
|
||||
+ [pysox](https://pypi.python.org/pypi/pysox/0.3.6.alpha)
|
||||
+ [pysqlcipher](https://pypi.python.org/pypi/pysqlcipher)
|
||||
+ numpy
|
||||
+ csscompressor (suggested, optional)
|
||||
+ adb and rooted android phone connected to a Linux/Mac OS.
|
||||
+ Silk audio decoder (included; just run `./third-party/compile_silk.sh`)
|
||||
+ gnu-sed
|
||||
+ adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash.
|
||||
+ Python >= 3.8
|
||||
+ sox (command line tools)
|
||||
+ Silk audio decoder (included; build it with `./third-party/compile_silk.sh`)
|
||||
+ Other python dependencies: `pip install -r requirements.txt`.
|
||||
|
||||
#### Get Necessary Data:
|
||||
|
||||
Note that commands involving `./android-interact.sh` are meant to be run on the computer.
|
||||
|
||||
+ (Requires Linux or Mac) Get the decrypted WeChat database and the avatar index:
|
||||
+ Automatic: `./android-interact.sh db-decrypt`
|
||||
+ 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
|
||||
+ Manual:
|
||||
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device. It should be a 32-character-long name consisting of hexadecimal digits.
|
||||
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/{EnMicroMsg.db,sfs/avatar.index}` from the device, possible ways are:
|
||||
+ `./android-interact.sh db`
|
||||
+ Use your rooted file system manager app
|
||||
+ Get WeChat uin (an integer), possible ways are:
|
||||
+ `./android-interact.sh uin`, which pulls the value from `/data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml`
|
||||
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
|
||||
+ Get your phone IMEI number (a positive integer), possible ways are:
|
||||
+ `./android-interact.sh imei`
|
||||
+ Call `*#06#` on your phone
|
||||
+ Find IMEI in system settings
|
||||
+ Decrypt database, will produce `decrypted.db`:
|
||||
|
||||
```
|
||||
./decrypt-db.py <path to EnMicroMsg.db> <imei> <uin>
|
||||
```
|
||||
|
||||
NOTE: you may need to try different ways to getting imei & uin,
|
||||
because things behave differently on different phones.
|
||||
|
||||
Also, if the decryption doesn't work with pysqlcipher, maybe try the version of sqlcipher in `legacy`.
|
||||
|
||||
|
||||
+ Copy the WeChat user resource directory `/mnt/sdcard/tencent/MicroMsg/${userid}/{emoji,image2,sfs,video,voice2}` from the phone's SD card to the `resource` directory:
|
||||
1. Pull database file and (for older WeChat versions) avatar index:
|
||||
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
|
||||
+ Manual:
|
||||
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device.
|
||||
It should be a 32-character-long name consisting of hexadecimal digits.
|
||||
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device.
|
||||
2. Decode `EnMicroMsg.db`. We do not provide instructions to do that.
|
||||
3. Copy the unencrypted WeChat user resource directory `/data/data/com.tencent.mm/MicroMsg/${userid}/{avatar,emoji,image2,sfs,video,voice2}` from the phone to the `resource` directory:
|
||||
+ `./android-interact.sh res`
|
||||
+ You might need to tweak `RES_DIR` in the script if the default doesn't work
|
||||
+ This can take a __long__ time. Some ways to do this faster:
|
||||
+ 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.
|
||||
+ Alternatively, you can use pipes. This is slower, but doesn't require any free space on the SD card:
|
||||
+ Change `RES_DIR` in the script if the location of these directories is different on your phone.
|
||||
For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
|
||||
+ This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive,
|
||||
`busybox tar` is recommended as the Android system's `tar` may choke on long paths.
|
||||
+ In the end, we need a `resource` directory with the following subdir: `avatar,emoji,image2,sfs,video,voice2`.
|
||||
|
||||
```sh
|
||||
# copy MicroMsg to the current directory
|
||||
adb shell 'cd /mnt/sdcard/tencent &&
|
||||
busybox tar czf - MicroMsg 2>/dev/null | busybox base64' |
|
||||
base64 -di | tar xzf -
|
||||
```
|
||||
4. (Optional) Install and start a WXGF decoder server on an android device. Without this, certain WXGF images will not be rendered or will be rendered in low resolution.
|
||||
See [WXGFDecoder](WXGFDecoder) for instructions.
|
||||
|
||||
4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2)
|
||||
and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering.
|
||||
|
||||
wget -c https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2
|
||||
tar xf emoji.cache.tar.bz2
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
+ 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:
|
||||
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
||||
|
||||
Screenshots of generated html:
|
||||
|
||||

|
||||
|
||||
### TODO List
|
||||
+ Search by uid/username
|
||||
+ Faster way to copy a directory from android (I don't know..).
|
||||
+ Fix rare unhandled types: > 10000 and < 0
|
||||
+ Better user experiences... see `grep 'TODO' wechat -R`
|
||||
+ more easy-to-use for non-programmers (GUI?)
|
||||
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
||||
|
||||
### TODO List (help needed!)
|
||||
* After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 -
|
||||
they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis.
|
||||
* Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu).
|
||||
* Fix rare unhandled message types: > 10000 and < 0
|
||||
* Better user experiences... see `grep 'TODO' wechat -R`
|
||||
|
||||
### 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">
|
||||
<img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" />
|
||||
</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
|
||||
# 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_DIR=`dirname "$PROG_NAME"`
|
||||
cd "$PROG_DIR"
|
||||
|
||||
source compatibility.sh
|
||||
|
||||
# Please check that your path is the same, since this might be different among devices
|
||||
RES_DIR="/mnt/sdcard/tencent/MicroMsg"
|
||||
MM_DIR="/data/data/com.tencent.mm"
|
||||
# RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
|
||||
RES_DIR="/data/data/com.tencent.mm/MicroMsg"
|
||||
|
||||
echo "Starting rooted adb server..."
|
||||
adb root
|
||||
|
||||
if [[ $1 == "uin" ]]; then
|
||||
adb pull $MM_DIR/shared_prefs/system_config_prefs.xml 2>/dev/null
|
||||
uin=$($GREP 'default_uin' system_config_prefs.xml | $GREP -o 'value="\-?[0-9]*' | cut -c 8-)
|
||||
[[ -n $uin ]] || {
|
||||
>&2 echo "Failed to get wechat uin. You can try other methods, or report a bug."
|
||||
exit 1
|
||||
}
|
||||
rm system_config_prefs.xml
|
||||
echo "Got wechat uin: $uin"
|
||||
elif [[ $1 == "imei" ]]; then
|
||||
imei=$(adb shell dumpsys iphonesubinfo | $GREP 'Device ID' | $GREP -o '[0-9]+')
|
||||
[[ -n $imei ]] || {
|
||||
imei=$(adb shell service call iphonesubinfo 1 | awk -F "'" '{print $2}' | sed 's/[^0-9A-F]*//g' | tr -d '\n')
|
||||
}
|
||||
[[ -n $imei ]] || {
|
||||
>&2 echo "Failed to get imei. You can try other methods, or report a bug."
|
||||
exit 1
|
||||
}
|
||||
echo "Got imei: $imei"
|
||||
elif [[ $1 == "db" || $1 == "res" ]]; then
|
||||
if [[ $1 == "db" || $1 == "res" ]]; then
|
||||
echo "Looking for user dir name..."
|
||||
sleep 1 # sometimes adb complains: device not found
|
||||
userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' \
|
||||
| awk '{if (length() == 32) print}')
|
||||
numUser=$(echo $userList | wc -l)
|
||||
# look for dirname which looks like md5 (32 alpha-numeric chars)
|
||||
userList=$(adb shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}')
|
||||
numUser=$(echo "$userList" | wc -l)
|
||||
# choose the first user.
|
||||
chooseUser=$(echo $userList | head -n1)
|
||||
chooseUser=$(echo "$userList" | head -n1)
|
||||
[[ -n $chooseUser ]] || {
|
||||
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
|
||||
exit 1
|
||||
@@ -50,54 +23,61 @@ elif [[ $1 == "db" || $1 == "res" ]]; then
|
||||
echo "Found $numUser user(s). User chosen: $chooseUser"
|
||||
|
||||
if [[ $1 == "res" ]]; then
|
||||
echo "Pulling resources... this might take a long time, because adb sucks..."
|
||||
mkdir -p resource; cd resource
|
||||
for d in image2 voice2 emoji video sfs; do
|
||||
mkdir -p $d; cd $d
|
||||
adb pull $RES_DIR/$chooseUser/$d
|
||||
cd ..
|
||||
[[ -d $d ]] || {
|
||||
>&2 echo "Failed to download resource directory: $RES_DIR/$chooseUser/$d"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
cd ..
|
||||
echo "Resource pulled at ./resource"
|
||||
echo "Total size: $(du -sh resource | cut -f1)"
|
||||
mkdir -p resource
|
||||
(
|
||||
cd resource || exit
|
||||
echo "Pulling resources... "
|
||||
for d in avatar image2 voice2 emoji video sfs; do
|
||||
echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..."
|
||||
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null |
|
||||
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
||||
[[ -d $d ]] && continue
|
||||
|
||||
echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..."
|
||||
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" |
|
||||
base64 -di | tar xzf - --strip-components 5
|
||||
[[ -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
|
||||
echo "Pulling database and avatar index file..."
|
||||
adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db
|
||||
[[ -f EnMicroMsg.db ]] && \
|
||||
echo "Database successfully downloaded to EnMicroMsg.db" || {
|
||||
>&2 echo "Failed to pull database by adb"
|
||||
exit 1
|
||||
}
|
||||
adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index
|
||||
[[ -f avatar.index ]] && \
|
||||
echo "Avatar index successfully downloaded to avatar.index" || {
|
||||
>&2 echo "Failed to pull avatar index by adb, are you using latest version of wechat?"
|
||||
exit 1
|
||||
for f in EnMicroMsg.db sfs/avatar.index; do
|
||||
echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..."
|
||||
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null |
|
||||
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
||||
[[ -f $f ]] && continue
|
||||
|
||||
echo "Trying to download $RES_DIR/$chooseUser/$f with tar & base64 ..."
|
||||
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | base64" |
|
||||
base64 -di | tar xzf - --strip-components 5
|
||||
[[ -f $f ]] && continue
|
||||
|
||||
echo "Trying to download $RES_DIR/$chooseUser/$f with adb pull..."
|
||||
adb root
|
||||
sleep 1
|
||||
adb pull $RES_DIR/$chooseUser/$f
|
||||
|
||||
[[ -f $f ]] || {
|
||||
echo "Failed to download $RES_DIR/$chooseUser/$f"
|
||||
}
|
||||
done
|
||||
echo "Database and avatar index file successfully downloaded"
|
||||
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
|
||||
echo "Usage: $0 <res|db-decrypt>"
|
||||
echo "Usage: $0 <res|db>"
|
||||
exit 1
|
||||
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
|
||||
# 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]>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ then
|
||||
fi
|
||||
# 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
|
||||
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)
|
||||
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"
|
||||
done
|
||||
done | sort -t $'\t' -k 2 -n | column -t -s $'\t'
|
||||
|
||||
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 -*-
|
||||
# File: dump-audio.py
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import argparse
|
||||
|
||||
from common.textutil import ensure_unicode
|
||||
from wechat.parser import WeChatDBParser
|
||||
from wechat.msg import TYPE_SPEAK
|
||||
from wechat.res import Resource
|
||||
from wechat.render import HTMLRender
|
||||
from wechat.libchathelper import LibChatHelper
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('name', help='name of contact')
|
||||
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
|
||||
parser.add_argument('--db', default='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')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = get_args()
|
||||
|
||||
name = ensure_unicode(args.name)
|
||||
output_file = args.output
|
||||
|
||||
parser = WeChatDBParser(args.db)
|
||||
res = Resource(args.res, '')
|
||||
res = Resource(parser, args.res)
|
||||
|
||||
if name and name in parser.msgs_by_chat:
|
||||
msgs = parser.msgs_by_chat[name]
|
||||
else:
|
||||
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
||||
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
||||
try:
|
||||
chatid = parser.get_chat_id(args.name)
|
||||
except KeyError:
|
||||
sys.stderr.write(u"Valid Contacts: {}\n".format('\n'.join(parser.all_chat_nicknames)))
|
||||
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
||||
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
|
||||
|
||||
libchat = LibChatHelper(parser, res)
|
||||
msgs = libchat.convert_msgs(msgs)
|
||||
voices = [m.sound for m in msgs if m.sound]
|
||||
for idx, v in enumerate(voices):
|
||||
p = v.find(':')
|
||||
v = v[p:]
|
||||
with open('/{}/{:04d}.mp3'.format(args.output, idx), 'wb') as f:
|
||||
f.write(v)
|
||||
voice_msgs = [m for m in msgs if m.type == TYPE_SPEAK]
|
||||
for idx, m in enumerate(voice_msgs):
|
||||
audio_str, duration = res.get_voice_mp3(m.imgPath)
|
||||
audio_bytes = base64.b64decode(audio_str)
|
||||
outf = f'/{args.output}/{idx:04d}-{duration:.1f}s.mp3'
|
||||
with open(outf, 'wb') as f:
|
||||
f.write(audio_bytes)
|
||||
print(f"Audio written to {outf}")
|
||||
|
||||
@@ -1,52 +1,66 @@
|
||||
#!/usr/bin/env python2
|
||||
# -*- coding: UTF-8 -*-
|
||||
# File: dump-html.py
|
||||
# Date: Wed Mar 25 17:44:20 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from common.textutil import ensure_unicode
|
||||
from wechat.parser import WeChatDBParser
|
||||
from wechat.res import Resource
|
||||
from wechat.render import HTMLRender
|
||||
|
||||
logger = logging.getLogger("wechat")
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('name', help='name of contact')
|
||||
parser.add_argument('--output', help='output html file', default='output.html')
|
||||
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
|
||||
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file')
|
||||
parser.add_argument('--res', default='resource', help='reseource directory')
|
||||
parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html')
|
||||
parser.add_argument('--db', default='EnMicroMsg.db.decoded',
|
||||
help='path to the decoded database, e.g. EnMicroMsg.db.decoded')
|
||||
parser.add_argument('--res', default='resource', help='the resource directory')
|
||||
parser.add_argument('--wxgf-server', help='address of the wxgf image decoder server')
|
||||
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat. Ignore for new version of wechat.')
|
||||
parser.add_argument('--start', help='start time in format of YYYY-MM-DD HH:MM:SS',
|
||||
type=datetime.fromisoformat)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = get_args()
|
||||
|
||||
name = ensure_unicode(args.name)
|
||||
output_file = args.output
|
||||
|
||||
parser = WeChatDBParser(args.db)
|
||||
res = Resource(parser, args.res, args.avt)
|
||||
|
||||
if name and name in parser.msgs_by_chat:
|
||||
msgs = parser.msgs_by_chat[name]
|
||||
else:
|
||||
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
||||
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
||||
try:
|
||||
chatid = parser.get_chat_id(args.name)
|
||||
except KeyError:
|
||||
sys.stderr.write(u"Valid Contacts: {}\n".format(
|
||||
u'\n'.join(parser.all_chat_nicknames)))
|
||||
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
||||
sys.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
|
||||
if args.start is not None:
|
||||
msgs = [msg for msg in msgs if msg.createTime > args.start]
|
||||
logger.info(f"Number of Messages after {args.start}: {len(msgs)}")
|
||||
|
||||
render = HTMLRender(parser, res)
|
||||
htmls = render.render_msgs(msgs)
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
|
||||
if len(htmls) == 1:
|
||||
with open(output_file, 'w') as f:
|
||||
print >> f, htmls[0].encode('utf-8')
|
||||
f.write(htmls[0])
|
||||
else:
|
||||
assert output_file.endswith(".html")
|
||||
basename = output_file[:-5]
|
||||
for idx, html in enumerate(htmls):
|
||||
with open(output_file + '.{}'.format(idx), 'w') as f:
|
||||
print >> f, html.encode('utf-8')
|
||||
with open(basename + f'{idx:02d}.html', 'w') as f:
|
||||
f.write(html)
|
||||
res.emoji_reader.flush_cache()
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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 common.textutil import safe_filename
|
||||
from wechat.common.textutil import safe_filename
|
||||
import sys, os
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
|
||||
logger = logging.getLogger("wechat")
|
||||
|
||||
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))
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0]))
|
||||
|
||||
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():
|
||||
print u"Writing msgs for {}".format(name)
|
||||
safe_name = safe_filename(name)
|
||||
with open(os.path.join(output_dir, safe_name + '.txt'), 'w') as f:
|
||||
for m in msgs:
|
||||
print >> f, m
|
||||
parser = WeChatDBParser(db_file)
|
||||
|
||||
for chatid, msgs in parser.msgs_by_chat.items():
|
||||
name = parser.contacts[chatid]
|
||||
if len(name) == 0:
|
||||
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 -*-
|
||||
# File: create_table.py
|
||||
# Date: Wed Mar 25 16:43:22 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
# Author: Yuxin Wu
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -10,7 +10,7 @@ import os
|
||||
from libchat import SqliteLibChat
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print "Usage: {} <DB file name>"
|
||||
print("Usage: {} <DB file name>")
|
||||
sys.exit()
|
||||
|
||||
db_name = sys.argv[1]
|
||||
@@ -1,8 +1,5 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
# File: libchat.py
|
||||
# Date: Sun Apr 12 21:08:51 2015 +0900
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
@@ -99,7 +96,7 @@ class SqliteLibChat(object):
|
||||
else:
|
||||
self.c.execute("SELECT * FROM message WHERE {}".format(
|
||||
' AND '.join(["{} = {}".format(k, v)
|
||||
for k, v in predicate.iteritems()])))
|
||||
for k, v in predicate.items()])))
|
||||
for row in self.c.fetchall():
|
||||
yield ChatMsg(*SqliteLibChat.postfilter(row))
|
||||
|
||||
@@ -113,5 +110,5 @@ if __name__ == '__main__':
|
||||
|
||||
for k in db.iterate_all_msg():
|
||||
from IPython import embed; embed()
|
||||
print k
|
||||
print(k)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
#!/usr/bin/env python2
|
||||
# -*- coding: UTF-8 -*-
|
||||
# File: libchathelper.py
|
||||
# Date: Thu Jun 18 00:02:35 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
import base64
|
||||
from pyquery import PyQuery
|
||||
@@ -12,8 +8,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
from libchat.libchat import SqliteLibChat, ChatMsg
|
||||
from .msg import *
|
||||
from common.timer import timing
|
||||
from common.progress import ProgressReporter
|
||||
from .common.timer import timing
|
||||
from .common.progress import ProgressReporter
|
||||
|
||||
class LibChatHelper(object):
|
||||
""" Build LibChat messages from WeChat Msg"""
|
||||
@@ -46,21 +42,27 @@ class LibChatHelper(object):
|
||||
return img, 'jpeg'
|
||||
elif msg.type == TYPE_EMOJI:
|
||||
md5 = msg.imgPath
|
||||
emoji_img, format = self.res.get_emoji_by_md5(md5)
|
||||
return emoji_img, format
|
||||
if md5:
|
||||
emoji_img, format = self.res.get_emoji_by_md5(md5)
|
||||
return emoji_img, format
|
||||
else:
|
||||
return '', ''
|
||||
elif msg.type == TYPE_CUSTOM_EMOJI:
|
||||
pq = PyQuery(msg.content)
|
||||
md5 = pq('emoticonmd5').text()
|
||||
img, format = self.res.get_emoji(md5, None)
|
||||
return img, format
|
||||
if md5:
|
||||
img, format = self.res.get_emoji(md5, None)
|
||||
return img, format
|
||||
else:
|
||||
return '', ''
|
||||
else:
|
||||
return '', ''
|
||||
|
||||
def _get_sound(self, msg):
|
||||
if msg.type == TYPE_SPEAK:
|
||||
audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
|
||||
return '{}:{}'.format(duration, base64.b64decode(audio_str))
|
||||
return ''
|
||||
return base64.b64decode(audio_str)
|
||||
return b''
|
||||
|
||||
def _get_extra(self, msg):
|
||||
ret = {}
|
||||
@@ -77,7 +79,7 @@ class LibChatHelper(object):
|
||||
if img:
|
||||
# TODO don't use b64, directly return image content
|
||||
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)
|
||||
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 -*-
|
||||
# File: list-chats.py
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
@@ -6,7 +6,7 @@
|
||||
from wechat.parser import WeChatDBParser
|
||||
import sys
|
||||
if len(sys.argv) != 2:
|
||||
print "Usage: {} db_file".format(sys.argv[0])
|
||||
print("Usage: {} db_file".format(sys.argv[0]))
|
||||
sys.exit(1)
|
||||
|
||||
db_file = sys.argv[1]
|
||||
@@ -14,4 +14,4 @@ db_file = sys.argv[1]
|
||||
parser = WeChatDBParser(db_file)
|
||||
chats = parser.msgs_by_chat.keys()
|
||||
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 -*-
|
||||
# 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 common.textutil import ensure_unicode
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
import numpy as np
|
||||
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]
|
||||
name = ensure_unicode(sys.argv[2])
|
||||
every_k_days = 2
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0]))
|
||||
|
||||
parser = WeChatDBParser(db_file)
|
||||
msgs = parser.msgs_by_chat[name]
|
||||
times = [x.createTime for x in msgs]
|
||||
start_time = times[0]
|
||||
diffs = [(x - start_time).days for x in times]
|
||||
max_day = diffs[-1]
|
||||
db_file = sys.argv[1]
|
||||
name = sys.argv[2]
|
||||
every_k_days = 2
|
||||
|
||||
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()
|
||||
parser = WeChatDBParser(db_file)
|
||||
msgs = parser.msgs_by_chat[name]
|
||||
times = [x.createTime for x in msgs]
|
||||
start_time = times[0]
|
||||
diffs = [(x - start_time).days for x in times]
|
||||
max_day = diffs[-1]
|
||||
|
||||
width = 20
|
||||
numbers = range((max_day / width + 1) * width + 1)[::width]
|
||||
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
|
||||
plt.xticks(numbers, labels)
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel("Number of msgs in k days")
|
||||
plt.hist(diffs, bins=max_day / every_k_days)
|
||||
plt.show()
|
||||
|
||||
# statistics by hour
|
||||
# I'm in a different time zone in this period:
|
||||
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
|
||||
#def real_hour(x):
|
||||
#for k, v in TZ_DELTA.iteritems():
|
||||
#if x > k[0] and x < k[1]:
|
||||
#print x
|
||||
#return (x.hour + v + 24) % 24
|
||||
#return x.hour
|
||||
#for k, v in TZ_DELTA.items():
|
||||
#if x > k[0] and x < k[1]:
|
||||
#print x
|
||||
#return (x.hour + v + 24) % 24
|
||||
#return x.hour
|
||||
#hours = [real_hour(x) for x in times]
|
||||
#plt.ylabel("Number of msgs")
|
||||
#plt.xlabel("Hour in a day")
|
||||
|
||||
@@ -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
|
||||
# File: compile_silk.sh
|
||||
# Date: Tue Jun 16 22:26:49 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
cd `dirname "$0"`/silk
|
||||
make
|
||||
|
||||
@@ -47,7 +47,7 @@ ifeq (yes,$(USE_NEON))
|
||||
endif
|
||||
|
||||
|
||||
CFLAGS += -Wall -enable-threads -O3
|
||||
CFLAGS += -Wall -O3
|
||||
|
||||
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
||||
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import logging
|
||||
@@ -21,10 +21,10 @@ def set_level_color(lvl, color):
|
||||
logger.addHandler(handler)
|
||||
|
||||
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')
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.info("info")
|
||||
logger.warn("warn")
|
||||
logger.warning("warn")
|
||||
|
||||
|
||||
@@ -1,78 +1,71 @@
|
||||
#!/usr/bin/env python2
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
# File: audio.py
|
||||
# Date: Fri Jun 26 10:42:41 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
import os
|
||||
from subprocess import PIPE, Popen, call
|
||||
import tempfile
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import pysox
|
||||
|
||||
from common.textutil import get_file_b64
|
||||
from .common.textutil import get_file_b64
|
||||
from .common.procutil import subproc_succ
|
||||
|
||||
SILK_DECODER = os.path.join(os.path.dirname(__file__),
|
||||
'../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):
|
||||
try:
|
||||
return do_parse_wechat_audio_file(file_name)
|
||||
except Exception as e:
|
||||
logger.error("Pase audio file {} error!".format(file_name))
|
||||
logger.error(e)
|
||||
logger.error(f"Error when parsing audio file {file_name}: {str(e)}")
|
||||
return "", 0
|
||||
|
||||
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
|
||||
|
||||
mp3_file = os.path.join('/tmp',
|
||||
os.path.basename(file_name)[:-4] + '.mp3')
|
||||
with open(file_name) as f:
|
||||
header = f.read(10)
|
||||
if 'AMR' in header:
|
||||
# maybe this is faster than calling sox from command line?
|
||||
infile = pysox.CSoxStream(file_name)
|
||||
outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
||||
chain = pysox.CEffectsChain(infile, outfile)
|
||||
chain.flow_effects()
|
||||
outfile.close()
|
||||
with tempfile.TemporaryDirectory(prefix="wechatdump_audio") as temp:
|
||||
mp3_file = os.path.join(temp,
|
||||
os.path.basename(file_name)[:-4] + '.mp3')
|
||||
with open(file_name, 'rb') as f:
|
||||
header = f.read(10)
|
||||
if b'AMR' in header:
|
||||
cmd = f"sox -e signed -c 1 {file_name} {mp3_file}"
|
||||
subproc_succ(cmd)
|
||||
cmd = f"soxi -D {mp3_file}"
|
||||
duration = float(subproc_succ(cmd))
|
||||
|
||||
signal = infile.get_signal().get_signalinfo()
|
||||
duration = signal['length'] * 1.0 / signal['rate']
|
||||
elif 'SILK' in header:
|
||||
raw_file = os.path.join('/tmp',
|
||||
os.path.basename(file_name)[:-4] + '.raw')
|
||||
proc = Popen('{0} {1} {2}'.format(SILK_DECODER,
|
||||
file_name, raw_file),
|
||||
shell=True, stdout=PIPE, stderr=PIPE)
|
||||
stdout = proc.communicate()[0]
|
||||
for line in stdout.split('\n'):
|
||||
if 'File length' in line:
|
||||
duration = float(line[13:-3].strip())
|
||||
break
|
||||
# The below is python2 only. It should be equivalent to using sox from command line
|
||||
# import pysox
|
||||
# infile = pysox.CSoxStream(file_name)
|
||||
# outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
||||
# chain = pysox.CEffectsChain(infile, outfile)
|
||||
# chain.flow_effects()
|
||||
# outfile.close()
|
||||
# signal = infile.get_signal().get_signalinfo()
|
||||
# duration = signal['length'] * 1.0 / signal['rate']
|
||||
elif b'SILK' in header:
|
||||
if not os.path.exists(SILK_DECODER):
|
||||
raise RuntimeError("Silk decoder is not compiled. Please see README.md.")
|
||||
|
||||
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:
|
||||
raise RuntimeError("Error decoding silk audio file!")
|
||||
|
||||
# I don't know how to do this with pysox
|
||||
proc = call('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(
|
||||
raw_file, mp3_file), shell=True)
|
||||
os.unlink(raw_file)
|
||||
else:
|
||||
raise NotImplementedError("Unsupported Audio Format! This is a bug!")
|
||||
try:
|
||||
raise NotImplementedError("Audio file format cannot be recognized.")
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
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 -*-
|
||||
# File: avatar.py
|
||||
# Date: Thu Jun 18 00:02:07 2015 +0800
|
||||
# Author: Yuxin Wu <[email protected]>
|
||||
|
||||
from PIL import Image
|
||||
import cStringIO
|
||||
import io
|
||||
import glob
|
||||
import os
|
||||
import numpy as np
|
||||
import logging
|
||||
import sqlite3
|
||||
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):
|
||||
def __init__(self, avt_dir, avt_db="avatar.index"):
|
||||
self.avt_dir = avt_dir
|
||||
def __init__(self, res_dir, avt_db="avatar.index"):
|
||||
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
|
||||
if self.avt_db is None or not os.path.isfile(self.avt_db):
|
||||
logger.warn(
|
||||
"Avatar database {} not found. Will not use avatar!".format(avt_db))
|
||||
self.avt_db = None
|
||||
self._use_avt = True
|
||||
if self.avt_db is not None:
|
||||
if len(glob.glob(os.path.join(self.sfs_dir, 'avatar*'))) == 0:
|
||||
# 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):
|
||||
""" username: `username` field in db.rcontact"""
|
||||
if self.avt_db is None: return None
|
||||
|
||||
username = ensure_bin_str(username)
|
||||
filename = md5(username)
|
||||
dir1, dir2 = filename[:2], filename[2:4]
|
||||
filename = os.path.join(dir1, dir2,
|
||||
"user_{}.png".format(filename))
|
||||
|
||||
try:
|
||||
try:
|
||||
pos, size = self.query_index(filename)
|
||||
return self.read_img(pos, size)
|
||||
except TypeError:
|
||||
logger.warn("Avatar for {} not found in avatar database.".format(username))
|
||||
return None
|
||||
except Exception as e:
|
||||
raise
|
||||
print e
|
||||
logger.warn("Failed to retrieve avatar!")
|
||||
if not self._use_avt:
|
||||
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
|
||||
fname = os.path.join(self.avt_dir,
|
||||
fname = os.path.join(self.sfs_dir,
|
||||
'avatar.block.' + '{:05d}'.format(file_idx))
|
||||
# a 64-byte offset of each block file
|
||||
start_pos = pos - file_idx * (2**32) + 64
|
||||
# offset of each block file: 17 + len(path)
|
||||
start_pos = pos - file_idx * (2**32) + 16 + len(filename) + 1
|
||||
try:
|
||||
with open(fname, 'rb') as f:
|
||||
f.seek(start_pos)
|
||||
data = f.read(size)
|
||||
im = Image.open(cStringIO.StringIO(data))
|
||||
im = Image.open(io.BytesIO(data))
|
||||
return im
|
||||
except IOError as e:
|
||||
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
|
||||
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)
|
||||
cursor = conn.execute("select Offset,Size from Index_avatar where FileName='{}'".format(filename))
|
||||
pos, size = cursor.fetchone()
|
||||
return pos, size
|
||||
cursor = conn.execute("select FileName,Offset,Size from Index_avatar")
|
||||
candidates = []
|
||||
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
|
||||
|
||||
|
||||
|
||||