掘金自动签到➕定时执行➕邮箱推送 你还想要啥❓

掘金自动签到 ➕ 定时执行 ➕邮箱推送 你还想要啥❓

前言

冒着被关进小黑屋的风险 写了个掘金自动签到抽奖程序 包含了自动执行、签到、免费抽奖、沾喜气、邮件通知的脚本,以后再也不用每天忘记签到了😄

事情是这样的 元旦那天由于玩的太嗨 忘记了掘金签到这么一回事 作为一名专业的摸鱼🐟选手 怎么能断签呢(好吧 我是为了第二天5120矿石)果断买了补签卡 后来在想 为啥不写一个每天自动执行的签到脚本呢??

重要❗

掘金团队已经对签到脚本采取措施,禁止🙅‍♂️使用自动签到脚本,违者将清空所有矿石或者封号,具体规则可参见禁止脚本签到行为沸点 ,大家还是遵循社区规范,每天登陆掘金进行签到。之前有使用过该项目的,建议将fork下来的仓库workflow手动禁止,或者直接删除项目,文章中的github项目链接我已删除。

image.png

具备能力

  • [x] action 每天9点定时执行
  • [x] 邮件通知
    • [ ] cookie过期邮件通知
  • [x] 签到
  • [x] 沾喜气
  • [x] 抽奖
  • [ ] 设定想要兑换的周边 自动计算还需要签到多少天
    目前就具备这么多能力 项目会持续维护 添加的新的功能(前提是不会被优弧关起来)如果有更好的项目可以在评论区提出

Start quickly

用编辑器打开项目后 需要将带有手动填写的几项数据修改为自己的 其他不要动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 配置文件
module.exports = {
// 需要手动填写
cookie: '',
// 请求地址
baseUrl: 'https://api.juejin.cn',
// api地址
api: {
...
},
// 邮箱配置
emailConfig: {
// 邮箱服务 163|qq
service: '163',
// 邮箱 手动填写
email: '',
// 邮箱授权码 手动填写
pass: '',
}
}

cookie获取方式

cookie 有过期时间 大概是一个月 或者是退出登陆也会过期
登陆进入到掘金,F12打开控制台,选择network后随便点击一个接口,找到请求头中的cookie,选中数据后右键复制值

image

邮箱设置

这里以163邮箱为例 qq邮箱同理 如果是163邮箱 直接将service字段设置为163(qq邮箱就写qq) 然后填入你自己的邮箱 邮件发送成功 登陆邮箱会看到你给自己发了一条邮件 就像这样

image

授权码获取⚠️:
登陆进入163邮箱 打开设置

image

将以下几个设置打开 打开IMAP/SMTP服务时会弹窗发送短信 微信扫码后就可以发送短信(qq邮箱这一步开启需要手动进行验证发送短信)

image

我这里已经添加过了 就直接点击新增授权 也是一样会弹出二维码扫码发送短信
image

短信发送完毕后点击我已发送 然后就会得到你的授权码(注意授权码只展示一次) 将授权吗粘贴到配置文件中的 pass字段

image

将所有的参数填入无误后 可以用命令node index.js本地运行 可以收到邮件并且邮件里有日志消息 恭喜你🎉 以后再也不用每天签到了(会不会被官方打死)

确认无误后将修改后的项目push 项目已经设置了自动执行 每天9点会自动执行签到 并且发送邮件进行通知

自从用了自动签到后 妈妈再也不用担心我忘记签到了 兑换Switch不是梦

具体实现

如果只关注脚本功能 看到这里就可以左拐🚪了 如果对实现感兴趣 这里也和你分享一下具体的实现思路

