Github Actions 自动签到

使用有风险,最好是在自己服务器上跑,可以是自己安装环境然后定时运行 checkin.js,也可以是用 自托管运行器

主要代码

checkin.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
name: CheckIn
on:
# 可手动触发工作流程
workflow_dispatch:
# push时触发工作流程
push:
# 分支 现在默认是main,而不是master
branches: [ main ]
# 将工作流程配置为在至少一个文件不匹配paths-ignore的paths时运行
# 忽略README.md和imgs文件夹
paths-ignore:
- 'README.md'
- 'imgs/**'
# 定时触发工作流程
schedule:
# UTC 1点30分(北京时间 9点30分)
- cron: 30 1 * * *
# 标星时触发工作流程
watch:
types: [started]
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checks out a copy of your repository on the ubuntu-latest machine
# 必须,不然到执行 node checkin.js 时会报错
- uses: actions/checkout@v2
- name: setup Node.js
# 理论上也可以自己命令行安装node
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: install axios
run: npm install axios
# 安装 puppeteer-extra-plugin-stealth 绕过检测
- name: install puppeteer-extra-plugin-stealth
run: npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
- name: CheckIn
run: node checkin.js
# 设置环境变量
env:
PUSHPLUS: ${{ secrets.PUSHPLUS }}
COOKIES: ${{ secrets.COOKIES }}
# 保持 action 活跃
- name: keep alive
uses: gautamkrishnar/keepalive-workflow@master # using the workflow with default settings
# 删除工作流记录
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 0
keep_minimum_runs: 10
delete_run_by_conclusion_pattern: success

checkin.js

稍微修改下就可以变成其他网站登录的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
const axios = require('axios');
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

const INFO = {
account: '账号',
leftDays: '天数',
checkInMessage: '签到情况',
checkInFailded: '签到失败',
getStatusFailed: '获取信息失败'
};

const checkCOOKIES = (COOKIES) => {
const cookies = COOKIES?.split('&&') || [];

if (!cookies.length) {
console.error('不存在 COOKIES ,请重新检查');
return false;
}

for (const cookie of cookies) {
if (!cookie.includes('=')) {
console.error(`存在不正确的 cookie ,请重新检查`);
return false;
}

const pairs = cookie.split(/\s*;\s*/);
for (const pairStr of pairs) {
if (!pairStr.includes('=')) {
console.error(`存在不正确的 cookie ,请重新检查`);
return false;
}
}
}

return true;
}

const rawCookie2JSON = (cookie) => {
return cookie.split(/\s*;\s*/).reduce((pre, current) => {
const pair = current.split(/\s*=\s*/);
const name = pair[0];
const value = pair.splice(1).join('=');
return [
...pre,
{
name,
value,
'domain': '******.rocks'
}
];
}, []);
};

const checkInAndGetStatus = async (cookie) => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

const cookieJSON = rawCookie2JSON(cookie);
await page.setCookie(...cookieJSON);

await page.goto('https://******.rocks/console/checkin', {
timeout: 0,
waitUntil: 'load'
});

page.on('console', msg => {
if (console[msg.type()]) {
console[msg.type()](msg.text());
} else {
console.log(msg.text());
}
});

const info = await page.evaluate(async (INFO) => {
const checkIn = () =>
fetch('https://******.rocks/api/user/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
body: JSON.stringify({
token: "******.network"
})
}).catch(error => {
console.warn('checkIn 网络错误。');
return { reason: '网络错误' }
});

const getStatus = () => fetch('https://******.rocks/api/user/status').catch(error => {
console.warn('getStatus 网络错误。');
return { reason: '网络错误' }
});

let ret = {};

const checkInRes = await checkIn();
if (!checkInRes.ok) {
const reason = checkInRes.reason || `状态码:${checkInRes.status}`;
console.warn(`checkIn 请求失败,${reason}`);
ret[INFO.checkInFailded] = reason;
} else {
console.info('checkIn 请求成功。');
const { message } = await checkInRes.json();
ret[INFO.checkInMessage] = message;
}

const statusRes = await getStatus();
if (!statusRes.ok) {
const reason = statusRes.reason || `状态码:${statusRes.status}`;
console.warn(`getStatus 请求失败,${reason}`);
ret[INFO.getStatusFailed] = reason;
} else {
console.info('getStatus 请求成功。');
const { data: { email, phone, leftDays } = {} } = await statusRes.json();
let account = '未知账号';
if (email) {
account = email.replace(/^(.)(.*)(.@.*)$/,
(_, a, b, c) => a + b.replace(/./g, '*') + c
);
} else if (phone) {
account = phone.replace(/^(.)(.*)(.)$/,
(_, a, b, c) => a + b.replace(/./g, '*') + c
);
}
ret[INFO.account] = account;
ret[INFO.leftDays] = parseInt(leftDays);
}

return ret;
}, INFO);

