由于疫情原因,幼儿园要求每天健康码打卡,每天都需要花时间重复这个动作,作为程序员,这肯定是不能容忍的,必须脚本安排起!

抓包分析API

首先我们需要做的就是抓包,提取API接口,有两种方式,一种是PC微信 + Fiddler,还有一种就是直接手机抓取,手机需要安装Http Canary,这是付费软件,当然也有破解版。

我这里就采用第一种方式了,Fiddler使用的是果壳破解版,下面就讲一下过程中遇到的问题,其实这个小程序在验证方面做的还是比较简单的,简单一个Authorization就完了,没有到期时长,不受机器限制;并且所有接口提交都不会验证客户端设备session等,大大有利于自动化脚本。

  1. 首先Fiddler抓取微信小程序,刚开始无法抓取,在网上搜了很多文章,发现问题出在微信小程序上,微信修改了框架导致的。参考上一篇文章:fiddler抓包PC微信小程序失败解决方案

  2. 然后是上传图片到OBS的问题:

    1. 虽然上在options中指定了host,但在headers中还是必须再加上Host
    2. 需要手动指定boundaryContent-Length
    {
        host: "obs.jielong.co",
    	headers: {
        	Host: "obs.jielong.co",
        	"Content-Length": await replaceCb2promise(form.getLength.bind(form), [])[1],
        	'Content-Type': 'multipart/form-data; boundary=' + form.getBoundary(),
    	},
    	body: form,
    }
    
    1. 发现一个问题,打卡项目的发布与否与图片提交地址有关系,没有Publisher的在本地,Publisher了的在华为云,相应的参数也会不同,有点小坑。
  3. 接着就是提交打卡记录,需要注意的是:

    1. 提交时需要添加Authorization
    2. Content-Type需要修改为application/json

接口分析其实是很简单的,结合以上需要规避的问题,基本上写自动化脚本就没啥问题了。

自动生成每日打卡截图

提交接口没问题了,打卡需要健康码截图,这里我就稍微偷了个懒,没有直接使用每日的健康码截图,而是将健康码截图后使用ps抹去了日期相关的信息,然后通过程序对图片写入响应日期信息。

整个脚本程序我使用tinyhttp写的,TS在处理图片方面还是比较麻烦,只能借用第三方库来处理,这里推荐两个库jimpimages,这里我采用的是jimp,需要说明的是在图片上生成日期的时候,jimp不支持丰富的样式,只能简单的字体。设置稍微复杂一点的文字就不行,比如字体颜色,不同字体大小等。这里我借助了其他人写的脚本来实现的。

先说下这个脚本的思路:

  1. 先创建透明图片,在图片上写入字体

  2. 用需要的颜色替换掉图片中字体的颜色

  3. 再将图片叠加到需要做水印的图片上

  4. 我在脚本源代码的基础上做了些许修改以适应通用场景

    // @ts-nocheck
    import Jimp from "jimp";
    import {Font} from "@jimp/plugin-print";
    
    const FONT_PATH = process.env.TMS_KOA_JIMP_FONT_PATH || Jimp.FONT_SANS_32_BLACK
    const FONT_SIZE = process.env.TMS_KOA_JIMP_FONT_SIZE || 32
    
    export default class {
        image: Jimp;
        font: Font;
        fontSize: number;
    
        // saver = null
    
        constructor(image: Jimp, font: Font, fontSize: number) {
            this.image = image
            this.font = font
            this.fontSize = fontSize
            // this.saver = new Saver()
        }
    
        /**
         *
         * @param {*} text
         * @param {*} x
         * @param {*} y
         * @param {*} color
         * @param {*} bgColor
         * @param {*} width
         * @param {*} fontSize
         * @param {*} align
         */
        addText(text: string, x: number, y: number, color: string, bgColor: string | null = null, width: number | null = null,
                fontSize: number = this.fontSize,  align: "left" | "center" | "right" = "center") {
            // 大小和位置
            let textRawWidth = Jimp.measureText(this.font, text) + 2
            let textRawHeight = Jimp.measureTextHeight(this.font, text) + 2
            // x = parseInt(x)
            // y = parseInt(y)
            let bgWidth = width // 背景宽度
            let textSize = fontSize > 0 ? fontSize : this.fontSize
            let textScale = textSize / this.fontSize
            // 颜色
            let textRGB = color ? Jimp.intToRGBA(Jimp.cssColorToHex(color)) : null
            let textBgHex = bgColor ? Jimp.cssColorToHex(bgColor) : null
            // 将文字放在透明的图片上
            let textImage = new Jimp(textRawWidth, this.fontSize + 2, 0x00000000)
            textImage.print(this.font, 1, 1, text)
            if (textRGB) {
                textImage.scan(0, 0, textRawWidth, textRawHeight, function (x: number, y: number, idx: number) {
                    let bitmap = textImage.bitmap
                    let red = bitmap.data[idx + 0]
                    let green = bitmap.data[idx + 1]
                    let blue = bitmap.data[idx + 2]
                    let alpha = bitmap.data[idx + 3]
                    if (x > 0 && y > 0 && x < bitmap.width && y < bitmap.height)
                        if (red === 0 && green === 0 && blue === 0 && alpha > 0) {
                            textImage.setPixelColor(Jimp.rgbaToInt(textRGB.r, textRGB.g, textRGB.b, alpha), x, y)
                        }
                })
            }
            // 缩放
            if (textScale !== 1) textImage.scale(textScale)
    
            if (textBgHex) {
                /* 生成背景 */
                let textScaleWidth = textRawWidth * textScale
                bgWidth = bgWidth || textScaleWidth
                let bgImage = new Jimp(bgWidth, textSize + 2, textBgHex)
                let textX = 0
                switch (align) {
                    case 'center':
                        textX = (bgWidth - textScaleWidth) / 2
                        break
                    case 'right':
                        textX = bgWidth - textScaleWidth
                        break
                }
                bgImage.composite(textImage, textX, 0)
                this.image.composite(bgImage, x, y)
            } else {
                this.image.composite(textImage, x, y)
            }
            return this.image
        }
    
        getBase64Async() {
            return this.image.getBase64Async(Jimp.AUTO)
        }
    }
    
  5. 调用

    serviceGenImage(path.join(__dirname, "images/owner.png"))
    

定时框架

脚本有了,健康码截图也有了,然后就是解放每日双手,程序自动运行,定时打卡了,ts下也有定时任务框架:cronsilkyTimer,相关对而言silkyTimer更符合我的口味,但是该框架还在测试阶段,稳定性不够。所以就选择了已经很成熟、稳定的corn,配置也很简单,就是个java下的corn一模一样,直接移植过来的,就不多介绍了。