有了想法之后就开始去扒掘金签到相关的接口 挨个接口点开看 都是给了些啥数据 每个数据都是用来干啥的 经过漫长的调试后 脚本签到能力就完成了 功能主要由一下几个接口组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 签到
checkIn: '/growth_api/v1/check_in',
// 查询签到
getCheckStatus: '/growth_api/v1/get_today_status',
// 查询签到天数
getCheckInDays: '/growth_api/v1/get_counts',
// 查询当前矿石
getCurrentPoint: '/growth_api/v1/get_cur_point',
// 查询抽奖
getlotteryStatus: '/growth_api/v1/lottery_config/get',
// 抽奖
draw: '/growth_api/v1/lottery/draw',
// 沾喜气
dipLucky: '/growth_api/v1/lottery_lucky/dip_lucky'
// 获取沾喜气列表用户
getLuckyUserList: '/growth_api/v1/lottery_history/global_big'

接下来就很简单了 接接口嘛 谁还不会了 找个请求库直接干 这里我选用的是axios

先配置一下请求 在index.js写入 将config文件中的cookie丢进请求头中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 配置请求地址
axios.defaults.baseURL = config.baseUrl

// 设置cookie
axios.defaults.headers['cookie'] = config.cookie

// 相应拦截处理
axios.interceptors.response.use((response) => {
const { data } = response
if (data.err_msg === 'success' && data.err_no === 0) {
return data
} else {
return Promise.reject(data.err_msg)
}
}, (error) => {
return Promise.reject(error)
})

接下来就直接请求接口,请求循序依次为

image.png

以下主要代码

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

/**
* 查看今天是否已经签到
*
* @return {Boolean} 是否签到过
*/
const getCheckStatus = async () => {
try {
const getCheckStatusRes = await axios({
url: config.api.getCheckStatus,
method: 'get'
})
return getCheckStatusRes.data
} catch (error) {
throw `查询签到失败!【${error}】`
}
}

/**
* 查询当前矿石
*
*/
const getCurrentPoint = async () => {
try {
const getCurrentPointRes = await axios({ url: config.api.getCurrentPoint, method: 'get' })
console.log(`当前总矿石: ${getCurrentPointRes.data}数`)
} catch (error) {
throw `查询矿石失败!${error.err_msg}`
}

}
/**
* 查询免费抽奖次数
*
* @return {Boolean} 是否有免费抽奖次数
*/
const getlotteryStatus = async () => {
try {
const getlotteryStatusRes = await axios({ url: config.api.getlotteryStatus, method: 'get' })
return getlotteryStatusRes.data.free_count === 0
} catch (error) {
throw `查询免费抽奖失败!【${error}】`
}
}

/**
* 获取沾喜气列表用户historyId
*
* @return {string} 被沾的幸运儿的history_id
*/
const getLuckyUserHistoryId = async () => {
try {
// 接口为分页查询 默认查询条10条数据 {page_no: 0, page_size: 5}
const luckyList = await axios({ url: config.api.getLuckyUserList, method: 'post' })
// 随机抽取一位幸运儿 沾他
return luckyList.data.lotteries[Math.floor(Math.random() * luckyList.data.lotteries.length)]?.history_id
} catch (error) {
throw `获取沾喜气列表用户historyId失败`
}
}

/**
* 沾喜气
*
*/
const dipLucky = async () => {
try {
// 获取historyId
const historyId = await getLuckyUserHistoryId()
// 沾喜气接口 传递lottery_history_id
const dipLuckyRes = await axios({ url: config.api.dipLucky, method: 'post', data: { lottery_history_id: historyId } })
console.log(`占喜气成功! 🎉 【当前幸运值:${dipLuckyRes.data.total_value}/6000】`)
} catch (error) {
throw `占喜气失败! ${error}`
}
}

/**
* 抽奖
*
*/
const draw = async () => {
try {
const freeCount = await getlotteryStatus()
if (freeCount) {
// 没有免费抽奖次数
throw '今日免费抽奖以用完'
}

// 开始抽奖
const drawRes = await axios({ url: config.api.draw, method: 'post' })
console.log(`恭喜你抽到【${drawRes.data.lottery_name}】🎉`)

// 先沾一下喜气
await dipLucky()

if (drawRes.data.lottery_type === 1) {
// 抽到矿石 查询总矿石
await getCurrentPoint()
}
} catch (error) {
console.error(`抽奖失败!=======> 【${error}】`)
}
}

