Skip to content

Commit

Permalink
Full automation via devtools protocol (#6)
Browse files Browse the repository at this point in the history
* ✨ (debugger) automatic QR sign-in

* ♻️ logger & context

* 📝 update README.md

* 📝 add devtools protocol instructions

* 🐛 top-level try-catch block

* ✨ (debugger) override ua
  • Loading branch information
ManiaciaChao authored Apr 25, 2021
1 parent fdf2a6f commit 8583317
Show file tree
Hide file tree
Showing 14 changed files with 458 additions and 127 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
## Features

- Multi-mode sign-in support: normal, GPS and QR code.
- Sign in **WITHOUT any assistance** from your classmates
- Automatically sign in **WITHOUT any assistance** from your classmates
- System-level notification support (test on windows 10 & macOS & gnome)
- Active development
- <del>Docker support</del> Incoming!
Expand Down Expand Up @@ -41,7 +41,7 @@ cp sample.config.json config.json

```javascript
{
"interval": 3000, // 签到检测轮询间隔,单位 ms
"interval": 10000, // 签到检测轮询间隔,单位 ms
"wait": 5000, // 检测到签到后等待时长,单位 ms
// 用于 GPS 签到(大概是 Google 坐标)
"lat": 30.511227, // 纬度
Expand All @@ -52,7 +52,6 @@ cp sample.config.json config.json
"copy": "echo {} | pbcopy", // qr.mode == "copy" 时启用,{} 为占位符
},
"qr": { // 用于二维码签到
"name": "张三", // 微助教用户名,判断签到是否成功
// 模式
// terminal: 终端打印二维码,微信扫码
// plain: 终端打印签到URL,微信打开
Expand All @@ -68,7 +67,7 @@ cp sample.config.json config.json

### Get your `openId`

Get your `openId` from WeChat official account `微助教服务号`. [How?](./AcquireOpenID.md)
Get your `openId` from WeChat official account `微助教服务号`. [How?](./docs/AcquireOpenID.md)

**Notice that `openId` will expire after thousands of requests or another entrace from WeChat.**

Expand Down Expand Up @@ -102,6 +101,17 @@ Attention that **the script WILL EXIT INSTANTLY when success, because a QR scan
via WeChat will causes the update of `openId`. You have to reacquire your new
`openId` and run this script again!**

### *Experimental* Devtools Protocol

Due to the limitation of WeChat API, the following WeChat-related processes can't be simulated:

* generate new `openId`
* simulate the QR scanning

However, [Devtools Protocol](https://chromedevtools.github.io/devtools-protocol/) can help automatize the whole thing.

Check [this](./docs/DevtoolsProtocol.md) for more details.

## Author

👤 **maniacata**
Expand Down
10 changes: 10 additions & 0 deletions config.json.bak
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"interval": 3000,
"wait": 5000,
"lat": 30.511227,
"lon": 114.41021,
"qr": {
"mode": "terminal"
},
"ua": ""
}
File renamed without changes.
79 changes: 79 additions & 0 deletions docs/DevtoolsProtocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Devtools Protocol

> **WARNING:** Making devtools enable could be dangerous. Anyone knows debugging port could read your cookies & modify your webpages. Do it at your own risk!
## Enable Remote Debugging

### Windows

Launch WeChat with command line argument `--remote-debugging-port=<PORT>` (using port 8000 for example).

### Wine

Basically the same as Windows, but you have to find out the right file to edit.

On archlinux, it's function `CallWeChat()` in `~/.deepinwine/deepin-wine-helper/run_v3.sh`:

```shell
CallWeChat()
{
# ...
CallProcess "$@" "--remote-debugging-port=<PORT>"
}
```

### iOS/iPadOS/macOS

I don't know how to make this work on Apple things.

### Android

USB/Wireless debugging is required to be enabled.

Check your connections:

```shell
$ adb devices
List of devices attached
<DEVICE_NAME> device
```

Forward to localhost:
```shell
$ adb -s <DEVICE_NAME> forward tcp:<PORT> localabstract:chrome_devtools_remote
```

In WeChat browser:

1. Open [debugmm.qq.com/?forcex5=true](debugmm.qq.com/?forcex5=true) to enable x5 core.
2. Open [http://debugtbs.qq.com/](http://debugtbs.qq.com/) to install x5 core.
3. Open [http://debugx5.qq.com/](http://debugx5.qq.com/). In `信息` tab, check option `打开TBS内核Inspector调试功能`.
4. Restart WeChat.

## Configuration

Open `http://localhost:<PORT>/json` to see if remote debugging works.

Add field `devtools` into your `config.json`

```json
{
// ...
"devtools": {},
// ...
}
```

or with your custom host & port:

```json
{
// ...
"devtools":{ //
"host": "127.0.0.1", // Optional, 127.0.0.1 by default
"port": <PORT>, // Optional, 8000 by default
"local": true, // Optional, true by default
},
// ...
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@types/qrcode": "^1.3.5",
"chrome-remote-interface": "^0.30.0",
"node-fetch": "^2.6.1",
"node-notifier": "^8.0.0",
"qrcode": "^1.4.4",
Expand Down
45 changes: 34 additions & 11 deletions src/QRSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import WebSocket from 'ws';
import { toString as toQR } from 'qrcode';
import { IBasicSignInfo } from './requests';
import { qr } from './consts';
import { copyToClipBoard } from './utils';
import { copyToClipBoard, makeDebugLogger } from './utils';
import { WechatDevtools } from './cdp';
import { IContext } from './sign';

const debugLogger = makeDebugLogger('QRSign::');

interface IChannelMessage {
id: string;
Expand Down Expand Up @@ -55,6 +59,11 @@ interface IQRMessage {
type successCallback = (result: IQRStudentResult) => void;
type errorCallback = (err: any) => void;

interface IQRSignOptions extends IBasicSignInfo {
setOpenId?: (openId: string) => void;
devtools?: WechatDevtools;
}

export class QRSign {
// static
static endpoint = 'wss://www.teachermate.com.cn/faye';
Expand All @@ -67,15 +76,16 @@ export class QRSign {
private interval: NodeJS.Timeout | undefined;
private onError: errorCallback | null = null;
private onSuccess: successCallback | null = null;

private ctx: IContext;
private currentQRUrl = '';

static testQRSubscription = (msg: IChannelMessage): msg is IQRMessage =>
/attendance\/\d+\/\d+\/qr/.test(msg.channel);

constructor(info: IBasicSignInfo) {
constructor(ctx: IContext, info: IQRSignOptions) {
this.courseId = info.courseId;
this.signId = info.signId;
this.ctx = ctx;
}

startSync(cb?: successCallback, err?: (err: any) => void) {
Expand All @@ -87,7 +97,7 @@ export class QRSign {
this.handshake();
});
this.client.on('message', (data) => {
console.log(data);
debugLogger(`receiveMessage`, data);
this.handleMessage(data.toString());
});
this.onError && this.client.on('error', this.onError);
Expand All @@ -109,15 +119,14 @@ export class QRSign {
private get seqId() {
return `${this._seqId++}`;
}
//

private sendMessage = (msg?: object) => {
console.log(msg);
debugLogger(`sendMessage`, msg);
const raw = JSON.stringify(msg ? [msg] : []);
this.client?.send(raw);
};

private handleQRSubscription = (message: IQRMessage) => {
private handleQRSubscription = async (message: IQRMessage) => {
const { data } = message;
switch (data.type) {
case QRType.code: {
Expand All @@ -126,6 +135,18 @@ export class QRSign {
return;
}
this.currentQRUrl = qrUrl;
// TODO: should devtools conflict with printer?
if (this.ctx.devtools) {
// automation via devtools
const { openId } = await this.ctx.devtools.finishQRSign(qrUrl);
// reset openId is mandatory, for scanning QR code triggering another oauth
this.ctx.openId = openId;
// Currently, QRType.result is still used for more infomations
// if (result.success) {
// this.onSuccess?.({} as IQRStudentResult);
// }
}
// manually print or execute command
switch (qr.mode) {
case 'terminal': {
toQR(this.currentQRUrl, { type: 'terminal' }).then(console.log);
Expand All @@ -141,11 +162,13 @@ export class QRSign {
default:
break;
}

break;
}
case QRType.result: {
const { student } = data;
if (student && student.name === qr.name) {
// TODO: get student info from devtools
if (student && student.name === this.ctx.studentName) {
this.onSuccess?.(student);
}
break;
Expand All @@ -168,13 +191,13 @@ export class QRSign {
if (!successful) {
// qr subscription
if (QRSign.testQRSubscription(message)) {
console.log(`${channel}: successful!`);
debugLogger(`${channel}: successful!`);
this.handleQRSubscription(message);
} else {
throw `${channel}: failed!`;
}
} else {
console.log(`${channel}: successful!`);
debugLogger(`${channel}: successful!`);
switch (message.channel) {
case '/meta/handshake': {
const { clientId } = message as IHandShakeMessage;
Expand All @@ -199,7 +222,7 @@ export class QRSign {
}
}
} catch (err) {
console.log(`QR: ${err}`);
console.error(`QR: ${err}`);
}
};

Expand Down
Loading

0 comments on commit 8583317

Please sign in to comment.