await browser.close();

return info;
};

const pushplus = (token, infos) => {
const data = {
token,
title: '签到',
content: JSON.stringify(infos),
template: 'json'
};
console.log('pushData', {
...data,
token: data.token.replace(/^(.{1,4})(.*)(.{4,})$/, (_, a, b, c) => a + b.replace(/./g, '*') + c)
});

return axios({
method: 'post',
url: `http://www.pushplus.plus/send`,
data
}).catch((error) => {
if (error.response) {
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
console.warn(`PUSHPLUS推送 请求失败,状态码:${error.response.status}`);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
console.warn('PUSHPLUS推送 网络错误');
} else {
// 发送请求时出了点问题
console.log('Axios Error', error.message);
}
});
};

const CheckIn = async () => {
try {
if (checkCOOKIES(process.env.COOKIES)) {
const cookies = process.env.COOKIES.split('&&');

const infos = await Promise.all(cookies.map(cookie => checkInAndGetStatus(cookie)));
console.log('infos', infos);

const PUSHPLUS = process.env.PUSHPLUS;

if (!PUSHPLUS) {
console.warn('不存在 PUSHPLUS ,请重新检查');
}

if (PUSHPLUS && infos.length) {
const pushResult = (await pushplus(PUSHPLUS, infos))?.data?.msg;
console.log('PUSHPLUS pushResult', pushResult);
}
}
} catch (error) {
console.log(error);
}
};

CheckIn();

Q&A

checkin.js 就是简单地使用 axios 发送请求,checkin.yml 的疑问点,简单的在上面已经做了注释,下面是我自己尝试过程中比较困惑过的:

GitHub 官方表示,从 2020 年 10 月 1 日起,在该平台上创建的所有新的源代码仓库将默认被命名为 “ main “,而不是原先的” master “,以避免联想奴隶制。

Renaming the default branch from master

1
2
3
# 标星时运行
watch:
types: [started]

官网文档 WatchEvent

标星 REST API

关注者API更改帖子

我的理解是Github仓库中 star 对应之前的 watch ,而Github Action还是采用之前的叫法,所以在 check.yml 中用的关键词是 watch

1
2
3
# Checks out a copy of your repository on the ubuntu-latest machine
# 必须,不然到执行 node checkin.js 时会报错
- uses: actions/checkout@v2

在 官方文档 - Github Actions 快速入门 - 创建第一个工作流程 中有这么一条注释:

- uses: actions/checkout@v2 语句的作用是在服务器上检出我们仓库的副本,没有这条语句的话,执行到 node checkin.js 时会报错,因为到这步时服务器上相当于只安装了 node.jsaxios 请求库,没有我们的项目,所以运行 node checkin.js 时会找不到 check.js

官方文档中有较为详细的解释 关于加密密码

着重看下 在工作流程中使用加密密码

我使用的是环境变量的做法,所以在 check.js 中就要去获得环境变量 COOKIESPUSHPLUS

如何从 Node.js 读取环境变量

1
2
3
4
5
const cookies = process.env.COOKIES?.split('&&') ?? [];

...

const PUSHPLUS = process.env.PUSHPLUS;
作者

DullSword

发布于

2020-11-20

更新于

2024-07-02

许可协议

评论