/**
*查询签到天数
*
* @return {Object} continuousDay 连续签到天数 sumCount 总签到天数
*/
const getCheckInDays = async () => {
try {
const getCheckInDays = await axios({ url: config.api.getCheckInDays, method: 'get' })
return { continuousDay: getCheckInDays.data.cont_count, sumCount: getCheckInDays.data.sum_count }
} catch (error) {
throw `查询签到天数失败!🙁【${getCheckInDays.err_msg}】`
}
}


/**
* 签到
*
*/
const checkIn = async () => {
try {
// 查询今天是否签到没
const checkStatusRes = await getCheckStatus()

if (!checkStatusRes) {
// 签到
const checkInRes = await axios({ url: config.api.checkIn, method: 'post' })
console.log(`签到成功,当前总矿石${checkInRes.data.sum_point}`)

// 查询签到天数
const getCheckInDaysRes = await getCheckInDays()
console.log(`连续抽奖${getCheckInDaysRes.continuousDay}天 总签到天数${getCheckInDaysRes.sumCount}`)

// 签到成功 去抽奖
await draw()
} else {
console.log('今日已经签到 ✅')
}

} catch (error) {
console.error(`签到失败!=======> ${error}`)
}
}

自动执行

关于自动执行 我最开始想的方案是通过服务器部署 开启一个定时任务去执行 这种方式需要有服务器 比较麻烦 也有人用云函数 我又懒得去注册 后了找到一种方案就是 白嫖Github Action 通过CI设置定时任务 每天自动执行 Github人人都有 要求也较低 由于我不是很懂CI这方面的东西 这次为了脚本也只是学了个皮毛 具体代码如下

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
name: jjCheckInScript

on:
schedule:
# 每天9点执行 时间格式 minute hour day month week 设置的时间是UTC 不是北京时间 需要+8
- cron: "0 1 * * *"

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
start:
# 运行环境为最新版的Ubuntu
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

[[安装node]].js
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14'

# 安装依赖并且执行脚本
- name: npm install
run: npm install

- name: Start task
run: node index.js

这里关键的代码是schedule 将触发任务的方式改为定时执行 到了设定好的时间后会自动执行任务 任务会以最新版本的ubuntu系统进行运行 安装node 安装项目中的依赖后 执行index.js中的代码 如果想要修改执行时间 按照minute hour day month week 的格式修改schedule字段即可(设置的时间是UTC 北京时间 需要**+8**)

邮件发送📧

邮件发送这里选用的是Nodemail库 它的功能十分强大 支持多种邮箱服务 支持HTML内容、纯文本内容、附件、图片等等 发送邮件方式也很简单 具体代码如下:

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
const nodemailer = require('nodemailer')

// 日志处理 将脚本日志通过ejs渲染成html
const logs = []
console.oldLog = console.log
console.oldErr = console.error

console.log = (str) => {
 logs.push({
   type: 'success',
   text: str
})
 console.oldLog(str)
}

console.error = (str) => {
 logs.push({
   type: 'error',
   text: str
})
 console.oldErr(str)
}

/**
* 发送邮件
*
*/
const sendEmail = async () => {
 try {
   const template = ejs.compile(fs.readFileSync(path.resolve(__dirname, 'email.ejs'), 'utf8'));
   const transporter = nodemailer.createTransport({
     service: process.env.SERVICE, // 邮箱服务
     port: 465, // 端口
     secure: true, // 使用TLS,SSL加密端口465
     secureConnection: true,
     auth: {
       user: process.env.EMAIL, // 发送者邮箱
       pass: process.env.PASS, // 邮箱授权码
    }
  })

   // 发送邮件
   await transporter.sendMail({
     from: process.env.EMAIL,
     to: process.env.EMAIL,
     subject: '掘金签到通知🔔',
     html: template({
       logs: logs
    })
  })

} catch (error) {
   console.error(`邮件发送失败!${error}`)
}

}

