Node写一个掘金自动签到脚本

2,651 阅读4分钟

需求分析

  • 每天规定时间自动签到
  • 签到后能进行免费自动抽奖
  • 脚本执行成功后, 能发送签到结果以及抽奖结果到邮箱
  • 可以支持多个人并发运行脚本

设计思路

  1. 首先每个用户应该是独立执行的脚本,通过pm2来管理脚本进程
  2. 用户可以手动设置Cookie以及脚本执行时间
  3. 用户可以设置邮箱来接收签到结果
  4. 用户还可以查看脚本执行情况(需要一个日志列表)
  5. 用户如果不想自动签到还可以手动关闭签到脚本

准备工作

环境搭建

  • 安装nvm管理 node
  • 安装node-schedule来实现定时任务执行
  • 安装nodemailer来发送邮件通知
  • 安装axios来请求掘金API
  • 安装pm2来管理多个人开启的进程

前期准备

  • 需要授权得到授权码的邮件地址用于向用户发送邮件通知
  • 需要一个服务器来执行应用, 如果只自己使用则只需要本地启动, 保持电脑不关机

掘金相关API

https://api.juejin.cn/growth_api/v1/get_today_status // 查看是否签到
https://api.juejin.cn/growth_api/v1/check_in // 签到接口
https://api.juejin.cn/growth_api/v1/lottery_config/get // 获取免费抽奖次数
https://api.juejin.cn/growth_api/v1/lottery/draw // 抽奖接口
https://api.juejin.cn/growth_api/v1/get_cur_point // 获取剩余矿石数量
https://api.juejin.cn/study_api/v1/competition/competition_list // 获取掘金活动`不需要Cookie`

项目开始

登录页面

截屏2024-04-11 16.49.06.png

  1. 登录即注册,如果是新的用户名,则是注册;如果是存在的则会校验用户密码
  2. 邮箱可填可不填,主要用于是否需要邮件通知
const { username, password } = loginForm
if (!username || !password) {
    resolve(returnMsg('账号名或密码不可为空', null, 500))
    return
}
if (!/^[A-Za-z0-9]+$/.test(username)) {
    resolve(returnMsg('账号名只能包含英文与数字', null, 500))
    return
}
const userInfo = checkUserIsExist(username)
if (userInfo && userInfo.password !== password) {
    resolve(returnMsg('密码不正确', null, 500))
    return
}
const fileName = `${__dirname}/config/token.js`
const { info: fileContent, watcherToken } = handleLoginInfo(userInfo, loginForm)
await writeFileSync(fileName, fileContent)
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Set-Cookie', [`username=${username}`, `watcherToken=${watcherToken}`])
// 用户名已存在时登陆 => 会重新生成watcherToken
resolve(returnMsg())
主要代码分析
  1. checkUserIsExist: 主要从用户表查看是否存在当前用户
  2. handleLoginIofo: 主要是生成一个用户的唯一watcherToken
  3. writeFileSync(): 主要是将用户写入用户表,新用户直接写入,老用户主要是更新watcherToken以及邮箱地址
  4. 最后设置Cookie

主页面

截屏2024-04-11 16.55.45.png 主页面主要分为: 配置模块,脚本模块,日志模块,活动列表

配置模块

20240411-165907.jpeg 点击设置进入配置页面

  1. 可以更新Cookie,设置的Cookie会存入用户表的对应用户下
  2. 可以设置接收签到结果邮箱接收地址
  3. 可以设置脚本执行时间(不设置默认签到时间:早上8点整)
  4. 所有的设置需要重启脚本才能起效
脚本模块
  1. 可以开启自动签到脚本
  2. 可以关闭自动签到脚本
  3. 下面脚本进程就是检测当前脚本是否开启成功,对应name就是 autoScript-<登录名>
日志模块
  1. 获得所有脚本签到结果日志列表
  2. 包括脚本签到执行日期,剩余矿石数,免费抽奖结果,邮件发送是否成功等
  3. 可以手动去更新日志列表(不过每日签到应该作用不大)
活动列表
  1. 不需要设置掘金Token也可以获取的数据
  2. 主要展示掘金相关的活动
  3. 活动主要包括当前进行中的以及历史的一些活动

核心代码

自动签到脚本

  1. 开启一个定时任务执行自动签到主函数
