Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full automation via devtools protocol #6

Merged
merged 6 commits into from
Apr 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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