邮件内容 我这里用ejs写了一个简单的日志模版 用来承载脚本的log 在前面我对console.log console.error进行重写 将str存储到logs数组中 吧logs数据传入模版引擎生成html

邮件发送其实很简单 具体配置可以查看官方文档 这里就解释一下用到的配置

  • service: 邮箱服务 Node mai l内部已经支持了很多邮箱服务 如果填写这个字段就不需要写host
  • host: 邮箱的主机IP地址 这一项一版在开启IMAP/SMTP后会展示邮箱的IP地址
  • prot: 端口号 默认的为465
  • secure: 配置安全链接
  • secureConnection: 使用SSL(默认为false)
  • auth.user: 发送者邮箱
  • auth.pass: 邮箱授权码
  • from: 发送者邮箱
  • to: 接受者邮箱
  • subject: 邮件主题
  • html: 邮件内容html字符串

Actions secrets 密码安全

关于cookie 邮箱授权码 刚开始是写在一个配置文件夹里面 后来啦哥提出了更好的方式 就是用Actions secrets 这种方式可以避免关键数据暴露 (万一那个无聊的家伙拿你cookie去梭哈了呢😏) 还是小心为上

其实使用也很简单 我之前对这块完全不熟悉 看了会官方文档 三下五除二就弄好了 在添加好secrets数据后 我们需要在blank.yml文件中对数据进行获取 获取方式也很简单 直接通过${{secrets.youKey}}就可以获取到 以该项目为例

1
2
3
4
5
6
# 环境变量
env:
COOKIE: ${{ secrets.COOKIE }}
PASS: ${{ secrets.PASS }}
EMAIL: ${{ secrets.EMAIL }}
SERVICE: ${{ secrets.SERVICE }}

在设置完之后 我们就可以在环境变量中使用这些数据 就像这样

1
2
process.env.COOKIE
process.env.EMAIL

但是获取到的数据在Actions中是无法进行展示的 在输出日志中 你定义的所有密码都会被清除 并在输出日志之前用星号替换 也是为了防止泄漏

image

如果觉得这样还不够安全 在代码中可以随意使用到数据 可以尝试一下对密码进行加密

Q&A

自动执行延迟

在开发测试的时 发现jobs没有按时执行 九点五分到公司打开actions时发现并没有执行jobs 刚开始还以为是cron 时间填写错误 修改时间后发现github actions定时任务会有延迟 延迟时间几分钟到十几分钟甚至一小时都有 但这个并不影响我们签到功能 只要是在今天签到都可以

以我测试为例 将 corn时间设置为每天的12:30 但实际执行时间为 12:51 差不多延迟了20分钟

1
2
3
4
on:
# 定时执行
schedule:
- cron: "30 4 * * *"

jobs执行时间

查看相关文档后发现 在GitHub中关于Schedule的定义:

Note: The schedule event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour. To decrease the chance of delay, schedule your workflow to run at a different time of the hour.

注意: 在高负载的 GitHub action 工作流运行期间,调度事件可能会被延迟。高负载时间包括每个小时的开始。为了减少延迟的机会,请安排您的工作流在一小时的不同时间运行。

也就是说 Schedule中的cron时间并不是真正执行的时候 而是工作流进入到GitHub进行计划排队时间 说简单点就是工作流进入到GitHub执行的队列时间 具体什么时候执行工作流 则需要看GitHub工作流的负载

这个问题在签到需求中并不是致命的问题 如果想要解决可以参考Github Action的 Schedule 运行不准时的解决办法这篇文章

为啥不用document.cookie?

控制台输入命令获取到的cookie并不完整

这是控制台获取到的cookie,对比一下接口的cookie,相差很大
image

声明📢

本项目仅适用于学习交流 并不具备其他用途 也没有经过掘金官方团队 若是被封号 与我无关(手动狗头保命)

有其他想法或功能 欢迎👏进行讨论 如果对你有帮助 给个Star行不行