const rule = new schedule.RecurrenceRule()
rule.hour = hour
rule.minute = minute
rule.second = second
schedule.scheduleJob(rule, main)
  1. 主函数流程
graph LR
A[获得当前用户对应的掘金Cookie] --> B[查看是否已经签到] -- 已签到 --> C[则无需再去签到]
B -- 未签到 --> 则调用接口去签到 --> D[获取签到结果] --> E[获取当前剩余矿石数量] --> F[获取免费抽奖次数]
C --> D
graph LR
A[获取免费抽奖次数] -- 次数0 --> B[没有免费次数则无需调接口去抽奖, 不然浪费矿石]
B --> D
A -- 次数1 --> C[则调用接口去抽奖] --> D[获取抽奖结果] --> E[根据当前用户是否设置邮件来发送执行结果]
async function main () {
    LOG_MSG.currentTime = `${getCurrentDate()} ${getCurrentTime()}`
    if (!username || !COOKIE) {
        LOG_MSG.error = '用户不存在,脚本执行失败'
        await toLog()
        return
    }
    const isSignedObj = await checkIsSignedIn(COOKIE)
    if (isSignedObj) {
        const { err_no, err_msg } = isSignedObj
        if (err_no ===  403 || err_msg === 'must login') {
            LOG_MSG.error = '当前Token登录失败, 请重新设置正确的Token'
            toLog()
            // Todo: 关闭脚本?
            return
        } else {
            LOG_MSG.signStatus = true
            EMAIL_MSG.signStatus = '今日已签到'
        }
    } else {
        const signStatusObj = await toSignIn(COOKIE)
        if (signStatusObj.data) {
            LOG_MSG.signResult = 'true'
            EMAIL_MSG.signStatus = '签到成功'
        } else {
            LOG_MSG.signResult = signStatusObj.err_msg || 'false'
            EMAIL_MSG.signStatus = signStatusObj.err_msg || '签到失败'
        }
    }
    const count = await toGetPointCount(COOKIE)
    LOG_MSG.remainedPoint = count || 0
    EMAIL_MSG.pointCount = count || 0
    const times = await queryFreeTimes(COOKIE)
    if (times) {
        LOG_MSG.freeDrawTimes = times
        const { lottery_name } = await toDraw(COOKIE)
        LOG_MSG.drawResult = lottery_name
        EMAIL_MSG.drawStatus = `${lottery_name}\n`
    } else {
        LOG_MSG.freeDrawTimes = 0
        EMAIL_MSG.drawStatus = '今日免费抽奖次数已用完'
    }
    if (email) {
        try {
            const emailResult = await sendEmail(email, handleEmailMessage())
            LOG_MSG.emailStatus = emailResult.messageId ? true : false
        } catch {
            LOG_MSG.emailStatus = false
        }
    }
    toLog()
}

邮件发送

const transporter = nodemailer.createTransport({
    host: SENDER.HOST,
    port: SENDER.PORT,
    secureConnection: true, // 不写这句会报错:Greeting never received
    auth: {
        user: SENDER.USER, 
        pass: SENDER.PASS  
    }
})

export const sendEmail = async (userEmail, content = RECIPIENT.DEFAULTMSG) => {
    return await transporter.sendMail({
        from: SENDER.USER,          // 发送邮件的地址
        to: userEmail,                   // 接收邮件的地址
        subject: RECIPIENT.SUBJECT, // 邮件标题
        text: content,              // 有 html,优先 html
        html: '',                   // html body
        attachments: ''             // 附件
    })
}

SENDER.USER: 设置一个用于发送给用户的邮箱
SENDER.PASS: 该邮箱对应的授权码

export const SENDER = {
    HOST: 'smtp.qq.com',
    PORT: 465,
    USER: '',  // 授权smtp邮箱地址
    PASS: ''   // 申请的授权码
}

export const RECIPIENT = {
    // 由用户设置
    // USER: '',
    SUBJECT: '掘金签到成功',
    DEFAULTMSG: `您于${new Date().toLocaleString()}, 在自动签到系统中签到成功`
}

如何获取Cooike

12.png

Github代码地址
测试Demo地址

PS: 这是作者自己搭的服务, 读者可自动决定是否加入开启签到脚本
PS: 如果有任务问题, 请在下面评论区提问